モチベーション
次のような何かデータベース上のテーブル行を表す、ありがちそうなクラスを考える。
# python じゃないよー。Booだよー。
class Person:
public Id as int
public Name as string
public Age as int
# その他いろんなもの
# 指定idのデータを取得する static メソッド。
public static def Get( id as int ) as Person:
# ...
きっとこのテーブルは、次のような同様のクラスからIdを使って参照されている。データベースに乗っていたり、シリアライズされたりするだろうから、Person を直接参照することはできない。
class Family:
public FatherId as int # PersonのId
public MotherId as int # PersonのId
# その他いろんなもの
このFamilyクラスインスタンスから、各メンバーのPersonを取得するのは簡単だけど面倒くさい。
f as Family
print Person.Get(f.FatherId).Name
print Person.Get(f.MotherId).Age # 女性の年齢を尋ねるとは不謹慎な!
Personから更に別のIdを使った参照があるとダラダラと Get() なメソッドが連鎖してしまう。
目標
上のFamilyが次のようなコードとなるような自動化の仕組み。
class Family:
public FatherId as int
public MotherId as int
public Father as Person: # このプロパティをコンパイル時に自動挿入
get:
return Person.Get(FatherId)
public Mother as Person: # このプロパティをコンパイル時に自動挿入
get:
return Person.Get(MotherId)
解決方法:GetするPropertyを勝手に追加してくれる属性を作る。
コンパイル時にGet()してくれるプロパティ構文木を追加する次の属性を作る。
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
# [IdField(クラス名)] とすると "return クラス名.Get()" というプロパティを追加生成する属性。
# AbstractAstAttribute は構文木を書き換える属性のためのクラス。Apply()をオーバーライドして属性適用時の処理を書く。
public class IdFieldAttribute( AbstractAstAttribute ):
private refclass as ReferenceExpression
def constructor( cls as ReferenceExpression ):
refclass = cls
def Apply( node as Boo.Lang.Compiler.Ast.Node ):
# c = 属性のついた構文木ノードの直近のclass定義
c = node.GetAncestor[of ClassDefinition]()
assert c != null
# idf: 属性のついたノードがフィールドかどうか。Idで終わっているかどうか等チェック。
idf = node as Field # フィールドの構文木ノードクラスは Boo.Lagn.Compiler.Ast.Field
return if idf == null or not idf.Name.EndsWith( 'Id' ) or idf.Name == 'Id' or not idf.IsPublic or idf.IsStatic
# フィールド名から Id を除いた名前でプロパティ構文木生成。
n = idf.Name[:-2]
pn = ReferenceExpression( n )
prop = [|
public $(pn) as $(refclass):
get:
return $(refclass).Get($(ReferenceExpression(idf.Name)))
|]
prop.LexicalInfo = idf.LexicalInfo
# 作ったプロパティをクラスにメンバとして追加する。
c.Members.Add( prop )
このソースを IdFieldAttribute.dll としてビルドする。次のような感じ。
$ booc -o:IdFieldAttribute.dll IdFieldAttribute.boo
このdllを他のプログラムコンパイル時に参照することで、次のようにIdField 属性を使用出来る。Person については前述のものと同じ。
class Family:
[IdField(Person)] # Father as Person というプロパティができる
public FatherId as int
[IdField(Person)] # Mother as Person というプロパティができる
public MotherId as int
f = Family( FamilyName:'Taira', FatherId:1, MotherId:2 )
print f.Father.Name # f.Father で直接 Person が取得できる。
print f.Mother.Name # 女性の年齢を聞くのはやめよう。
注意点
名前違いのプロパティを構文木レベルで追加しているだけなので、同名のメンバーがすでに定義されていたら、当然コンパイルエラー。
IQuackFu
実は IQuackFu インターフェースを使うと、上記の方法よりももっと簡単でメンテナンス性のいい実装になったりはする。
ただ、IQuackFu は型指定できない(objectだけ)ので、型違いのエラーを実行時にしか検出できない。
結論
まぁまぁ便利だけど、この IdField はどうしてもほしい物か?と聞かれるとやや疑問ではあるかも。
好き勝手な属性を作り続けると、何をやってるのかわからない大変なプログラムになるかもしれない。
ただ、このぐらいなら許されるような気がする。