2009年12月19日土曜日

モデル上に明示されたトランザクションとしての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やストアドプロシージャなどデータモデルにアクセスする各処理は、こうしたモデルに関する情報に基づいて適宜一貫性を保つようにすることで、一貫性上の問題を起きづらくできる。

2009年12月12日土曜日

KPT2009

というわけで、2008のKPTおよび2009年のTRY評価を踏まえた上での2009年の振り返り。

■ KEEP
* 英語
  • 日常の情報ソースを含めた多聴・多読
  • 定期的な実務翻訳
* 体系的な学習
  • コンピュータ科学の不足部分学習
  • 「モデル」とその扱いの学習
  • 数学とそこそこ仲良くする、関わり続ける
  • DDDの学習
* 交流
  • 内外の勉強会への参加
  • 社内勉強会の開催
* 現場の改善
  • 現場での設計プラクティスの改善
  • 現場の洗脳(自分がやりたいことをやるための土壌作り)
  • コードの適正化への働きかけ
* 趣味
  • 楽器を定期的にさわって楽しむ

■ PROBLEM
  • 数学の基礎知識が圧倒的に不足。数学系の勉強会に出続けるのはつらい。技術士の一次にも不足。
  • 精読とリスニングの力があまりあがっていない。基礎からやらねば。
  • 満足いくようなソフトウェアを作れていない。何か1つ作りたい。
  • 相変わらず趣味と料理が…orz。好きなことを学ぶのが趣味みたいなもんだが…。
  • 会いたい人に会いにいけてない。躊躇してもしょうがない。

■ TRY
* 英語
  • 来年こそTOEICで900点!
  • 精読の力を身につける。
  • リスニングの力をつけるために、シャドウイングをもっと実践する。
* 実務
  • 自信を持って内外に紹介できるシステムを作る。
  • DDDのアイディアを使って、現場の開発を改善する。コードの可読性も向上させる。
  • ESBをうまく使ってみる。
  • 機会があればDSLをうまく使ってみる。
* その他学習
  • より利用者側にフォーカスしたアーキテクチャの学習。The Timeless Way of Buildingのシステム版のようなもの。いかにシステムと人間が同居するのかを探求。
  • 数学。まずは技術士試験用に基本的なところから。
  • OOの原典。どうも、原典を読まないと話が空転する場合があるようだ。
  • コンピュータ科学。どのあたりに行くかは未定…
* 社交/発信
  • 社外での勉強会開催。一度くらいやってみたい。
  • 何か役に立つソフトウェアを1つ作って公開。
  • 会うべき人に会いにいく。
* 趣味
  • 今年こそバンド。無理か?
  • 年賀状を自作の絵で。無理か?

思いつく限りではこんなところ。あまり面白くない!てか地味なんだよ!

2009年TRYの評価

明日のJavaEE勉強会のポジペネタでもあるので、この機会に整理。まずは、KPT2008のTRY(つまり2009年のTRY)の評価から。今年のTRYはこんなんでした。

■TRY
* 英語により慣れる
  • 今年はTOEIC 900点を目指したい
  • オリジナルが英語の情報は全部英語で読む
* 交流をより増やす
  • OSSプロジェクトに参加する or 書籍出版に関わる
  • 何という無理くささ
* 体系的な勉強
  • ドメイン工学系(GenarativeProgramming => Multiparadigm Design)
  • ビジネスモデリング系(Ericsson-Penker => Analysis Patterns => Data Moedl Patterns)
  • DSL系(Fowler DSL Books WIP => Meta Programming System)
  • ビジネス系(会計~中小企業診断士を趣味的に)
* 趣味
  • ベース楽しく弾く
  • できればアマ志向でバンド組んでスタジオで騒ぐ
  • かっくいいのかいてpixivとかに投稿してみたい
  • もっかい将棋はじめてもいいかなぁ
* 料理
  • とりあえずペペロンチーノで乳化あたりから

英語に慣れる
TOEICは900点には届かず。相変わらず、リスニングでの取りこぼしと、文法まわりのミスが目立つ。反面、リーディング部分はほぼ満点で、高速でざっくり流し読みする、というのは非常に得意になった。ここ数年、インプットとしての英語情報の量に圧倒されていたため、多少の精度は犠牲にしてとにかく高速で流し読みするスキルを鍛えてきた。このスキルをどうやら取得できたのは歓迎すべきことだ。長文の英語を読む前の、あの「ヤだなー」という感じや、読んでいるとすぐに疲れてくる感じはほとんどなくなり、日本語を読むのと近い感覚で読み進められるようになってきた。

しかしながら、文法を含めた精読のスキルはあまり向上しておらず、特に実務翻訳を行う場合に苦労した。わかりづらい英文に対して、論理的に英語の意味を確定していく、という作業の精度が、流し読みのレベルに比べると低いのだ。
今後はリスニングとあわせて、精読のスキルを重点的に鍛えていきたい。

交流をより増やす
交流…増えたのかなぁ。層・圏・トポスや檜山さんのセミナーである程度増えたんだけど、最近予定が合わずにあまり行けてないからなぁ…。
交流する人数が劇的に増えているわけではないと思う。そもそもそういうタイプの人間でもないし。今交流がある人とのつきあいを大事にしつつ、機会をみつけてちょこちょこ手を伸ばしていく感じにしたい。
書籍の出版に関しては、層・圏・トポスのご縁からお手伝いさせてもらえることに。色々ご迷惑をおかけします。お世話になってます。

体系的な勉強
今年やった勉強は、こんなところ。
  • 圏(基礎およびモナドまわり)
  • ラムダ計算(初等テキスト)
  • コンパイラ基礎(ふつパイラで)
  • DDD(書籍勉強会開催+現場でいくつか実践)
  • DSL(JavaEE勉強会でFowlerのDSL本輪講中)
  • 微積分(技術士向けに勉強はじめた程度)
ドメイン工学系やビジネスモデリング系はあまり進んでいない。去年力を入れていた、「システム全体」や「人間系+コンピュータ系の複合システム全体」をうまく捉えるための、より広範・抽象的で多分に曖昧さを含む方向にはあまり手を伸ばさず、詳細・厳密な方向へ行っている印象。コンピュータ科学の専門教育受けてないので、専門知識や土台の不足を実感したのが主な要因か。来年はもう1度そっち方面に向かうかもしれない。
ビジネス系にまったく手を出せていないのは、自分には基礎的な技術の勉強とビジネス系の勉強を同時に進めるほどの器用さがないと悟ったから。仕事での必要性を考えると、基礎技術を固めるのが重要だと考えて、ビジネス系は優先順位を下げている。来年もそうなりそう。

趣味
ベースはちょこちょこさわっているけど、継続してやっている感じではないなぁ。面白い曲を見つけたら耳コピして演奏、楽しんで満足している程度。バンドを組むにはほど遠い。環境だけは整えたんだけど。

絵については、好きな漫画をパクってイラスト描いてみたり、会議中に色々描いて(^^;)描くことに慣れるようにしているんだけど、あまりうまくなった感なし。描き散らしているだけで、きちんとした作品にするまで頑張れていないんだよなぁ。基本的なスキルも身に付いてない印象。絶対的に量が足りてない。とりあえず形から入る意味で、ペンタブでも買ってみようかと画策中。来年の年賀状(メール)は、手書きの絵を送りたいものだ。

料理
orz…iPhoneにクックパッド入れたよ!

全体的に見ると、趣味系がいまいち、あとはそこそこ頑張っているが満点では決してない、という感じか。トータル65点くらいかな。KPT2009に続きます。