コードの断片を受け取ってパースツリーを構築し、それをDSLで操作する。
IMAPの検索コマンド
SEARCH subject "entity framework" sentsince 23-jun-2008 not from "@ayende.com"
C#のDSLイメージ
var threshold = new DateTime(2008, 06, 23); var builder = new ImapQueryBuilder((q) => (q.Subject == "entity framework") && (q.Date >= threshold) && ("@ayende.com" != q.From));
意味論モデル
クエリオブジェクト
class ImapQuery... internal List<ImapQueryElement> elements = new List<ImapQueryElement>(); public void AddElement(ImapQueryElement element) { elements.Add(element); }
クエリオブジェクトに渡されるインタフェースとその実装
interface ImapQueryElement { string ToImap(); }
class BasicElement : ImapQueryElement { private readonly string name; private readonly object value; public BasicElement(string name, object value) { this.name = name.ToLower(); this.value = value; validate().AssertOK(); } class NegationElement : ImapQueryElement { private readonly BasicElement child; public NegationElement(BasicElement child) { this.child = child; }
これを用いたクエリモデルの表現
var expected = new ImapQuery(); expected.AddElement(new BasicElement("subject", "entity framework")); expected.AddElement(new BasicElement("since", new DateTime(2008, 6, 23))); expected.AddElement(new NegationElement( new BasicElement("from", "@ayende.com")));
IMAPクエリの生成
class ImapQuery... public string ToImap() { var result = ""; foreach (var e in elements) result += e.ToImap(); return result.Trim(); } class BasicElement... public string ToImap() { return String.Format("{0} {1} ", name, imapValue); } private string imapValue { get { if (value is string) return "\"" + value + "\""; if (value is DateTime) return imapDate((DateTime)value); return ""; } } private string imapDate(DateTime d) { return d.ToString("dd-MMM-yyyy"); } class NegationElement... public string ToImap() { return String.Format("not {0}", child.ToImap()); }
C#での構築
ビルダはコンストラクタでlambdaをとる。
class ImapQueryBuilder... private Expression<Func<ImapQueryCriteria, bool>> lambda; public ImapQueryBuilder(Expression<Func<ImapQueryCriteria, bool>> func) { lambda = func; }
class ImapQueryBuilder... internal class ImapQueryCriteria { public string Subject {get { return ""; }} public string To {get { return ""; }} public DateTime Sent {get { return DateTime.Now; }} public string From {get { return ""; }}
class ImapQueryBuilder... public ImapQuery Content { get { if (null == content) { content = new ImapQuery(); populateFrom(lambda.Body); } return content; } } private ImapQuery content;
populateFromが肝。再帰的にツリーをたどっている。
class ImapQueryBuilder... private void populateFrom(Expression e) { var node = e as BinaryExpression; if (null == node) throw new BuilderException("Wrong node class", node); if (e.NodeType == ExpressionType.AndAlso) { populateFrom(node.Left); populateFrom(node.Right); } else content.AddElement(new ElementBuilder(node).Content); }
サンプル実装のふりかえり
説明と構築方法の違いについて。説明の部分ごとに実装を分けたが、実際の構築時にはその方法はとらなかった。
IMAP DSLの構築において解析木の走査を行わなかった。代替としてMethod Chainingを利用。
class Tester... var builder = new ChainingBuilder() .subject("entity framework") .not.from("@ayende.com") .since(threshold);
ビルダの実装は以下の通り。
class ChainingBuilder... private readonly ImapQuery content = new ImapQuery(); private bool currentlyNegating = false; public ImapQuery Content { get { return content; } } public ChainingBuilder not { get { currentlyNegating = true; return this; } } private void addElement(string keyword, object value) { ImapQueryElement element = new BasicElement(keyword, value); if (currentlyNegating) { element = new NegationElement((BasicElement) element); currentlyNegating = false; } content.AddElement(element); } public ChainingBuilder subject(string s) { addElement("subject", s); return this; } public ChainingBuilder since(DateTime t) { addElement("since", t); return this; } public ChainingBuilder from(string s) { addElement("from", s); return this; }
このようにした理由の一つは内部DSLの構造がIMAPクエリに似ている方がシンプルになるから。