dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

IDDD本もくもく読書会メモ#4(第6章 値オブジェクト)

第5章エンティティのメモが書けないまま第6章を終えてしまった。
ので、記憶の新しいうちに値オブジェクトの方のメモを書く。

過去メモ
教材

学習メモ

値オブジェクトとは?
  • ドメイン内の何かを計測したり定量化したり、あるいは説明したりするためのオブジェクト
  • 例:姓・名や電話番号、金額や数量など
  • なぜ値オブジェクトを使うのか
    • int 型のようなプリミティブな型ではなく専用の値オブジェクトにすることで、ドメインの業務をプログラムでわかりやすく示すことができる
    • 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 にはこれに加え、プリミティブな型よりも制約を設けることでその値が備えるべき要件を明確にする、といった記述があった気がする
      • 例:数量に int 型を使うと Java の場合 -2^31 ~ 2^31 - 1 までの整数を許容してしまうが、数量にマイナスはない。また要件によっては最大値もあるかもしれないし、その場合バリデーションロジックを記述することで要件が明確になる
エンティティと値オブジェクトの違い、使い分け
  • 違いは 一意な識別子を必要とするかどうか
    • 値オブジェクトは「一意に識別して変更を管理する必要がないモノ」(CodeZine より)
  • 値オブジェクトにすべきところをエンティティにしてしまいがちだが、値オブジェクトにすることで使いやすくテストしやすくなるので、積極的に利用すべき

可能な限り、エンティティよりは値オブジェクトを使ってモデリングすべきだと聞いたら、驚くかもしれない。ドメインの概念をエンティティとしてモデリングしなければいけないとしても、そのエンティティの設計は、子エンティティのコンテナではなく値のコンテナとして組み立てるよう心がけるべきだ。このアドバイスは、単なる気まぐれによるものではない。値型は何かを計測したり定量化したり説明したりするときに使うもので、作成やテストがしやすいし、使うのも最適化するのも保守するのも楽だ。

  • 値オブジェクトを使用するかどうかの判断基準は以下

あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること。値オブジェクトに、自分が伝える属性の意味を表現させ、関係した機能を与えること。値オブジェクトを不変なものとして扱うこと。同一性を与えず、エンティティを維持するために必要となる複雑な設計を避けること。[Evans](97ページ)


値オブジェクトの特徴
  1. 計測/定量化/説明
    • 値オブジェクトは、ドメインの何かを計測・定量化・説明した結果である
  2. 不変性
  3. 概念的な統一体
    • 一つの属性値だけでは意味を持たず、それぞれが組み合わさることで適切な説明をできることを「概念的な統一体」と呼ぶ
    • 例:「50,000,000ドル」は「50,000,000」と「ドル」という2つの属性があるが、これらを切り離すと別の意味になる(か、意味をもたなくなる)
  4. 交換可能性
  5. 等価性(値が等しいかどうかを、他と比較できる)
  6. 副作用のない振る舞い


標準型(タイプコード)
  • 区分や種類を示す
    • 例:「電話番号」というユビキタス言語に対して、それが自宅の番号なのか職場の番号なのかそれ以外の番号なのか、といった種類についての説明
  • 現場で役立つシステム設計の原則 に書いてあった気がする
  • Enum を使うことを推奨してる
  • データベースのレコードを値オブジェクトに変換し、それをタイプコードとする手法もある


6.4 値オブジェクトのテスト

https://github.com/VaughnVernon/IDDD_Samples/blob/master/iddd_agilepm/src/test/java/com/saasovation/agilepm/domain/model/product/backlogitem/BusinessPriorityTest.java#L27-L45

public void testCostPercentageCalculation() throws Exception {
                                                                       
    BusinessPriority businessPriority =
        new BusinessPriority(new BusinessPriorityRatings(2, 4, 1, 1));
                                                                       
    BusinessPriority businessPriorityCopy =
        new BusinessPriority(businessPriority);
                                                                       
    assertEquals(businessPriority, businessPriorityCopy);
                                                                       
    BusinessPriorityTotals totals =
        new BusinessPriorityTotals(53, 49, 53 + 49, 37, 33);
                                                                       
    float cost = businessPriority.costPercentage(totals);
                                                                       
    assertEquals("2.7", this.oneDecimal().format(cost));
                                                                       
    assertEquals(businessPriorityCopy, businessPriority);
}
6.5 実装
  • (著者は通常)値オブジェクトの初期化は2つのコンストラクタを用意する:
    1. 状態の属性を設定するために必要なすべてのパラメータを受け取る、「プライマリ」コンストラクタ。属性の初期化にはprivateなセッターを用いる(自己委譲)
    2. 既存の値をコピーして新しいコンストラクタを作るコピーコンストラクタ。シャローコピーを行う
      • ディープコピーもできなくはないが処理が複雑になる&不変な値を扱うときにはインスタンス間で属性/プロパティを共有したところで問題ないはず


疑問とか

「属性」と「プロパティ」は区別して使ってる?(6.1 値の特徴 > 概念的な構造体)
  • CodeZine だと属性(プロパティ)と表記しているが、本書では区別しているっぽい
  • → 属性は値オブジェクトではない標準的な型による値、値オブジェクトになったものはプロパティと呼んでいるんだと理解した
「自己委譲」は一般的な用語?(6.5 実装)

ググっても出てこなかった。文字とおり自己+委譲なので処理を自分自身のクラスの別のメソッドに任せているから?

値オブジェクトのストラテジ/ポリシーにあたる部分?(6.5 実装)

以下がストラテジパターンにあたるというのがよくわからなかった。

public float costPercentage(BusinessPriorityTotals aTotals) {
    return (float) 100 * this.ratings().cost() / aTotals.totalCost();
}
                                                                                     
public float priority(BusinessPriorityTotals aTotals) {
    float costAndRisk = this.costPercentage(aTotals) + this.riskPercentage(aTotals);
                                                                                     
    return this.valuePercentage(aTotals) / costAndRisk;
}
                                                                                     
public float riskPercentage(BusinessPriorityTotals aTotals) {
    return (float) 100 * this.ratings().risk() / aTotals.totalRisk();
}
                                                                                     
public float totalValue() {
    return this.ratings().benefit() + this.ratings().penalty();
}
                                                                                     
public float valuePercentage(BusinessPriorityTotals aTotals) {
    return (float) 100 * this.totalValue() / aTotals.totalValue();
}
                                                                                     
public BusinessPriorityRatings ratings() {
    return this.ratings;
}

ストラテジパターンの説明 や同じ箇所で言及されてる PofEAA のセパレートインターフェース なんかを見る限り、共通のインターフェースを用意してストラテジごとの実装は各クラスに分けて切替可能にする、とかのようだ。

↑の例だと、今は costPercentage() の算出ロジックが1種類しかないから実装をべたっと書いてしまっているけど、別の算出方法が登場した際にはどっかにインターフェースだけ定義して実装とは分離することになるんだろうか。


おわりに

第6章も骨太で 2 回に分けてなんとか読み終えた。
6.2 節の、コンテキストをまたいだ場合にエンティティではなく値オブジェクトとして定義することで責務を少なめに抑える、というのがまだぴんと来ていない。

また 6.6 節については Hibernate というツール(ORM)の具体的な使い方が中心で最後まで読みきらなかったけど、コードが多かったせいか Hibernate がどういうものであるかは感じ取ることができてよかった。

詳しく知りたくなったらこのあたり読めばいいんだろうか。
Hibernateで理解するO/Rマッピング(1):O/Rマッピングの役割とメリット - @IT


次回

10/4(水) 19:00 予定です。connpass ページ作ったら貼る。


資料