アナパタ リターンズ その5:Knowledge Level (Accountability)


前回の続き。

Knowledge Level

オブジェクトがどのように振る舞うべきかについて述べてるオブジェクトのグループを Knowledge Level(知識レベル) として区別して識別する(別名 メタレベル)。


モデルは、構造や振る舞いをルール化する Knowledge Level と、問題領域でのできごとを記述する Operational Level(操作レベル) に分けて表現すると思考が明確になる。
Operational Level のオブジェクトは、Knowledge Level のオブジェクトにより どのように振る舞うかが規定され、これを付け替えることで振る舞いを変えることができる。


被雇用者(従業員)の例を以下に示す。

被雇用者は給与の支払い方法と、退職金積み立て方法の組み合わせで構築される。この組み合わせを表現するEmployee Typeがある。従業員がどのように振る舞うかをカスタマイズする場合には、支払いと退職金積立を組み合わせた新しいEmployee Typeを作成する。選択して配線することがシステムのコンフィグレーションとなる。



Accountability への Knowledge Level の適用

では前回の Accountability に知識レベルを適用する。以下は前回の Accountability。


Accountability の基本形は不安定である。これは Party の親子関係を規定する知識(ルール)がモデルとして表現されていないためである。Party の親子関係にビジネス上のルールを適用するには Knowledge Level を導入すると良い。


親と子の間に存在する説明責任がどのような関係であるかを表す型 Accountability Type は、 どのような Party が親と子の関係になれるかという知識である Connection Rule を持つ。Connection Rule は どのような Party のグループ(型)が、親子関係となることができるかというルールを Knowledge として表す。

オブジェクト図の例

2つの Accountability Type がある例を以下に示す。

appointment(委嘱) という Accountability Type は 顧問医 と 医者 の親に病院があることを許しており、つまり、病院組織は顧問医と医者を委嘱する責任があることを表す。

patient care(患者管理)という Accountability Type は 患者 の親に 顧問医 があることを許し、つまり顧問医は患者に対して患者管理を行う責任があることを表す。

ある責任関係の型 Accountability Type に対して、親子の関係を許容される Party Type を Connection Rule が規定している。親子関係となり得るルールが分離されているため、どのような責任の関係にあるかという Accountability Type とは別に、存在しうる親子関係を柔軟に規定できるようになる。


実装例

Party をグルーピングする型となる知識レベルのクラスである PartyType を導入する。

class PartyType extends NamedObject {
  public PartyType(String name) {
    super(name);
  }
}


Party は PartyType を持つ。

class Party ...
  private PartyType type;
  public Party(String name, PartyType type) {
    super(name);
    this.type = type;
  }
  PartyType type() {
    return type;
  }
}


AccountabilityType はいくつかの ConnectionRule を保持し、

class ConnectionAccountabilityType extends AccountabilityType ...
  Set<ConnectionRule> connectionRules = new HashSet<>();
  public ConnectionAccountabilityType(String name) {
    super(name);
  }
  void addConnectionRule (PartyType parent, PartyType child) {
    connectionRules.add(new ConnectionRule(parent, child));
  }


ConnectionRule は許可される親と子を規定する。

class ConnectionRule {
  PartyType allowedParent;
  PartyType allowedChild;
  public ConnectionRule(PartyType parent, PartyType child) {
    this.allowedChild = child;
    this.allowedParent = parent;
  }
}


以上で、各オブジェクトを組み合わせることができるようになった。

class Tester...
  private PartyType hospital = new PartyType("Hospital");
  private PartyType doctor = new PartyType("Doctor");
  private PartyType patient = new PartyType("Patient");
  private PartyType consultant = new PartyType("Consultant");
  
  private ConnectionAccountabilityType appointment 
    = new ConnectionAccountabilityType("Appointment");
  private ConnectionAccountabilityType supervision 
    = new ConnectionAccountabilityType("Supervises");

  public void setUp()...
    appointment.addConnectionRule(hospital, doctor);
    appointment.addConnectionRule(hospital, consultant);

    supervision.addConnectionRule(doctor, doctor);
    supervision.addConnectionRule(consultant, doctor);
    supervision.addConnectionRule(consultant, consultant);

    mark = new Party("mark", consultant);
    tom = new Party("tom", consultant);
    stMarys = new Party ("St Mary's", hospital);

では、ルールを適用するコードを入れていく。

class Accountability...
  static boolean canCreate (Party parent, Party child, AccountabilityType type) {
    if (parent.equals(child)) return false;
    if (parent.ancestorsInclude(child, type)) return false;
    return type.canCreateAccountability(parent, child);
  }

操作レベルのオブジェクトである Accountability の canCreateメソッドから、AccountabilityType の canCreateAccountability メソッドに委譲してルールの問い合わせを行う。


委譲された AccountabilityType では、

class AccountabilityType...
  boolean canCreateAccountability(Party parent, Party child) {
    return areValidPartyTypes(parent, child);
  }

class ConnectionRuleAccountabilityType  extends AccountabilityType ...
  protected boolean areValidPartyTypes(Party parent, Party child) {
    Iterator it = connectionRules.iterator();
    while (it.hasNext()) {
      ConnectionRule rule = (ConnectionRule) it.next();
      if (rule.isValid(parent, child)) return true;
    }
    return false; 
  }

areValidPartyTypes メソッドにて、自身の保持するルールで、指定された親と子が親子関係が許されるかを問い合わせる。


ConnectionRule には規定されたルールを記述。

class ConnectionRule...
  boolean isValid (Party parent, Party child) {
    return (parent.type().equals(allowedParent) &&
      child.type().equals(allowedChild));
  }


では、これらが協調して動作するか。

class Tester...
  public void testNoConnectionRule() {
    try {
      Accountability.create(mark, stMarys, appointment);
      fail("created accountability without connection rule");
    } catch (Exception ignore) {}
      assert(!stMarys.parents().contains(mark)); // am I paranoid?
    }

Levelled Accountability Type

ConnectionRule は、Accountability Type に規則を付与する一般的な方法だが、PartyType に厳格なレベル付けが必要になる場合がある。例えば、国(nation) - 州(state) - 郡(county) - 市(city) のような地域構造。
ConnectionRule にてこれを表現することもできるが、Accountability Type 上にレベル付けするほうがよりシンプルとなる。




LevelledAccountabilityType は内部にレベル付けされた PartyType を持ち、そのレベルに応じた親子関係を強制する。

class LevelledAccountabilityType extends AccountabilityType {
  private PartyType[] levels;
  public LevelledAccountabilityType(String name) {
    super(name);
  }
  void setLevels(PartyType[] arg) {
    levels = arg;
  }
  protected boolean areValidPartyTypes(Party parent, Party child) {
    for (int i=0; i<levels.length; i++) {
      if (parent.type().equals(levels[i]))
        return (child.type().equals(levels[i+1]));
    }
    return false;
  }
}


テストコードは以下。

public class LevelledTester extends junit.framework.TestCase {
  private PartyType nation = new PartyType("nation");
  private PartyType state = new PartyType("state");
  private PartyType county = new PartyType("county");
  private PartyType city = new PartyType("city");

  private Party usa, ma, nh, middlesex, melrose;
  private LevelledAccountabilityType region = new LevelledAccountabilityType();

  public LevelledTester(String name) {
    super(name);
  }

  public void setUp() {
    PartyType[] levels = {nation, state, county, city};
    usa = new Party("usa", nation);
    ma = new Party("ma", state);
    nh = new Party("nh", state);
    middlesex = new Party("usa", county);
    melrose = new Party("usa", city);

    region.setLevels(levels); // LevelledAccountabilityType

    Accountability.create(usa, ma, region);
    Accountability.create(usa, nh, region);
    Accountability.create(ma, middlesex, region);
    Accountability.create(middlesex, melrose, region);
  }
  public void testLevels() {
    assert(melrose.ancestorsInclude(ma, region)); 
  }
  public void testReversedLevels() {
    try {
      Accountability.create(ma, usa, region);
      fail();
    } catch (Exception ignore) {}
  }
  public void testSameLevels() {
    try {
      Accountability.create(ma, nh, region);
      fail();
    } catch (Exception ignore) {}
  }
  public void testSkipLevels() {
    try {
      Accountability.create(ma, melrose, region);
      fail();
    } catch (Exception ignore) {}
  }
}