Closed14

『ドメイン駆動設計入門 ( https://amzn.to/3YZ0fAL ) 』ノート

aki kureaki kure

2章. 値オブジェクト

  • 値オブジェクトとは「システム固有の値を表したオブジェクト」
  • 例えば、氏名を表す場合を考えてみる。
  • プリミティブ型で表す場合は、拡張性やメンテナンス性に欠ける
# プリミティブ型で表現する場合
# 氏や名前を取り出そうとすると文字列処理が必要。また、氏名の前後が逆に配置されているケース等では対応しずらい。
fullname = "Kure Akira"
first_name = fullname.split(" ")[0] # 拡張性に欠ける

値オブジェクトにした場合、上記のようなケースでも以下のようにクラスとすることで、表現性が増す。


class Fullname
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

full_name = Fullname.new("Akira", "Kure")
full_name.first_name # 簡単かつ安全

  • 値の性質は以下3つに整理できる
    • ①不変
    • ②交換可能
    • ③等価性により比較可能

①不変については例えば、以下のようにオブジェクト自体は変更せずに、値の変更を許容してはいけないということ。

full_name = Fullname.new("Akira", "Kure")
full_name.first_name = "Manabu" # No Good

もし、値を異なるものにしたい場合は、②交換によって、実現するべきである。

full_name = Fullname.new("Akira", "Kure")
full_name = Fullname.new("Manabu", "Kure")

ちなみに、参考リンクにあるように簡易に不変なオブジェクトを使うためのDataクラスというのがRuby3.2で追加された。

③については例えば、通常の値であれば、以下のように true が得られる。

1 == 1 # => true

真偽は、オブジェクトとしては別物だが中身によって判定されている。

値は値自身ではなく、それを構成する属性によって比較されるということです。システム固有の値である値オブジェクトも値と同様に、値オブジェクトを構成する属性(インスタンス変数)によって比較されます(p.24)

値と同じように値オブジェクト同士が比較できるようにする方が自然な記述になります。(略)このような自然な記述を行うためには値オブジェクトが比較するためのメソッドを提供する必要があります。

例えば以下のような感じか。

class Fullname
  # 上述部は省略
  def ==(full_name)
    self.first_name == full_name.first_name &&
    self.last_name == full_name.last_name
  end
end

full_name1 = Fullname.new("Akira", "Kure")
full_name2 = Fullname.new("Akira", "Kure")
full_name1 == full_name2 # => true

どこまでを値オブジェクトとするかは要件次第。例えば、この例だと、氏や名前もクラスにしてもいいかもしれない。著者の考えは以下。

「そこにルールが存在しているか」という点と「それ単体で取り扱いたいか」という点を重視しています(p.30)

この例では、氏や名前単体で扱うシーンは無さそうなので、値オブジェクトにするまでは無いのでは、ということ。


参考リンク

https://techracho.bpsinc.jp/hachi8833/2023_02_27/126975

https://blog.saeloun.com/2022/11/22/data-immutable-object/

https://blog.osa.in.net/value-object-in-ruby/

aki kureaki kure

3章. エンティティ

エンティティの性質

①可変
②同じ属性であっても区別される
③同一性により区別される

# ①可変である
user = User.first
user.name # => Kure
user.name = "Kure2"

# ②同じ属性であっても区別される
user1 = User.find(1)
user2 = User.find(2)
user1.name # => "Kure"
user2.name # => "Kure"

user1 == user2 # => false
# ③同一性により区別される
# > 値オブジェクトの比較処理ではすべての属性が比較の対象となっていましたが、エンティティの比較処理では同一性を表す識別(id)だけが比較の対象となります。(p.57)

user1a = User.find(1)
user1b = User.find(1)
user1a.id # => 1
user1b.id # => 1

user1a == user1b # => true

あるモデルを、値オブジェクトとするか、エンティティとするか

何を値オブジェクトにして、何をエンティティにするかとう判断の基準が欲しいところです。ライフサイクルが存在し、連続性が存在するかというのは大きな判断基準になります(略)
ユーザーはライフサイクルをもち、連続性のある概念です。ユーザはエンティティで間違いなさそうです。(p. 58)

同じものごとを題材にしても、それを取り巻く環境によってモデルに対する捉え方は変わります。値オブジェクトにも、エンティティにもなりえる概念があることを認識し、ソフトウェアにとって最適な表現方法がいずれになるのかは意識しておくとよいでしょう。(p.59)

aki kureaki kure

4章、ドメインサービス・6章、アプリケーションサービス

ドメインサービス

例ではユーザーの存在重複チェック。ユーザーモデルに配置するのは違和感があるので、ユーザーサービスというドメインサービスに配置。
ただし、

すべてのふるまいはドメインサービスに移設できます。やろうと思えばいくらでもドメインモデル貧血症を引き起こせてしまいます。... 可能な限りドメインサービスは利用しないでください。(p.73)

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

例としては、ユーザー登録や退会など、アプリケーションとしてのふるまいの実現を対象にする。

退会したりといったことはアプリケーションを成り立たせるための操作です。それらのふるまいはドメインに存在する概念ではなく、ユーザー機能を新たに実現するために作られたものです。したがってユーザーの登録や退会といったふるまいはアプリケーション固有のふるまいであり、それが定義されるサービスはアプリケーションのサービス、つまりアプリケーションサービスです。ドメインサービスとアプリケーションサービスは対象となる領域が異なるだけで本質的には同じものです。まずサービスがあり、それの向いている方向がドメインであるか、アプリケーションであるかで分けられているのです。 p. 156

aki kureaki kure

リポジトリ

エンティティやドメインサービスの中等で、DBへのアクセス処理等を書くと、責務が曖昧になるので、それ用のインターフェースやクラスに分けようという話。
ただ、Railsの場合は、モデルが直接DBとやり取りするメソッドを持っている(しかもたくさん)ので、必要性がイマイチ無いかも?
ChatGPT先生に聞くと、モデルの肥大化の対応や、モックを仕込みやすくする時等に用いるといいらしいが、煩雑になりそうな気もしないでもない。

有名なリポジトリのコードざっと見てもそれっぽいことしているコードがぱっとは見つからない?
https://zenn.dev/takahashim/articles/ac725fb16ec7a11809c5

gitlab さんのところで、 finder というのはそれっぽいが、検索用のユーティリティという感じだし、リポジトリという感じとは違う感じかな。
https://github.com/gitlabhq/gitlabhq/blob/master/app/finders/boards/visits_finder.rb

aki kureaki kure

7章. 依存関係逆転原則

これはSOLID原則とかの時によく目にするやつ。
書籍ではインターフェースのある言語なので、インターフェースでやり取りしているが、Rubyの場合はダックタイピングで実現。以下みたいなイメージでいいかと。

# @param [#run]
def perform(obj)
  obj.run
end

関連
https://zenn.dev/shuhei_takada/articles/7ea60cc7253c43

aki kureaki kure

14章、レイヤードアーキテクチャでユーザー登録されるまでの流れを4層に沿って追っていく。

プレゼンテーション層(ユーザーインターフェース層)

登録のリクエストを受け取ったら、ユーザー登録のコマンドオブジェクトを生成し、アプリケーションサービスの登録処理を呼び出す。

[HttpPost]
public UserPostResponseModel Post([FromBody] UserPostRequestModel request)
{
var command = new UserRegisterCommand(request.UserName);
var result = userApplicationService.Register(command);

return new UserPostResponseModel(result.CreatedUserId);
}

https://github.com/nrslib/itddd/blob/4b045b7d808ae15df4085769789d5045ff8fc361/Layered/WebApplication/Controllers/UserController.cs#L46-L53

アプリケーション層

アプリケーション層では、アプリケーションサービスで、ユースケースの処理を行う。具体的には以下を呼び出す

  • ドメイン層
    • ユーザーファクトリでユーザーモデルのオブジェクトを生成
    • ユーザーサービスで重複チェック
  • インフラストラクチャ層
    • リポジトリでユーザーを保存
コマンドオブジェクト
    public class UserRegisterCommand
    {
        public UserRegisterCommand(string name)
        {
            Name = name;
        }

        public string Name { get; }
    }

https://github.com/nrslib/itddd/blob/a918365c24f199660b861e72b6ef1a3450a19129/SampleCodes/Chapter6/_32/UserRegisterCommand.cs#L3-L11

アプリケーションサービス
        public UserRegisterResult Register(UserRegisterCommand command)
        {
            using (var transaction = new TransactionScope())
            {
                var name = new UserName(command.Name);
                var user = userFactory.Create(name);
                if (userService.Exists(user))
                {
                    throw new CanNotRegisterUserException(user, "ユーザは既に存在しています。");
                }

                userRepository.Save(user);

                transaction.Complete();

                return new UserRegisterResult(user.Id.Value);
            }
        }

https://github.com/nrslib/itddd/blob/4b045b7d808ae15df4085769789d5045ff8fc361/Layered/SnsApplication/Users/UserApplicationService.cs#L48-L65

ドメイン層

ユーザーファクトリ
    public class InMemoryUserFactory : IUserFactory
    {
        public SerialNumberAssigner NumberAssigner { get; } = new SerialNumberAssigner();

        public User Create(UserName name)
        {
            var rawId = NumberAssigner.Next();

            return new User(
                new UserId(rawId.ToString()),
                name,
                UserType.Normal
            );
        }

https://github.com/nrslib/itddd/blob/4b045b7d808ae15df4085769789d5045ff8fc361/Layered/InMemoryInfrastructure/Persistence/Users/InMemoryUserFactory.cs#L6-L19

ユーザーのドメインサービス
        public bool Exists(User user)
        {
            var duplicatedUser = userRepository.Find(user.Name);

            return duplicatedUser != null;
        }

https://github.com/nrslib/itddd/blob/4b045b7d808ae15df4085769789d5045ff8fc361/Layer

ユーザーモデル

https://github.com/nrslib/itddd/blob/4b045b7d808ae15df4085769789d5045ff8fc361/Layered/SnsDomain/Models/Users/User.cs

インフラストラクチャ層

ユーザーのリポジトリ

https://github.com/nrslib/itddd/blob/4b045b7d808ae15df4085769789d5045ff8fc361/Layered/EFInfrastructure/Persistence/Users/EFUserRepository.cs

aki kureaki kure

https://zenn.dev/link/comments/0660a3ff007458
と、Railsを当て込むと、

プレゼンテーション層→View+コントローラ
アプリケーション層→コントローラ+サービスクラス+モデル
ドメイン層→モデル
インフラストラクチャ層→モデル

となるかな。
アプリケーション層の処理をどこに書くかが曖昧そうだが。

このスクラップは4ヶ月前にクローズされました