お久しぶりです。
最近タスクが溜まりすぎてブログや個人のプロジェクトに一切手が付けられていない状態ですが、生きております。
今回はそのタスクのうち、CyberNeko HTML ParserにてDOM解析をする機会があり、NodeListの設計に疑問を感じたのでそのお話をしていきます。
前提条件
Java 8u131をオブジェクト指向言語として使用し、ライブラリにCyberNeko HTML Parser 1.9.14を使用します。
問題点の前に
問題点をお話する前に、CyberNeko HTML Parserの簡単な使い方を説明しておきます。
まずは解析対象のHTMLファイルです。
<!-- test.html --> <html> <head> <title>Title</title> </head> <body> <div>1</div> <div>2</div> <div>3</div> <div>4</div> </body> </html>
次に、簡単なJavaコードです。
DOMParser parser = new DOMParser(); parser.setFeature("http://xml.org/sax/features/namespaces", false); parser.parse("test.html"); Document document = parser.getDocument(); NodeList nodeList = document.getElementsByTagName("div"); for (Integer i = 0; i < nodeList.getLength(); ++i) { String value = ((Element)nodeList.item(i)).getTextContent(); System.out.println(value); }
結果はこんな感じになるでしょう。
1 2 3 4
単純にdivタグの値を列挙してみました。
実行結果についてはさほど意味はありませんが、以下ではこのコードと対比していきますので簡単に載せておきます。
詳しい使い方についてはネットに情報がたくさんあるのでそちらを参照してください。
具体的な問題点
で、このコードの何が気持ち悪いかと言うと、for文でしかループ処理ができないんですよ。
オブジェクト指向言語では相手が管理する値に対しての処理は相手が行うのが一般的です。
今回はNodeListが管理する値ですのでNodeListが行うべき責務でしょう。
そもそもIterable
これではオブジェクト指向どころか使い勝手が悪いので専用のラッパークラスを作成します。
まずは内部で使用するIteratorクラスを作成します。
import java.util.Iterator; import org.w3c.dom.Element; import org.w3c.dom.NodeList; public class NodeListIterator implements Iterator<Element> { /** * 内部で使用するNodeListオブジェクトです。 */ private NodeList nodeList; /** * インデックス番号を管理するインスタンス変数です。 */ private Integer index = 0; /** * NodeListのイテレータを提供します。 * @param nodeList NodeListのオブジェクト */ public NodeListIterator(NodeList nodeList) { this.nodeList = nodeList; } /** * 次の値があるかどうかを返します。 */ @Override public boolean hasNext() { return this.index < this.nodeList.getLength(); } /** * 次の値をElement型として返します。 */ @Override public Element next() { return (Element)this.nodeList.item(this.index++); } }
内包したNodeListを利用して簡単なIteratorクラスを実装します。
今回はElement型しか利用しないのでElement型で定義しますが、必要に応じて変更してください。
IteratorクラスができるとあとはIterableクラスを作成するだけです。
値自体の操作はIteratorがやってくれるのでIterable
import java.util.Iterator; import org.w3c.dom.Element; import org.w3c.dom.NodeList; public class IterableNodeList implements Iterable<Element>, NodeList { /** * 内部で使用するNodeListです。 */ private NodeList nodeList; /** * NodeListのイテレータパターンをサポートします。 * @param nodeList NodeListのオブジェクト */ public IterableNodeList(NodeList nodeList) { this.nodeList = nodeList; } /** * イテレータオブジェクトを取得します。 */ @Override public Iterator<Element> iterator() { return new NodeListIterator(this.nodeList); } /** * インデックス番号の値をElement型として取得します。 * @param index アイテムを取得する際のインデックス番号 */ @Override public Element item(int index) { return (Element)this.nodeList.item(index); } /** * NodeListの長さを取得します。 */ @Override public int getLength() { return this.nodeList.getLength(); } }
一応NodeListとしても使用できるようにNodeListを継承してありますが、使わないのであれば特段必要ではありません。
これで拡張for文とforEachメソッドへの対応が完了しました。
拡張for文では内部で暗黙的にイテレータが使用されており、IteratorとIterableを用意すれば使えるようになります。
しかし、forEachメソッドの実装が見当たりませんね。
実はforEachメソッドはインタフェースに実装が定義されているのでオーバーライドなしで使用できます。
また、forEachメソッドは内部で拡張for文が使用されていますので絶対に両者の実装が必要になるというわけです。
ではこのクラスを用いて書き直してみましょう。
DOMParser parser = new DOMParser(); parser.setFeature("http://xml.org/sax/features/namespaces", false); parser.parse("test.html"); Document document = parser.getDocument(); IterableNodeList nodeList = new IterableNodeList(document.getElementsByTagName("div")); // 拡張for文 for (Element element : nodeList) { String value = element.getTextContent(); System.out.println(value); } // forEachメソッド nodeList.forEach((element) -> { String value = element.getTextContent(); System.out.println(value); });
めちゃくちゃスッキリ・・・。
ただ、new IterableNodeListとしなければならないところが少々臭います。
ここをNodeListで済ませることができればすんなり行くのですが・・・。
おわりに
以上がCyberNeko HTML Parser、ひいてはNodeListインタフェースのオブジェクト指向への導きでした。
そもそもの話、CyberNeko HTML Parserが悪いのではなく、NodeListインタフェースの設計が微妙なのだと思います。
NodeListインタフェースはListという名前を付けているのだからデフォルトで実装すべきではと思うのですが・・・。
公式インタフェースでこの差があるのはどうなのかと疑問に思えてしまいます。
オブジェクト指向言語という名を語るのであればもうちょっと何とかしてほしいところですが、何か理由があるのでしょうか。
とりあえずはこの実装で使うのがベストかなという印象です。