asoview! TECH BLOG

アソビュー株式会社のテックブログ

ValueObjectのすゝめ

ValueObjectのすゝめ

Photo by bruce mars on Unsplash

アソビュー Advent Calendar 2020の16日目です。

こんにちわ、今年1月に入社したサーバーサイドエンジニアの頭島です。

テーマ

今回は、ドメイン駆動設計(以下DDD)の技術的アプローチのひとつでもあるValueObjectについて紹介させていただきたいと思います。私自身アソビューで1年間DDDをやってきて今では空気を吸うようにValueObjectを作っていますが、改めて導入のしやすさと恩恵を感じます。既存のソフトウェアにも導入しやすいので、さくっと導入して恩恵を感じてみましょう!といったテーマになります。

アソビューにおけるDDD

アソビューでは、DDDを導入しています💡過去には技術顧問としてドメイン駆動設計で有名な増田 亨(@masuda220)氏を迎えたこともあり、日頃からドメインモデリングを意識して開発しています🔥

ValueObjectとは?

intString で表現していた値をクラスにしたものです。下記例ではUserIdUserNameValueObjectです。


/* ValueObject */
class UserId {
int value;
}
/* ValueObject */
class UserName {
String value;
}
/* ValueObjectで構成されたユーザクラス */
class User {
UserId userId;
UserName userName;
}
/* よくあるユーザクラス */
class User {
int userId;
String userName;
}

ValueObjectの何がいいの?

開発中に感じられるメリットについて3つご紹介したいと思います。

  • 修正漏れの防止

下記のようなPurchaseクラスとSettlementクラスがあるとします。Purchase.idとSettlement.purchaseIdは同意で購入IDを表現します。この状態で、購入IDを持つクラス全てに修正が必要となる場合・・・

①の場合、機会的に修正箇所を抽出するのに一苦労しそうです。変数名によるgrepや、修正担当のエンジニアの知識に頼ることになりそうです。その結果漏れが発生しやすいですね。

②のように、ValueObjectにしてあげると機械的にPurchaseIdクラスの参照箇所を洗い出せばよいので、修正漏れを防止できますね😃


/* ① ValueObject導入前 */
class Purchase {
int id;
}
class Settlement {
int id;
int purchaseId;
}
/* ② ValueObject導入後 */
class Purchase {
PurchaseId id;
}
class Settlement {
SettlementId id;
PurchaseId purchaseId;
}

  • ロジックの凝集

上記サンプルコードで、購入IDのバリデーションロジックを複数箇所に実装するとします。下記の手段があり、個人的には★の数が多いほどベターだと思っています。

①複数の実装箇所にベタ書き(★)

-> ロジックの実装箇所が分散にしていて、可読性・保守性が悪い。

②共通処理クラスに実装(★★)

-> ロジックの実装箇所が凝集されているため保守性こそ悪くないですが、「他の場所でも実装する可能性あるから、とりあえず共通処理クラスに実装しとけばいいっしょ!」となりがちです。共通処理クラスが膨大になり可読性が下がります。ロジックの実装箇所も曖昧ですね。

③購入IDクラスに実装(★★★)

-> ロジックの実装箇所が凝集されているため保守性もよく、そもそも購入IDに関するロジックなので購入IDクラスで定義したほうがわかりやすい!ロジックの実装箇所も明確になってきますね😃

そして③を実現するためには、ValueObjectが必要になります。バリデーションロジックをPurchaseIdクラスに定義して、あとは呼び出すだけでOKです💡


/* ValueObject */
class PurchaseId {
int value;
/* 複数箇所で実装したかったバリデーションロジック */
public boolean isValid() {
return ~~~
}
}
class Purchase {
PurchaseId id;
/* 購入IDのバリデーション */
public boolean isPurchaseIdValid() {
return id.isValid()
}
}
class Settlement {
SettlementId id;
PurchaseId purchaseId;
/* 購入IDのバリデーション */
public boolean isPurchaseIdValid() {
return purchaseId.isValid()
}
}

  • ちょっとした可読性の向上

下記Settlementクラスがどのような値を持っているか認識する場合、

①は変数名でしか認識できない

②は変数名でもクラスでも認識できる

一見大差ないように思えます。変数名定義の重要性は前提のもと、①のように変数名に対する異常な関心は意外とストレスです。そしてSettlementIdクラスがどのようなドメインであるか、どのようなロジックを保持しているかというと、ロジック凝集がきちんと実施されていればSettlementIdクラスを参照することですべて把握できます。ValueObjectを導入すればストレス解消です😌

-----------------------------------
/* ① ValueObject導入前 */
class Settlement {
int id;
int purchaseId;
}
/* ② ValueObject導入後 */
class Settlement {
SettlementId id;
PurchaseId purchaseId;
}
-----------------------------------

まとめ

ValueObjectを導入するために必要なことは、最低限下記2つです。

  • ValueObjectのクラスを作成する
  • ValueObjectに関するロジックを、ValueObjectクラスに凝集する

これだけで、恩恵を受けることができると思います!

また、DDDで本格的にValueObjectを扱う場合いくつかルールがあり、それらを適用することで、より安全性の担保されたソフトウェア開発を行うことができます💡

  • 不変であること(一度生成されたら破棄されるまで値を変更しない。setterなどで不本意に変更しない。)。
  • あらゆる場面において、同じ値のValueObject同士は同じものと考える。

etc…

※DDDに関する詳細は、書籍またはDDDの詳しい記事がたくさんあるのでご参照ください!

さいごに

アソビュー!では一緒に働くメンバーを募集しています。
少しでもご興味がありましたらお気軽にご応募ください!