NUnit の説明用に作ってみたのですが、説明にしてはコードが難しいので、こっちにのほうに公開します。
コードに関しては、たった 400 行弱の実装で、同じぐらいの長さの NUnit のコードがあります。
最初は、あまりテストファーストで作らなかったので、無駄なコードが混じっているのですが、ひとまず作ったときの手順をさらして行きますね。もうちょっとまとめたら codeplex にでも公開します。
まず、基本クラスをざっと設計してしまいます。
1 2 3 4 5 6 7 8 9 10 11 | class EXDocument { XmlDocument _xmldoc ; } class EXElement { XmlElement _xmlelement ; } class EXElement : List<EXElement> class EXAttr { XmlAttribute _xmlattr ; } class EXAttrs : List<EXAttr> |
これだけですw 内部に XmlDocument/XmlElement を持たせて、それを参照して XPath 風に動かします。
C++ の時は、このあたりの実装も考えていたのですが、C# は面倒なので XML 関係はお任せにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | [Test] public void TestRoot() { EXDocument doc = new EXDocument(); doc.LoadXML( @" <members> <person>masuda</person> </members> " ); EXElement el = doc / "members" ; Assert.IsNotNull(el); Assert.AreEqual( "members" , el.Name); Assert.AreEqual( "" , el.Value); EXElements els = doc / "members" / "person" ; Assert.IsNotNull(els); Assert.AreEqual(1, els.Count); el = els[0]; Assert.IsNotNull(el); Assert.AreEqual( "person" , el.Name); Assert.AreEqual( "masuda" , el.Value); } |
最初のテストケースがこれです。
EXDocument::LoadXML は、単純に XmlDocument::LoadXML を呼び出すだけです。
このあたりは【テストファースト】でもありますが、この回の場合は UI 設計も同時にはいっています。
UI 設計という言葉を使うのは、EXDocument をどのように扱うのか?どのように扱いたいのか?というインターフェースの設計だからです。
XML データを読み込んだ後は、
1 2 3 4 5 | // ルート要素を取得する EXElement el = doc / "members"; // person 要素を取得する EXElements els = doc / "members" / "person"; |
という使い方がしたい、という(自分の)要件なのです。
さて、これをコンパイルしようとすると、まずは、「EXDocument と string で / 演算子は使って EXElement に変換できない」というコンパイルエラーがでます。
という訳で、仰せの通り EXDocument クラスに / 演算子を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public static EXElement operator /(EXDocument doc, string s) { if (doc._documentElement != null ) { if (doc._documentElement._xmlelement.Name == s) { return doc._documentElement; } else { return EXDocument._emptyElement; } } return null ; } |
_documentElement を参照しているのが冗長ですが、ルート要素のタグ名と引数の文字列を比較して、マッチしたらルート要素を返す。マッチしなかったら空の要素を返す、ってことにしています。
# 空の要素を返すのは、扱いが便利になる null object パターン(empty object パターン?)です。
# null ポインタを返してエラーにするよりも、何もしないものをさらっと返すほうが、呼び出し側のコードが短くなります。
これだけのコードで、
EXElement el = doc / “members”;
の部分は通ってしまいます。
更に、もうひとつの要件をクリアすためにコンパイルエラーを見ます。
「EXElement と string の / 演算子で、EXElements に変換できない」というコンパイルエラーです。
これも先のコードと同じように EXElement クラスに / 演算子を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public static EXElements operator /(EXElement el, string tag) { EXElements els = new EXElements(); foreach ( XmlNode nd in el._xmlelement.ChildNodes ) { XmlElement xl = nd as XmlElement; if ( xl != null ) { if ( xl.Name == tag ) { EXElement ne = el._document.CreateElement(); ne._xmlelement = xl; els.Add(ne); } } } return els; } |
ややこしいそうですが、やっていることは大したことはありません。
対応する XmlElement の子要素に対してタグ名にマッチするものをコレクションしているだけです。コレクションがばんばん作られますが、まぁ、中身の XmlElement オブジェクトはポインタとして参照するので、メモリ的には問題ないでしょう。
こんな風にすると、さっくりと、下記の2つのコードがコンパイルできるようになります。
1 2 3 4 5 | // ルート要素を取得する EXElement el = doc / "members"; // person 要素を取得する EXElements els = doc / "members" / "person"; |
お次は、EXElement とか EXElements のままだと使いづらいので、string に直接代入できるようにします。