TaNA LABO
エンジニアリング

April 17, 2020

DDD
アーキテクチャ

ドメイン駆動設計入門は新人時代にDDD挫折した私にも分かりやすかった

post 35

先輩エンジニアからエリック・エヴァンスのDDD本を薦められた新人時代。当時は今よりも技術力が未熟な上、内容も難解だったので、数ページ見開いただけで挫折してしまった。それから長い間DDDに触れる機会は無く現在に至る(読んだ人に話を聞いても、よく分からなかったと言う人ばかりだった)


あれから数年コードを書き続けたけど、自身の中で指針となるコードの書き方も定まっておらず、現場毎に異なる開発ルールに翻弄されている気がしたので、再度DDDへ入門することに決めた。しかし数年コードを書き続けたとしても、エリック・エヴァンス本は相変わらず敷居が高い。

なのでこちらの本読んでみたが、一読した感想はとにかく理解しやすく「ちゃんとオブジェクト指向で開発しよう♪」との印象を受けた。記事タイトルどおり、DDD挫折した私にも分かりやすい良書。

本書のあらすじ

エリック・エヴァンス本では、ユビキタス言語の解説に始まり、ドメインエキスパートや境界付けられたコンテキスト、エンティティ、値オブジェクトなど、とにかく専門用語が多くて初心者を悩ませる。

本書ではそんな挫折をしないで済むように、ドメイン駆動設計で重要な 概念を抽出するモデリング には触れず 概念を実装に落とし込むパターン を中心に解説されている。

■ 知識を表現

・値オブジェクト ★

・エンティティ ★

・ドメインサービス ★


■ アプリケーションを表現

・リポジトリ ★

・アプリケーションサービス ★

・ファクトリ ★


■ 知識を表現(発展形)

・集約

・仕様

ドメイン駆動設計のパターンだけを取り入れる手法は 軽量DDD と呼ばれるが 重要なことはドメインの本質と向き合うことで、パターンはあくまでサポート役であること を念押しされている。

ドメイン駆動設計のコンセプト

ドメイン駆動設計の コンセプト とは!?

ビジネスの問題を解決するため、ビジネスの理解進め、ビジネスを表現し、ビジネスとコードを結び付け、継続的かつ反復的な改良を施せるように枠組みを作ることで、ソフトウェアをより役立つものにしようとするもの(ソフトウェアにおいては、至極当たり前の話なんだけど・・・)

またソフトウェア開発の ドメイン とは、プログラムを適用する対象となる領域 で、ドメインに含まれるものを考えるのが重要(会計システムなら金銭や帳票、物流システムなら貨物や倉庫、輸送手段)

知識を表現 ① ー 値オブジェクト

ドメイン知識をコードへ落とし込む基本パターンとして挙げられる 値オブジェクト は、ドメインオブジェクトの一つで、システム固有の値を定義する役割を担う(FirstNameとLastNameが該当)

class FullName : IEquatable<FullName>
{
  private readonly FirstName firstName;
  private readonly LastName lastName;
  public FullName(FirstName firstName, LastName lastName)
  {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  # 省略
}

# 名を表す値オブジェクト
class FirstName
{
  private readonly string value;
  public FirstName(string value)
  {
    if (string.IsNullOrEmpty(value)) throw new ArgumentException("1文字以上である必要があります。", nameof(value));
    this.value = value; 
  }
}

# 姓を表す値オブジェクト
class LastName
{
  # 省略
}

わざわざ値オブジェクトを定義せずに、プリミティブ型だけでも実装は可能だが、それでは汎用的で表現力に乏しいプログラムになりがち。値オブジェクトで定義をすれば、クラスにルールが付与されるので、ドキュメントとして機能させることが可能になる(パッと見で何やってるか理解しやすい)

また値オブジェクトは ただのデータ構造体ではなく、ふるまいも定義可能で、自身のルールを語るドメインオブジェクト になり、ロジックの散在が改善されて可読性の向上が見込める。

また本書では、値オブジェクトとして定義するかの判断基準として、そこにルールが存在しているかそれ単体で取り扱いたいか という点を挙げられていた(判断難しい)

知識を表現 ② ー エンティティ

もう一つのドメインオブジェクトに エンティティ が紹介されており、値オブジェクトとの違いが紛らわしく、本書では同一性で識別されるか否か、またはライフサイクルが存在するか否かを判断基準にしている。ちなみに以下ブログでは、具体例を挙げて説明されている。

エンティティ 値オブジェクト
同一性判定 識別子が同一であれば同一 保持する属性が全て同一であれば同一
可変性 可変(ライフサイクルを持つ) 不変(生成されたら破棄されるだけ)
例) エンティティを社員で考えてみる

# 山田さんという社員は、ある会社において社員番号という識別子(123)で同一判定される
# 部署や所持金や体重が変わろうと、別人にはならない。
# 部署や所持金などの属性は変わるので、本質的には ”可変” である。

例)値オブジェクトを10円玉で考えてみる

# 造幣局でもない限り、2つの10円玉が並んでいて、区別することない(同じ機能を持つ硬貨)
# 2つの10円玉を区別する必要がない(=不変)場合、10円玉は値オブジェクトとして定義

基本的にはエンティティが自身の属性として、値オブジェクトを保持する関係となる。

DDD基礎解説:Entity、ValueObjectってなんなんだ

知識を表現 ③ ー ドメインサービス

最後に値オブジェクトやエンティティに記述すると不自然に見えるので、これらのふるまいを定義する ドメインサービス がある。具体的に 不自然なふるまい とはこんなコードのこと(これはドメインのものを表現する時よりも、ドメインの活動を表現する際によく見られる)

# エンティティ
class User
{
  # UserId と UserName は 値オブジェクト
  public User(UserId id, UserName name)
  {
    # 省略
  }
  # 不自然なふるまい
  public bool Exists(User user)
  {
    # 重複を確認するコード
  }
}

var userId = new UserId("id");
var userName = new UserName("nrs");
var user = new User(userId, userName);
var duplicateCheckResult = user.Exists(user);

上記では重複有無を、自身に対して問い合わせすることになり、多くの開発者を混乱させる不自然な記述に見える。このような不自然なふるまいの解決には、ドメインサービスの利用を推奨している。

# ドメインサービス
class UserService
{
  public bool Exists(User user)
  {
    # 重複を確認する処理
  }
}

ドメインサービスでは、自身のふるまいを変更するようなインスタンス特有の状態は持たせない。役割は理解しやすいが濫用は危険で、エンティティや値オブジェクトで定義出来ないか検討するのが吉。

■ 物流システムにおけるドメインサービスの実装例

post 35 1

物流拠点をエンティティとして定義。

class PhysicalDistributionBase 
{
  # (...略...)

  public Baggage Ship(Baggage baggage)
  {
    # (...略...)
  }

  public void Receive(Baggage baggage)
  {
    # (...略...)
  }
}

物流拠点の出庫(Ship)と入庫(Receive)はセットで実施されるので、輸送をサービスに定義。

class TransportService
{
  public void Transport(PhysicalDistributionBase from, PhysicalDistributionBase to, Baggage baggage)
  {
    var shippedBaggage = from.Ship(baggage);
    to.Receive(shippedBaggage);

    # 配送の記録処理など
  }
}

アプリケーションを実現 ① ー リポジトリ

アプリケーションを表現するパターンの一つが リポジトリ で、データを永続化(インスタンスを保存して復元出来るようにすること)し、再構築する処理を抽象的に扱うオブジェクトになる。

永続化 とはインスタンスを保存し、復元できるようにすること!!

# リポジトリを利用したユーザ作成処理
class Program
{
  private IUserRepository userRepository;

  public Program(IUserRepository userRepository)
  {
    this.userRepository = userRepository;
  }

  public void CreateUser(string userName)
  {
    var user = new User(new UserName(userName));
    var userService = new UserService(userRepository);
    if (userService.Exists(user)) {
      throw new Exception("既に存在します");
    }
    userRepository.Save(user);
  }
}

リポジトリの役割は、あくまでもオブジェクトの永続化なので、ユーザ重複確認のようなドメインルールに近い内容の実装は好ましくなく、そういった処理はドメインサービスが主体で実装するのを推奨(本書でも、Userの識別子であるUserIdでの検索メソッドはリポジトリ定義が望まいとしており、リポジトリへの定義は単純なCRUD操作に限られる印象を受ける)

public interface IUserRepository
{
  User Find(UserId id);

  # ドメインサービスに定義すべき内容
  bool Exists(User user);

  # 特定項目のCRUDはNG(多くのCRUD定義が乱立する結果となる)
  void updateName(UserId id, UserName name);
  # オブジェクトが保持する項目の変更はオブジェクト自身に依頼
  void Save(User user);
}

アプリケーションを実現 ② ー アプリケーションサービス

不自然さを解決するサービスに アプリケーションサービス があり、ドメインオブジェクトのふるまいを呼び出す役目を担っている。ドメインオブジェクトの公開には大きな危険性が潜んでいるので、本書では公開しないことを推奨している(代用としてDTOの活用だったり、コマンドオブジェクトがある)

public class UserApplicationService
{
  private readonly IUserRepository userRepository;
  private readonly UserService userService;

  public UserApplicationService(IUserRepository userRepository, UserService userService)
  {
    this.userRepository = userRepository;
    this.userService = userService;
  }

  public void Register(string name)
  {
    var user = new User(new UserName(name));

    # NGパターン
    var duplicatedUser = userRepository.Find(mailAddress);
    if (duplicatedUser != null) {
      throw new Exception(mailAddress);
    }
    # OKパターン(ドメインサービスを利用してユーザの重複チェックを実施)
    if (userService.Exists(user)) {
      throw new Exception("既に存在しています");
    }
    userRepository.Save(user)
  }
}

アプリケーションサービスは、あくまでもドメインオブジェクトのタスク調整に徹し、ドメインルールを記述させないことが重要(ルールはドメイン側に寄せることが大切)

値オブジェクトやエンティティは、自身のふるまいを持っているが、サービスは自身のためのふるまいを持たず、活動や行動を表すことが多い。またドメインにおける活動をドメインサービスとし、アプリケーションとして成立させるためのサービスが、アプリケーションサービスとなる。

アプリケーションを実現 ③ ー ファクトリ

アプリケーションを実現させる最後のパターンに、複雑なオブジェクトの生成処理をオブジェクトとして定義する ファクトリ があり、これは生成のみを責務とするオブジェクトになる。

public class UserFactory : IUserFactory
{
  public User Create(UserName name)
  {
    # データベースのコネクション処理
    # 省略

    # インスタンスの返却
    return new User(id, name);
  }
}

ファクトリ利用の目安について、生成処理が複雑でない場合は素直にコンストラクタでの呼び出しが望ましく、データベース接続など複雑な処理はファクトリ実装でカプセル化して柔軟性を確保。

Chapter11では解説された全知識を活用して、SNSを実装するサンプルが説明されている。

参考文献

正誤表@ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本
ドメイン駆動設計のメリットと始め方@CodeZine
little hands’ lab - ドメイン駆動設計を布教したい
[DDD]ドメイン駆動設計で実装を始めるのに一番とっつきやすいアーキテクチャは何か
今すぐ「レイヤードアーキテクチャ+DDD」を理解しよう(golang)
Goのpackage構成と開発のベタープラクティス
ドメイン駆動設計の比類なきパワーでRailsレガシーコードなど大爆殺したるわあああ
RailsでDDD


©Copyright2020 TaNA LABO. All Rights Reserved.