モデル上に明示されたトランザクションとしてのAggregate

最初に読んだ時には今ひとつ理解できなかったAggregate。社内のDDD読書会で読み返してみて感じたのは、「Aggregateは、従来の手続き的な一貫性維持に関する情報(トランザクションスコープ含む)をモデル上に明示したものである」と捉えることで理解しやすくなる、ということ。以下もう少し詳しくみていく。

Aggregateの定義と位置づけ
DDDのGlossaryを見るとAggregateは、
A cluster of associated objects that are treated as a unit for the purpose of data changes. External references are restricted to one member of the AGGREGATE, designated as the root. A set of consistency rules applies within the AGGREGATE'S boundaries.
データ変更を行う上で1単位として扱う必要があるような、相互に関連するオブジェクト群。外部からのAGGREGATEへの参照は、ルートとして識別されたものだけに制限される。一貫性を保つためのルールの集合は、AGGREGATE境界の内部に対して適用される。

このように説明されており、データの一貫性を保つ単位であることが強調されている。しかし、本を流し読みしただけでは、Repositoryと同レベルのビルディングブロックとして扱うほどの重要性は感じない。冒頭に述べたとおり、Aggregateの位置づけやその重要性は、従来のトランザクション設計と対比させることでわかりやすくなるのではないかと思う。


手続き型トランザクション
まず最初に、従来の手続き型アプリケーションでのトランザクション設計を考えてみたい。従来自分が行っていた典型的なトランザクション設計は、手続きの範囲を指定することでトランザクションスコープを設定し、その内部で主にデータモデルに対して楽観・悲観の同時実行制御を行う、というものだった。DDDの構成要素で言えば、アプリケーション層のService(のメソッド)をトランザクション境界として設計し、SQLやストアドプロシージャでロックをかけることが多い。

DDDのAggregateで紹介されている、注文(PurchaseOrder)、注文明細(PurchaseOrderLineItem)、商品(Product)の例を使った例を考えてみる。

[単純な注文のモデル]




(残念ながら自分が)よく見るコードはこんな感じだろうか。

[AddPurchaseOrderLineItemService]
public class AddPurchaseOrderLineItemService {
    @Transactional
    public void addNewItem(Command cmd) {
        ...
        // PurchaseOrderとPOLineItemを結合したオブジェクトのList
        List<PurchaseOrderResult> porList = poDao.findForUpdate(criteria);
        ...
        PurchaseOrderLineItem newItem = new PurchaseOrderLineItem(poList.get(0).getId(), ...);
        ...
        // 内部ではinsert
        if (porList.get(0).getLimit() >= (totalPrice(porList) + newItem.getPrice())) {
            poLineItemDao.insert(newItem);
        }
        ...
    }
}
上記のコードでは、一貫性維持に関する知識は…
  • 一貫性維持の範囲
    • => ApplicationService上の@Transactionalアノテーション
  • 一貫性維持範囲内で守るべき不変式
    • => if文の条件式
  • 同時実行制御戦略
    • => DAOのメソッドによるロック
これらの中に分散して存在し、モデル(データモデルorオブジェクトモデル)上ではまったく表現されていない。
さらに、ApplicationServiceはユーザーイベント単位に作成されることが多いため、同じモデル(ここではPurchaseOrder)の一貫性に関する情報であるにも関わらず、ユーザーイベントごとにも散逸してしまうことになる。


一貫性の知識を手続きからモデルへ

DDDでは、ドメイン知識はドメイン層のモデルとして表現するのが原則であり、重要な知識であればモデル上で明示的に表現することを強く推奨している。この方針に沿って上記の問題を解決しようとすれば、自然とモデル上への一貫性を表現する単位の導入に辿り着く。具体的には…
  • 一貫性維持の範囲
    • => 維持範囲のモデル表現としてAggregateを導入する
  • 一貫性維持範囲内で守るべき不変式
    • => Aggregateルートの変更操作内に移動
    • => グローバルなルールや、ユビキタス言語に登場すべきルールであれば、Rule/Specificationの導入を検討
  • 同時実行制御戦略
    • => モデル側で表現
    • => ドメイン知識ではないので、外部に追い出す
以下、具体的に見ていく。

一貫性維持の範囲を明示
最終的にはService上で手続きに対する一貫性制約を明示することにはなると思うが、まずはモデルに対して一貫性の維持範囲を設定し、必要に応じてそれらをServiceで調整する、という方針に転換する。今回の例で用いた言語(Java5 or later)ではAggregateを明示的に扱うことができないため、設計上の決めごとに加え、対応するRepositoryを作ることで間接的にAggregateを表現する。

今回の例での一貫性維持単位、つまりAggregateを構成する要素は、PurchaseOrderとPurchaseOrderLineItemとする。ルートは当然PurchaseOrderになる。

現状のDAOでは、Aggregateの子であるPurchaseOrderLineItemを直接取得・変更するようになっているので、AggregateルートであるPurchaseOrderを取得するように変更し、名称もPurchaseOrderRepositoryに変更する。

[AddPurchaseOrderLineItemService]
public class AddPurchaseOrderLineItemService {
    @Transactional
    public void addNewItem(Command cmd) {
        ...
        PurchaseOrder po = poRepository.findForUpdate(cmd.id);
        ...
        PurchaseOrderLineItem newItem = new PurchaseOrderLineItem(po.getId(), ...);
        ...
        if (po.getLimit() >= po.getTotal() + newItem.getPrice())) {
            po.add(newItem);
            poRepository.save();
        }
        ...
    }
}
また、最低限一貫性を持たせねばならない範囲はPurchaseOrderのAggregateであるため、トランザクションスコープはPurchaseOrderの状態変更メソッドであるaddItemに設定する。トランザクション設定値としては、このAggregateが必要とする値を設定する。
# 今回はデフォルト値のまま

[PurchaseOrder]
public class PurchaseOrder {
    List items = Lists.newArrayList();
 
    @Transactional
    public void addItem(PurchaseOrderLineItem item) {
        if (limitOver(item.getPrice())) {
            throw new ...
        }
        items.add(item);
    }

    private boolean limitOver(int price) {
        return (price + total) > limit;
    }
}
これで、呼び出し側がトランザクションを開始していなくても、PuchaseOrderが必要なトランザクションを設定できる。

不変式をモデル内に移動
if文内の条件式をAggregateルートの変更操作内に(不変式として)移動させる。これで、ドメイン層のモデル上で表現された。

[PurchaseOrder]
public class PurchaseOrder {
    List items = Lists.newArrayList();

    public void addItem(PurchaseOrderLineItem item) {
        if (limitOver(item.getPrice())) {
            throw new ...
        }
        items.add(item);
    }

    private boolean limitOver(int price) {
        return (price + total) > limit;
    }
}

移動後のサービスには、重要な知識は何も残っていない。

[AddPurchaseOrderLineItemService]
public class AddPurchaseOrderLineItemService {
    @Transactional
    public void addNewItem(Command cmd) {
        ...
        PurchaseOrder po = poRepository.findForUpdate(cmd.id);
        ...
        PurchaseOrderLineItem newItem = new PurchaseOrderLineItem(po.getId(), ...);
        ...
        poRepository.save();
        ...
        }
}

同時実行制御戦略の場所を移動
同時実行制御戦略を、取得操作側ではなくEntity側に設定する。また、具体的な制御戦略はドメイン知識ではないので、モデルから追い出すのが良い。

[PurchaseOrder]
@Entity
@org.hibernate.annotations.Entity(
    optimistcLock = OptimisticLockType.VERSION
)
public class PurchaseOrder {
    List items = Lists.newArrayList();
    
    public void addItem(PurchaseOrderLineItem item) {
        if (limitOver(item.getPrice())) {
            throw new ...
        }
    items.add(item);
    }

    private boolean limitOver(int price) {
        return (price + total) > limit;
    }
}

# TODO:とはいえ、ここの記述はイメージで、この設定でPurchaseOrderLineItemを追加した場合にPurchaseOrderのVERSIONが更新されるようにできるかは未確認…Hibernate詳しくない^^;

Repositoryのインタフェースからは、同時実行制御戦略を消す。

[AddPurchaseOrderLineItemService]
public class AddPurchaseOrderLineItemService {
    @Transactional
    public void addNewItem(Command cmd) {
    ...
    PurchaseOrder po = poRepository.find(cmd.id);
    ...
    }
}
維持範囲の変更/調整
DDD本書では、一貫性の維持範囲内にProductを含めるかどうかという議論が出ており、以下のような論点から組み入れるのは好ましくない、という結論になっている。
  • 個々の注文を編集するためにProductまで同時実行制御の対象となってしまい、編集エラーが多発する
    • Productは、PurchaseOrder/PurchaseOrderLineItemに比べてHigh-Contention
  • Productのpriceが変更になった場合、過去のPurchaseOrderの金額に影響を与えたくない
これらはいずれもドメイン知識(もしくはそこから得られる洞察)であるため、ドメインモデルに反映したい。Aggregateを明示的に示し、PurchaseOrderAggregateからProductを除外しておくことで、こうした設計判断をモデル上で示すことができる。
いずれにせよ、ProductをPurchaseOrderAggregateの子にすると、Productの変更をPurchaseOrder経由で行うことになり、これは明らかにおかしいので、ソフトウェア設計の視点からも当然の判断だと思う。


手続き+データのパラダイムでは
一貫性を保たねばならない範囲をモデル上に表現すべき、というのはオブジェクトモデルに限った話ではなく、データモデル上で明示するのも良いプラクティスだろう。DAOやストアドプロシージャなどデータモデルにアクセスする各処理は、こうしたモデルに関する情報に基づいて適宜一貫性を保つようにすることで、一貫性上の問題を起きづらくできる。

コメント