S2JDBCでリッチドメインモデルはYes?No?

S2JDBCを使ってエンティティ内にビジネスロジックを定義する
リッチドメインモデルの手法は効果的なのだろうか?


現時点での私の考えはNo。
S2JDBCのようなレイジーロードをサポートしていないO/Rマッパーで
リッチドメインモデルを実現するのは実務的ではない。」のではないか
と思っています。


理由は『エンティティに定義したビジネスロジックは、
(主に関連をたどるような)データアクセスのロジックに強く依存する』ことに起因しています。


レイジーロードがある場合は、データアクセスのロジックを
透過的に扱うのでここで言う依存の問題は実質的に該当しません。
しかし、レイジーロードが無いアーキテクチャでは「ビジネスロジック」を
持つエンティティが「データアクセスのロジック」を持つアクションに
依存することなり、結果的にエンティティを呼ぶアクションとエンティティが
実質的に相互依存してしまいます。相互依存はなんとしても避けたい。


上手く説明できないので、サンプルのソースコードで改めて説明します。

@Entity
public class Department {

    @Id
    @GeneratedValue
    public Integer id;

    public String name;

    @OneToMany(mappedBy = "department")
    public List<Employee> employeeItems;

    /* 部署に所属する社員の平均給与を算出します。 */
    public Integer getAverageSalary() {
        if (!exsistEmployeeItems()) {
            return 0;
        }

        int totalSalary = 0;
        for (Employee e : employeeItems) {
            totalSalary += e.salary;
        }
        return totalSalary / employeeItems.size();
    }

    private boolean exsistEmployeeItems() {
        return !CollectionUtil.isEmpty(employeeItems);
    }
}


上記は、部署エンティティを示すソースコードです。
DepartmentクラスのgetAverageSalaryメソッドが
「エンティティに定義したビジネスロジック」に相当します。


このメソッドを作成する人は、@OneToManyアノテーションが付いている
employeeItemsプロパティをフェッチせずにこのメソッドが呼ばれた
時のことを想定しなければなりません。
実際には、例外にすべきか、それとも、null を返すべきか、
もしくは、数値であれば 0 を返すべきか、などで迷うと思われます。


一応、ドメインモデル大好き派の人からアドバイスをもらって、
今回の例では0を返すようにしました。しかし、今思えば、レイジーロードありきの
アーキテクチャにおける定石を前提としたアドバイスのような気がしてならない。
(そもそもという話はありますが、レイジーロード無しアーキテクチャにおいては、
関連エンティティがなければ例外にするのが良いと思う。)


それはさておき、このgetAverageSalaryメソッドを外部から呼ぶコードは以下のとおりです。

    Department dept = jdbcManager
            .from(Department.class)
            .id(departmentId)
            .leftOuterJoin("employeeItems") // ★ポイント
            .getSingleResult();
    averageSalary = dept.getAverageSalary();


ポイントは『.leftOuterJoin("employeeItems")』の箇所です。
@OneToManyアノテーションが付いているDepartmentクラスの
employeeItemsプロパティを関連エンティティとしてフェッチ
することの指定がこの処理の内容です。


問題は、employeeItemsプロパティをフェッチする処理が必要であるかどうかは、
getAverageSalaryメソッドの内部を調べないと分からないことです。
これは、先ほど申し上げたクラス同士の実質的な相互依存は起因しています。


これくらいだったら、なんとかなると思う人もいると思うので、
もう少し複雑な例を見てみましょう。

@Entity
public class Employee {

    @Id
    @GeneratedValue
    public Integer id;

    public String name;

    public Integer salary;

    public Integer departmentId;

    @ManyToOne
    public Department department;

    /* 所属部署の平均給与と自分自身の給与との差分を算出します。 */
    public int getDiffrenceAverageSalary()  {
        if (department == null) {
            return 0 - salary;
        }
        return department.getAverageSalary() - salary;
    }
}


上記はEmployeeエンティティのソースコードです。
getDiffrenceAverageSalaryメソッドは
『所属部署の平均給与と自分自身の給与との差分を算出します。』
という責務を持つ「エンティティに定義したビジネスロジック」です。
これまた departmentプロパティやdepartment.employeeItemsプロパティが
フェッチされていない時の対処方に悩まされます。


さらに、getで始まるメソッド名にしていれば、フレームワークやユーティリィティが
プロパティとして認識するので、リフレクション経由で不用意に呼び出されても、
困らないように対策を施す必要があります。


getDiffrenceAverageSalaryメソッドをアクションとかから
呼び出すには以下のようなコードになります。

    Employee emp = jdbcManager
        .from(Employee.class)
        .id(id)
        .leftOuterJoin("department") // ★ポイント
        .leftOuterJoin("department.employeeItems") // ★ポイント
        .getSingleResult();
    int diffrenceAverageSalary = emp.getDiffrenceAverageSalary();


Employeeエンティティは2段階にフェッチジョインしなければいけません。
コードレベルではたった2行増えるだけですが、この2行を書くために、
Employee#getDiffrenceAverageSalaryメソッドとそこから呼ばれている
Department#getAverageSalaryメソッドの内部構造を把握する必要があります。


これでは、エンティティに存在するメソッドを気軽に呼ぶことができず、
生産性の向上につながりません。


結論としては、レイジーロードをサポートしていないO/Rマッパーでは、
リッチドメインモデルの採用を見送り、トランザクションスクリプト
採用するのが無難で良い選択肢だと思っています。


実際のトランザクションスクリプトのイメージとしては、
アクションクラスやロジッククラスで
データアクセスのロジックとビジネスロジックをペアで扱う感じです。



いろいろ書かせて頂きましたが、自分もまだまだ
定石を探るべく試行錯誤中で、激しく勘違いしている可能性もあるので、
何か気づいたことがあれば、気軽にアドバイスを頂ければ、嬉しいです。