-ドメイン層とDataSqueeze

昨日の例は、とても限られた条件の整った場合で、実際に同様のアダプターを作成する際にはいくつか悩みが出てくるはずです。たとえば、

  • オブジェクトにプロパティの数が多い場合
  • オブジェクトが他オブジェクトのファサードとなっていた場合
  • オブジェクトの初期化がコンストラクタだけでなく、プロパティや初期化メソッドを活用しなければならない場合

オブジェクトにプロパティの数が多い場合には、そのままページに全部ダンプするのが、たとえSerializeAdapterより効率がよかろうと、厳しいこともあると思われます。また、オブジェクト生成・初期化に複雑な手順が必要なときに、アダプターの内部でその生成ロジックを記述するべきではありません。なぜならスクイーズするオブジェクトが存在する以上、アプリケーション内部ですでにオブジェクト生成ロジックが用意されているからです。ドメイン層の操作を各所に散らす作りにするのは実装戦略上、好ましくありません。
Tapestryのドメイン層へのゲートウェイとしては、まず活用できるものに、Globalオブジェクトがあります。これはIEngine#getGlobal()で取得できます。

コラム:GlobalとVisitの管理

GlobalオブジェクトもVisitオブジェクトも、その管理を行うのはIEngineです。意外なことに両者ともにIEngineを実装するAbstractEngineのメンバーとなっています。ページオブジェクトから両者を取得する際には、ページからエンジンオブジェクトを経由して取得しています。Visitにアクセスを始めると、エンジンはHTTPセッションを作成し、TapestryサーブレットであるApplicationServletがエンジンを丸ごとセッションに保存するようになります。Visitにアクセスしていないとき、すなわちステートレスな状態では、エンジンはリクエストへの対応が終了するとApplicationServletの持つエンジンプールに戻されます。リクエストの度に呼び出されるIEngine#setupForRequest()メソッド内において、ServletContextにすでに作成されたGlobalオブジェクトが無いかをテストされます。すでにオブジェクトが有る場合にはそのオブジェクトの参照をエンジンのメンバーにセットして、getGlobal()メソッドの返値とします。無い場合には、IEngine#createGlobal()を呼び出します。この際、本編の議論で問題となるのは、getGlobal()の呼び出しでGlobalオブジェクトの用意がされるのではないことです。IEngine#setupForRequest()が実行される以前は、常にgetGlobal()の結果はnullとなってしまいます。

このGlobalオブジェクトを経由してドメイン層のアクセスを行うにあたり、コラム中でも指摘しているようにGlobalオブジェクトはエンジンの生存期間のある時点(#setupForRequest()からcreateGlobal()が実行された時)以降から利用できます。DataSqueezeよりドメイン層にアクセスするために、カスタムアダプターにGlobalの参照が欲しいところですが、これが直接は不可能です。なぜなら、DataSqueezeもまた、エンジンにそのライフサイクルを管理されていて、その生成は#setupForRequest()の中で、Globalオブジェクトの取得生成以前に行われます。順番の問題で、カスタムアダプターを登録するために用いるcreateDataSqueeze()メソッドからはGlobalオブジェクトに単純にはアクセスできません。もちろん、createDataSqueeze()の中でGlobalオブジェクトの初期化も行えばよいのですが、直後にTapestryの実装できちんと行われている作業を代替するのは冗長です。そこで、カスタムアダプターではその登録時(createDataSqueeze()メソッドで処理される時)には、踏み台としてエンジンの参照を取得しておくだけにとどめるのがテクニックとなります。

public class ManagerAdapter implements ISqueezeAdaptor {
  private IEngine engine;
  private UserOperation operation;
  public ManagerAdapter(IEngine engine) {
    this.engine = engine;
  }
  private void setup() {
    if(operation == null) {
      operation = (UserOperation)
        ( (S2Container)engine.getGlobal() ).getComponent(UserOperation.class);
    }
  }
  public void register(DataSqueezer squeezer) {
    squeezer.register(UserOperation.PREFIX_MANAGER, Manager.class, this);
  }
  public String squeeze(DataSqueezer squeezer, Object data) throws IOException {
    setup();
    return operation.squeezeManager((Manager)data);
  }
  public Object unsqueeze(DataSqueezer squeezer, String string) throws IOException {
    setup();
    return operation.unsqueezeManager(string);
  }
}

上記は、実際の案件コードから抽出してきた例です。Managerオブジェクトをスクイーズするのに、実際の作業はすべてオペレーションであるUserOperation#squeezeManager(Manager)/unsqueezeManager(String)を通して行っています。そのUserOperationは、Globalオブジェクトに登録されたS2Containerから取得しています。その他は先の例とまったく変わりません。
さて、UserOperationの中で行っている、スクイーズとその戻しロジックは以下のとおりです。

public String squeezeManager(Manager manager) {
  String id = manager.getUserID();
  return prefix + Base64Util.encode(id.getBytes());
}

public Manager unsqueezeManager(String string) throws IOException {
  String id = string.substring(1);
  id = new String(Base64Util.decode(id));
  return getManager(id);
}

UserOperation#unsqueezeManager()では最後にgetManager(String)というメソッドでオブジェクトを取得しています。このメソッドは、DAOを利用してDBよりオブジェクトを取り出しています。

public User getManager(String userID) {
  User user = dao.getUser(userID);
  if(user instanceof Manager) {
    return (Manager)user;
  }
  throw new UserRoleException(userID, USER_TYPE_MANAGER);
}

蛇足ですが、DAOの該当部分を抜粋すると以下のとおりです。ここではDbUtilsを利用しています。

public User getUser(String userID) {
  try {
    QueryRunner runner = new QueryRunner(dataSource);
    String sql = "select * from USERS where USERID = '" + userID + "'";
    return (User)runner.query(sql, new UserBeanHandler());
  } catch(Exception e) {
    throw new SystemException(e);
  }
}

このように、DBと連携したカスタムアダプターを作ることによって、複雑で大きなオブジェクトもページにダンプする内容はDBのプライマリキーだけになります。