一連の業務ルールを通じてロジックを構成し、それぞれには条件とアクションを持っている。
if candidate is of good stock and candidate is a productive member of society then candidate is worthy of an interview if candidate's father is a member then candidate is of good stock if candidate is English and candidate makes ten thousand a year then candidate is a productive member of society
「◯◯だったら△△する」のような条件テストを行うケースが多々あり、Production Rule Systemは1つの条件に対して1つのアクションを持つ。
どのように動作するか
基本構造は一連のルールがあり、各ルールは条件とアクションで構成されるシンプルなもの。条件はブーリアン式でアクションは任意。
Production Rule Systemの複雑なところは、ルールの実行方法を決定する部分。通常はルール実行の制御を単一のコンポーネントで行い、ルールエンジンや推論エンジン、スケジューラなどと呼ばれる。条件がtrueを返すと活性化(activated)すると言い、ルールのアクションを実行することは発動する(firing)と言う。
ルールのシーケンスは、任意の順序(最もシンプル)、システムで定義された順序(メールフィルタなど)、もしくは優先順位による順序で発動される。優先順位による順序づけは問題になることがある(smell)ので、本当に適しているかどうかを見直すべき。 連鎖 最もシンプルなProduction Rule Systemでのルール検証は、全部のルールをスキャンしてエラーがあればログはNotificationで通知する方式。
しかし、ルールのアクションによって全体の状態が変化することが多いため、ルールを再評価する必要がある。このルール間の相互作用は前向き連鎖(forward-chaining)と呼ばれる。また、ゴールから開始してどのルールがtrueになると目的のアクションに辿りつけるかを見つける方法を後向き連鎖(backward chaining)と言うが、あまり一般的ではないので本書では扱わない。
Contradictory Inferences
Patterns in Rule Structure
例:バリデーション(C#)
例題として、架空のビクトリア英語クラブに参加するアプリケーション考える。バリデーションルールは以下のような感じ。
モデル
必要なモデルは、まず人に関する情報。
class Person... public string Name { get; set; } public University? University { get; set; } public int? AnnualIncome { get; set; } public Country? Nationality { get; set; }
バリデーションエンジンの基本的な構造はバリデーションルールのリスト。
class ValidationEngine... List <ValidationRule> rules = new List<ValidationRule>();
interface ValidationRule { void Check(Notification note, Person p); }
エンジンを実行するには、各ルールを実行してNotificationで結果を集める。
class ValidationEngine... public Notification Run(Person p) { var result = new Notification(); foreach (var r in rules) r.Check(result, p); return result; }
最も基本なバリデーションは1つの述語(predicate)と、失敗した時のメッセージ記録。
class ExpressionValidationRule : ValidationRule { readonly Predicate<Person> condition; readonly string description; public ExpressionValidationRule(Predicate<Person> condition, string description) { this.condition = condition; this.description = description; } public void Check(Notification note, Person p) { if (! condition(p)) note.AddError(String.Format("Validation '{0}' failed.", description)); }
実際に実行するには以下のようにコマンドクエリインターフェースを使う。
engine = new ValidationEngine(); engine.AddRule(p => p.Nationality != null, "Missing Nationality"); var tim = new Person("Tim"); var note = engine.Run(john);
パーサー
内部DSLをC#のlambdaを使って表す。
class ExampleValidation : ValidationEngineBuilder { protected override void build() { Validate("Annual Income is present") .With(p => p.AnnualIncome != null); Validate("positive Annual Income") .With(p => p.AnnualIncome >= 0);
ビルダを作成しエンジンを設定する。Validateメソッドにて子のルールビルダを設定する。
abstract class ValidationEngineBuilder { public ValidationEngine Engine { get; private set; } protected ValidationEngineBuilder() { Engine = new ValidationEngine(); build(); } abstract protected void build(); protected WithParser Validate(string description) { return new ValidationRuleBuilder(description, Engine); } class ValidationEngine... public void AddRule(Predicate<Person> condition, string errorMessage) { rules.Add(new ExpressionValidationRule(condition, errorMessage)); }
interface WithParser { void With(Predicate<Person> condition); }
DSLを進化させる
NULLチェックを捕捉する良い方法について考える。ひとつの方法は文字列としてプロパティ名を書いて、ロジックのチェックにリフレクションを使うこと。
MustHave("University");
DSLでこう書けるようにするには、モデルとパーサを増やす必要がある。
class ValidationEngineBuilder... protected void MustHave(string property) { Engine.AddNotNullRule(property); } class ValidationEngine... public void AddNotNullRule(string property) { rules.Add(new NotNullValidationRule(property)); }
class NotNullValidationRule : ValidationRule { readonly string property; public NotNullValidationRule(string property) { this.property = property; } public void Check(Notification note, Person p) { var prop = typeof(Person).GetProperty(property); if (null == prop.GetValue(p, null)) note.AddError("No value for {0}", property); }
ポイントはSemantic Modelを変更する必要はなかったこと。単にビルダに少しコードを追加するだけ。
class ValidationEngineBuilder... protected void MustHaveALT(string property) { PropertyInfo prop = typeof(Person).GetProperty(property); Engine.AddRule(p => prop.GetValue(p, null) != null, String.Format("Should have {0}", property)); }
ビルダにロジックを入れるのは多くの場合それほど影響はないが、本来はSemantic Modelにロジックを入れたほうが、Semantic Modelを見る機会が増えて理解を深めるのに役立つ。
例:適性ルール(C#)
セレブ向けクラブの入会条件を考える。