アリスはリフレクションがお嫌い | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/8092
の続きで、属性/アノテーションの話を追記。
リフレクションはクラスに対して static な情報だということが解ったら、じゃあ「属性」ってのも static な情報というのが分かるので、このあたりは同根なんだなというのが分かる。ケント・ベック氏の「テスト駆動開発入門」に、とあるプログラム言語を学ぼうとしたときに自動テストツール(xUnit)を書いてみると、パターンが網羅されていて学びやすい、ということが書いてある。実際、xUnit のツールにはデザインパターンが多く使われていて、その実現をそれぞれのプログラム言語がどうやって実装しているのかというのを見ることができる。
よくあるテストメソッドとテストクラスの情報収集の方法に
– C++ の場合は、あらかじめ登録が必要
– Java の場合は、先頭にTestがくっついている
– C# の場合は [TestMethod] 属性を付ける
という違いがある。今だともうちょっと違っているけど、初期のころの xUnit のコードを見ると、その当時の文法で駆使されていて、その後にちょっとずつ他の言語の流儀が取り入れられているのが分かります。
C++ の場合は、リフレクションがないので、テストメソッドをいちいち登録するしかない…けど、テンプレートを駆使して明示的には登録しているようにみえないけど、実は登録してるって方法を取ってる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | TEST_CLASS(UnitTest1) { public : TEST_METHOD(TestMethod1) { auto m = new Prada(); m->setStyle(10); m->setColor( "Pink" ); auto plist = m->properties; for each ( auto prop in plist ) { auto n = prop.first; auto v = prop.second; void *x = v(m); } } }; |
最近の VC++ でテストプロジェクトを作ると、TEST_CLASSとTEST_METHODというマクロが定義されていて、うまく登録できるようになっている。
初期の Java の場合は、属性/アノテーションがなかったので、実行したいテストメソッドと普通のメソッドを区別するために、メソッドの先頭に「Test」とつけておく。でもって、リフレクションを使って先頭が Test になっているメソッドだけを実行するという方式をとる。
これは、結構シンプルでよいのだけど、メソッドの名前自体に意味を持たせてしまうのが、なんだかなーという感じだったので、最近の JUnit では、「@Test」というアノテーションが付けられる。
1 2 3 4 5 6 7 8 | public class JunitExampleTest { @Test public void testFoo() { JunitExample example = new JunitExample(); assertEquals(1, example.foo()) } } |
Java の後発になる C# の場合は、リフレクションだけでは足りないとみて、属性(Attribute)というのものを付けている。実際の使い方はアノテーションと同じだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { var m = new Prada { Style = 10, Color = "Pink" }; var ti = m.GetType().GetTypeInfo(); var plist = ti.DeclaredProperties; foreach ( var prop in plist) { var n = prop.Name; var v = prop.GetValue(m); Debug.WriteLine($ "prop: {n} value: {v}" ); } } } |
当初、属性というものが使われていたときに、他の言語にはアノテーション自体の機能がなかったのと、アスペクト指向が流行り始めていて、そのアノテーション(装飾)と区別がつかなかったという経緯もあって、なんかいまいちな言語仕様な感じがしたのだが、MVCパターンのように動的に Action メソッドを呼び出すようなスタイルを考えたとき、ちょっと便利に使える。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | [Produces( "application/json" )] [Route( "api/People" )] public class PeopleApiController : Controller { private readonly ApplicationDbContext _context; public PeopleApiController(ApplicationDbContext context) { _context = context; } // GET: api/People [HttpGet] public IEnumerable<Person> GetPerson() { return _context.Person; } // GET: api/People [HttpGet] [Route( "search/name/{value}" )] public IEnumerable<Person> GetPersonByName([FromRoute] string value ) { return _context.Person.Where( t => t.Name.Contains(value)); } // GET: api/People [HttpGet] [Route( "search/age/{value}" )] public IEnumerable<Person> GetPersonByAge([FromRoute] int value) { return _context.Person.Where(t => t.Age >= value); } |
これは、ASP.NET MVCアプリケーションのルーティングのテスト用に作ったサンプルコードだが、たくさんの属性が使われている。要は、クラスに対して static な情報を属性に詰め込めばよいので、Produces属性でJSON形式を返すとか、Route属性でURL呼び出しの形式を設定するとか、HttpGet属性でGETメソッドだけに呼び出しを限るとかの設定ができる。
これらの情報はDI(インジェクション)という形で、設定ファイルなどに追い出すことも可能なのだが、そもそも設定ファイルとこのControllerクラスが1対1になっているのだったら、その「設定」自体をコードで書こうと外部の設定ファイルに書こうと意味は同じはずだ。ならば、属性で、ということになる。当然、コードに埋め込まれてしまう欠点もあって、ちょっとだけルーティングを変えようと思っても、このコードを変えないといけないので、そういう場合は DI のように設定自体を外付けしてしまったほうがよい。
これは、属性とDIのどちらがいいという話ではなくて、利用するときの状況によりけり、というパターンかなと思っている。一般的な使い分けとしては、コーディング時に決定してしまうもの(あとからの設定では変えられないもの、代替できないもの)は属性に、動作環境によって変わるもの(データベースの接続文字列とか)は設定ファイルに出せばよい。
属性を使わないで実装する
じゃあ、本題として、ルイスはアリスが属性/アノテーションが嫌いだから、属性を使わないで自前で実装するんやという話になっているので、ASP.NET MVC っぽい Action メソッドの呼び出しをどうやって実装するのかを考えてみる。初期の JUnit のように諸々の規約をメソッド名に追加してもよいのだが、あれこれ設定するのは大変だ。
1 2 3 4 5 6 7 8 9 10 11 12 | class AliceAttribute : Attribute { } class DefaultAttribute : Attribute { public string Name { get ; set ; } } [Alice] class Prada { public int Style { get ; set ; } [Default(Name = "White" )] public string Color { get ; set ; } } |
こんな風な Alice 専用の Prada クラスを作ってみる。クラスの属性に「Alice」を付けておくと、Alice 専用のクラスという訳だ。ついていない場合は、普通のクラス。
あと、デフォルトの値を設定できるように、Default属性を作ってみる。
1 2 3 4 5 6 7 8 9 10 11 | public void TestMethod3() { var m = new Prada { Style = 10, Color = "Pink" }; var ti = m.GetType().GetTypeInfo(); var attr1 = ti.GetCustomAttribute<AliceAttribute>(); var attr2 = ti.GetProperty( "Color" ).GetCustomAttribute<DefaultAttribute>(); Debug.WriteLine($ "prop: Color attr: {attr2.Name}" ); } |
C# での属性呼び出しは、GetCustomAttributeメソッドを使えばよい。GetCustomAttributeで null が返ってきたら属性が設定されていないという訳で、Alice 属性がついているかどうかが分かる。全ての属性を取りたい場合は、GetCustomAttributes メソッドを使うが、どんな属性にも対応するということはないのであまり使わないだろう。クラス特性を調べるときとかに使うかな。
Default 属性で設定した Name プロパティは、そのまま属性オブジェクトの Name プロパティで見れる。この属性オブジェクトが、クラスやメソッドに設定したときにクラス情報と一緒にくっついてくるというイメージでよい。結局のところ、GetTypeInfoメソッドを呼び出してクラス情報を取るので、リフレクションと同じ方式になる。
これを C++ で書き直してみると、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class AliceAttribute {}; class DefaultAttribute { public : std:: string Name; DefaultAttribute(std:: string &name) { this ->Name = name; } }; class Prada2 { private : int _style = 0; std:: string _color = "" ; public : int getStyle() { return _style; } void setStyle( int v) { _style = v; } std:: string getColor() { return _color; } void setColor(std:: string v) { _color = v; } public : static AliceAttribute *Alice; static DefaultAttribute *DefaultColor; Prada2() { Prada2::Alice = new AliceAttribute(); Prada2::DefaultColor = new DefaultAttribute(std:: string ( "White" )); } }; TEST_CLASS(UnitTest2) { public : TEST_METHOD(TestMethod1) { auto m = new Prada2(); m->setStyle(10); m->setColor( "Pink" ); auto alice = m->Alice; auto color = m->DefaultColor->Name; } }; |
適当な属性クラス(AliceAttribute、DefaultAttribute)を用意して、Prada2クラスの static メンバとして設定しておく。汎用的に作る場合は、リフレクションのときと同じように map を使うのだが、クラス自身の属性とメンバの属性があるので2つ作らないといけないのがちょっと面倒…だけでも、まあできないことはない程度にはわかるだろう。
逆にいえば、クラスの情報として、static で設定しているメンバ変数は属性に置き換えられる、ということだ。リフレクションを使うので、呼び出し側でひと手間かかるが、この設定の命名規約があれこれと悩むよりも、Attribute クラスを適当に作ってしまって属性に対する設定のほうで、あれこれと変えるということもできるということだ。
そういう意味で、
– クラスが外部公開する const 変数
– 属性
– DI の設定
を使い分けていくことになる。