DSL / Generation Gap


生成の乖離

一言要約

継承によって非生成コードから生成コードを別に分ける。

要約

  • 生成されるコードと手書きのコードは異なる扱いをしなければならない。
  • このパターンを最初に説明したのは、故ジョン・ブリシディーズであるが、ファウラーのはそれとは少し異なる。
    • ブリシディーズの説明では、手書きのコードは生成されたクラスのサブクラスだった。

どのように動作するのか

  • 基本形は、生成するスーパークラスと手書きのサブクラス。
  • 生成するスーパークラスでは抽象メソッドを使用することで、サブクラスのメソッドを呼び出すことができる。
  • よく見るパターンは、生成するクラスのスーパークラスとなる手書きのクラスを含めた3クラス構成。
  • 生成されるコードは編集しにくいので、可能な限り小さい方が良い。
  • 3クラスの概要
    • 手書きのベースクラス
      • コード生成時のパラメータに影響を受けない非可変ロジックを含む
    • 生成されるクラス
      • ロジックはパラメータから生成される
    • 手書きの具象クラス
      • 生成されるクラスに依存する
  • 3クラスはいつも必要というわけではない。
    • 非可変ロジックがなければ、手書きのベースクラスは不要。
    • 生成されたクラスをオーバーライドする必要がなければ、手書きの具象クラスは不要。
  • もう一つの理にかなった構成は、手書きのスーパークラスと生成されたサブクラス。
  • しばしば、生成クラスと手書きのクラスは複雑な構造になる。
  • これはコード生成の便宜に払う代償である。

※内容要確認

  • 生成の乖離の問題についての対策
    • 空の具象クラスを作成する。

いつ使うか

  • 生成の乖離は、別々のファイルに分割して1つの論理クラスを作成することができる効果的な技術である。
  • そのためには継承の仕組みのある言語で、オーバーライド可能な可視性にしておく必要がある。
  • 複数のファイルの中に1つのクラスのためのコードを置けるなら、生成の乖離の代替案となる。
    • C#のパーシャルクラスやRubyのオープンクラスなど。
    • C#のパーシャルクラスはオーバーライドする仕組みを与えない。
    • Rubyのオープンクラスは生成されたコードの後に手書きのコードを評価することにより制御する。
  • 生成の乖離に代わる一般的な初期のものは、「コード生成開始」と「コード生成終了」のようなコメントの間の領域に、コードを出力することだった。
    • この問題は、人が生成されたコード変更すると厄介なソースの差分管理を混乱させる。
    • 手書きコードと生成されたコードを別々のファイルに保つことは良い考えである。

※書籍追記分

  • 生成の乖離は良いアプローチだが、手書きコードと生成されたコードを別々のファイルに保つ唯一の方法ではない。
  • それぞれを呼び合う分割されたクラス2つでもうまく働く。
  • 共同して働くクラスは使用したり理解するにあたってより簡潔な仕組みであり、一般的にはこれを好む。
  • 相互作用がより複雑になる時にだけ、生成の乖離を推す。
    • 例えば、生成されたクラスに初期の振る舞いがあり、特別な場合だけオーバーライドしたい時。

例:データスキーマからクラスを生成する(Javaと少しのRuby)

例題としてデータスキーマからクラスを生成する場合を考える。データスキーマは以下の通り。

firstName : text
lastName : text
empID : int

サンプルデータ。

martin, fowler, 222
neal, ford, 718
rebecca, parsons, 20

スキーマに対するセマンティックモデル。

class Schema...
    attr_reader :name
    def initialize name
        @name = name
        @fields = []
    end

class Field...
    attr_accessor :name, :type
    def initialize name, type
        @name = name
        @type = type
    end

スキーマファイルの解析コード。

class Schema...
    def load input
      input.readlines.each {|line| load_line line }
    end

    def load_line line
      return if blank?(line)
      tokens = line.split ':'
      tokens.map! {|t| t.strip}
      @fields << Field.new(tokens[0], tokens[1])
    end

    def blank? line
       return line =~ /^\s*$/
    end

生成の乖離によって生成したいコード。

public class PersonDataGen extends AbstractData {
  
  private String firstName;
  public String getFirstName () {
    return firstName ;
  }
  public void setFirstName (String arg ) {
    firstName = arg;
  }
  protected void checkFirstName(Notification note) {};
  
  private String lastName;
  public String getLastName () {
    return lastName ;
  }
  public void setLastName (String arg ) {
    lastName = arg;
  }
  protected void checkLastName(Notification note) {};
  
  private int empID;
  public int getEmpID () {
    return empID ;
  }
  public void setEmpID (int arg ) {
    empID = arg;
  }
  protected void checkEmpID(Notification note) {};

生成コードのためのテンプレート。

public class <%=name%>DataGen extends AbstractData {
  <% @fields.each do |f| %>
  private <%= f.java_type %> <%= f.name %>;
  public <%=f.java_type%> <%=f.getter_name%> () {
    return <%=f.name%> ;
  }
  public void <%= f.setter_name %> (<%= f.java_type %> arg ) {
    <%= f.name %> = arg;
  }
  protected void <%= f.checker_name %>(Notification note) {};
  <% end %>
class Field...
    def java_type
      case @type
        when "text" : "String"
        when "int"  : "int"
        else raise "Unknown field type"
      end
    end

    def method_suffix
      @name[0..0].capitalize + @name[1..-1]
    end

    def getter_name
      "get#{method_suffix}"
    end

    def setter_name
      "set#{method_suffix}"
    end

    def checker_name
      "check#{method_suffix}"
    end

生成されたクラスを継承したクラス。

public class PersonData extends PersonDataGen {

  public String getLastName() {
    return capitalize(super.getLastName());
  }
  public String getFirstName() {
    return capitalize(super.getFirstName());
  }
  private String capitalize(String s) {
    StringBuilder result = new StringBuilder(s);
    result.replace(0,1, result.substring(0,1).toUpperCase());
    return result.toString();
  }
  public String getFullName() {
    return getFirstName() + " " + getLastName();
  }

スーパークラス。

class AbstractData...
  public Notification validate() {
    Notification note = new Notification();
    checkAllFields(note);
    checkClass(note);
    return note;
  }
  protected abstract void checkAllFields(Notification note);
  protected  void checkClass(Notification note) {}

生成されたクラス。

class PersonDataGen...
  protected void checkAllFields(Notification note) {
    
    checkFirstName (note);
    
    checkLastName (note);
    
    checkEmpID (note);
    
  }

チェックメソッドは空のメソッドをフックしているため、具象クラスでチェックメソッドをオーバーライドする。

class PersonData...
  protected void checkEmpID(Notification note) {
    if (getEmpID() < 1) note.error("Employee ID must be postitive");
  }

ファウラーへのフィードバック

担当者のつぶやき

翻訳&要約が微妙ですいません。

みんなの突っ込み