インドカレーファンクラブ

パソコン、カメラ

ボトムアップDDDを読んだ結果として自分が見返す用のメモ

要約ではないです。メモです
コードでは面倒くさいので色々(例えばコンストラクタとか)端折ってる

色々間違ってるかもしれないのでちゃんと本を読んだほうがいい

https://www.amazon.co.jp/gp/product/479815072X/

本にする前のをWebで見れたりもする

nrslib.com

ドメインオブジェクト(DomainObject)

値オブジェクト(ValueObject)

ValueObjectの定義とは?と問われた場合、例を示してこういうものといった方が楽な気がする

特徴としては...

  • 不変である
  • 交換が可能である
  • 等価性によって比較される
// 不変である
// 交換が可能である
var name = new Name("hoge");
name.Change("fuga");    // NG
name = new Name("fuga") // OK

「等価性によって比較される」についてはIEquatableを実装していることとイコール

オブジェクトをValueObjectとするかどうかという基準:
→ そこにルールが存在しているか、単体で扱いたいか

不変についての補足

例えばclass Moneyがあったとしたらその加算処理はAddによって成され、新しいインスタンスを返す(不変だから)

class Money
{
    // Moneyの新しいインスタンスを返す
    public Money Add(Money arg) {}
}

var milk = new Money(170, "JPY");
var ticken = new Money(900, "JPY");
var result = milk.Add(ticken);

思ったこと:今回の場合、+演算子オーバーロードとかしたくなる

エンティティ(Entity)

ValueObjectと対を成すもの
ライフサイクルがあるのがEntity

特徴としては...

  • 可変である
  • 同じ属性であっても区別される
  • 同一性により区別される
    • 同一性の判断の実態は大概識別子(ID)

可変であるということで、EntityではValueObjectのところに書いた name.Change("fuga"); はOK

・同じ属性であっても区別される
・同一性により区別される

ってのがわかりにくかも

同じ属性というのは例えばユーザの名称
同一性というのは上述の通りID

例えばID:1のユーザの名称が"hoge"だったとして、その名称を変更する処理のなかで以下のように変化したとしても同一ユーザですね。という話

  ID: 1, Name: "hoge"
→ ID: 1, Name: "fuga"

逆に言うと名称が一致していてもIDが違ったら別のもの(同姓同名)

値オブジェクト(ValueObject)とエンティティ(Entity)で迷ったら

可変なオブジェクトより不変なオブジェクトの方が安心ということで、一旦ValueObjectにしておけば安心とのこと

TODO: 本書を読んでいてValueObjectの例があまり思い浮かばなかった
具体例をもって分類して考えた方が良さそう

ドメインサービス(DomainService)

DomainObjectのふるまいでDomainObjectに記述すると不自然なものを書くとこ

例えばUserクラスの重複チェック処理(Exists())なんかは、そのインスタンスメソッドして作ると変なことになる
後述するRepositoryに繋ぎに行ったりすると更に微妙

// 変
User checker = new User(new UserName("checker"));
User target = new User(new UserName("hoge"));
checker.Exists(target);

思ったこと:上記のようにRepositoryに依存する処理はさておき、そうでなければウッカリstaticメソッドとしてDomainObjectに定義しそう

レポジトリ(Repository)

DomainObjectとデータストアの仲介をするとこ

オブジェクトを繰り返し利用するには 、何らかのデータストアにオブジェクトのデータを氷続化(保存)し 、再構築 (復元)する必要があります。リポジトリはデータを永続化し再構築するといった処理を抽象的に扱うためのオブジェクトです。

Repositoryの中でクエリ書いたりするとのこと
ORM(本書ではEF)にてテーブル及びカラムと紐付けするオブジェクトをDataModelと呼んでいるぽい
(場所的には Infrastructure.DataModel.Users.User とか)
DomainObjectが特定のデータ基盤に依存するのを防ぐため、別途DataModelを設けてるイメージ → このクラスはテーブル名ナントカと対応していて... カラムはフィールド名ナントカと対応していて... みたいなことをDomainObjectに記述するのを避けたいってこと

Repositoryの中でDBと直接やりとりするところだけDataModel、メソッドとしてのI/Oは普通にDomainObjectを使う

こんな感じ

public class UserRepository : lUserRepository
{
    private readonly MyDbContext context;

    // ...

    public User Find(UserName name)
    {
        UserDataModel target = 
            context.Users
            .FirstOrDefault(userData => userData.Name == name.Value);
        if (target == null) { return null; }
        // DataModel to DomainObject
        User result = ToModel(target);
        return result;
    }

    public void Save(User user)
    {
        UserDataModel found = context.Users.Find(user.ld.Value);
        if (found == null)
        {
            // DomainObject to DataModel
            UserDataModel data = ToDataModel(user);
            context.Users.Add(data);
        }
        else
        {
            // Merge DomainObject and DataModel
            UserDataModel data = Transfer(user, found);
            context.Users.Update(data);
        }
        context.SaveChanges();
    }

    // ...

DomainServiceとの債務分割に注意

Repository側をなるべくシンプルなCRUDにするように

class UserDomainService
{
    bool Exist(User user)
    {
        // こうではなく
        // repository.Find(user)
        // こっちの方が良いとのこと
        repository.Find(user.UserName)

        // この後nullチェックとか...
    }
}

class UserRepository
{
    User Find(UserName name)
        => // ...
}

// つまり... Service側ではUser単位で存在チェックするけど、
// その裏側のRepositoryではnameで引いてるよ。という感じ

IFを使ってテスト用のRepositoryを設けよう

private Dictionary<UserID, User> Store {get;} みたいなものをテーブルの代わりに使うイメージ

思ったこと:テストはインメモリでやる!ということだけど、これってSQLのテストにはならない
EFとかRailsのARみたいにクエリ書かかないタイプのORMならいいのかもだけど、Dapperとかだと微妙かな~
結合テストをする場合、Mock使うよりこっちの方が便利だとは思う。SQLのテストはさておき

アプリケーションサービス(ApplicationService)

ユースケースを実現するやつ。DomainServiceの対になる

例えばユーザ登録とか取得なんかはApplicationService

// 引数がプリミティブ型なのがポイントかも
void Register(string name)
{
    // nameを持つUserのインスタンス(DomainObject)つくって
    // 存在チェック(DomainService)して
    // 登録(Repository)
}

User Get(string userId)
{
    // userIdを値に持つUserIdのインスタンス(DomainObject)を作って
    // IDを元にデータを引く(Repository)
}

この時、ApplicationServiceを利用する側にDomainObjectを露出するか否かが問題になってくる
→ ApplicationServiceの呼び出し元にDomainObjectを直接触られると、メソッドを予期せず利用される懸念がある(ChangeName()は値を代入し直す処理だけどデータストアのUpdate的なニュアンスで使われたり...)

その対策としてDTO(DataTransferObject)を利用すると良いとのこと

public class UserData
{
    public UserData(string id, string name)
    {
        Id=id;
        Name = name;
    }
    public string Id { get; }
    public string Name { get; }
}

public class UserApplicationService
{
    // 呼び元に返すのはプリミティブ型のみを保持するDTO
    public UserData Get(string userid)
    {
        Userld targetld = new Userld(userid);
        User user = userRepository.Find(targetid);
        UserData userData = new UserData(user.ld.Value, user.Name.Value);
        return userData;
    }
}

思ったこと:DTOを使うかどうかはプロジェクトのポリシーによるところで、正しいけどここまでやるのは冗長かな。と個人的には思ってしまう
最終的にアウトプットに使うオブジェクト(ViewModelとか)に持たせたりするオブジェクトをDTOにする、ってとこまでやるとそこまで無駄に感じられなくなるかもだけど、DTOとDomainObjectで共通する処理が出てくるのではないか?!とか心配になる
ここらへんは実装してみて学ぶしかないか

更新系の処理のためのCommandObject

更新系(Update, Delete)の処理について、「ユーザ名だけ変更したい、アドレスだけ変更したい、...」等々の要望に対応していく場合、素でやると単純にServiceの引数を増やしていくことになる。大変 void Update(string userId, string name = null, string email = null, ...etc)

そういうときはCommandObjectを使うと部分更新に便利で良いらしい

public class UserUpdateCommand
{
    // 更新するフィールドにのみ値を入れる
    public UserUpdateCommand(string id, string name = null, string mailAddress = null)
    {
        Id = id;
        Name = name;
        MailAddress = mailAddress;
    }

    public string Id { get; }
    public string Name { get; }
    public string MailAddress { get; }
}

public class UserApplicationService
{
    // CommandObjectを引数に
    public void Update(UserUpdateCommand command)
    {
        UserId targetld = new Userld(command.ld);
        User user = userRepository.Find(targetid);
        if (user == null) { throw new UserNotFoundException(targetid); }

        // nameのチェック
        string name = command.Name;
        if (name != null)
        {
            UserName newUserName = new UserName(name);
            user.ChangeName(newUserName);
            if (userService.Exists(user)) { throw new Exception(user, "ユーザが既に存在しています"); }
        }

        // emailも似たようにチェック
        // ...

        userRepository.Save(user);
    }
}

// こう呼ぶ
var updateCommand = new UserUpdateCommand(id)
{
    Name = "hoge"
    // メールアドレスも更新したかったら...
    //Email = "hoge@example.com"
}
userApplicationService.Update(updateCommand);

思ったこと:部分更新に便利とは言うけど、いうほど手間は変わって無いのでは?
単純に引数群を1オブジェクトにまとめる意義はあるけど
あとなんとなくRailsっぽくなる気がするけど気のせいかも

低凝集にならないよう気をつける

あとApplicationServiceは低凝集になりがちなので気をつける

例えば以下のような状態だと、コンストラクタDIするRepository, DomainServiceの後者が一部のメソッドからしか呼ばれない状態

class UserApplicationService
{
    // repo
    private readonly IUserRepository userRepository; 
    // domain service
    private readonly IUserService userService;

    public void Register(string name, string mailAddress)
        =>  // RepositoryとDomainService両方使う

    public void Delete(string name, string mailAddress)
        =>  // Repositoryのみ使う
}

そういうときはUserRegisterService, UserDeleteServiceに分割するといいよとのこと

思ったこと:それぞれの処理が激長いなら分割もいいと思うけど、見通し悪くなるから一つのApplicationServiceにまとめちゃっていいんじゃないかなと思う

ファクトリ(Factory)

オブジェクトの生成を債務とするオブジェクト

例えばUserのインスタンスを新規に生成するとき、DBから次のIDを採番して、そのIDをもたせたかったりする
そういう時、UserのDomainObjectにその処理を書くと、その中でデータストア(今回はDB)に接続することになりNG
(DomainObjectは高レベル概念、DB操作は低レベル概念)
そこでFactoryクラスをつくる

こういう感じ

public interface IUserFactory
{
    User Create(UserName name);
}

public class UserFactory : IUserFactory
{
    public User Create(UserName name)
    {
        string seqld;
        // ここでコネクションつくってDB接続してDB固有の関数やらで次のIDを取得
        // Repositoryからもってくるわけではない
        // Factoryから外れるけど、FactoryナシでRepositoryで採番するのもあり
        seqld = /* hogehoge */

        UserId id = new Userld(seqld);
        return new User(id, name);
    }
}

public class UserApplicationService
{
    // 例のCommandObjectを引数に使う
    public void Register(UserRegisterCommand command)
    {
        UserName userName = new UserName(command.Name);
        // ここでFactoryを使う
        User user = userFactory.Create(userName);
        if (userService.Exists(user)) { throw new CanNotRegisterUserException(user); }
        userRepository.Save(user);
    }
}

思ったこと:TODO 上記のようにDB接続が必要な時以外でFactoryを使うシーンがあんまり想定できないので、色々やってみたほうがよさそう

定義するnamespace的な話

Factoryクラスはその存在に気づきやすいように、DomainObjectと同じところに置くとよい

・HogeDomain.Models.Users.User
・HogeDomain.Models.Users.IUserFactory

クラスではなくメソッドでFactory

Factoryクラス(のCreate())そのものではなく、DomainObjectにFactoryとして機能するメソッドを設ける場合もある

// こういうことをするとあんまりよくない
Circle circle = new Circle(
    // サークル主催者のID
    user.id, // DomainObjectのgetterを使ってる(集約的によくない)
    new CircleName("hoge circle"))
);

// なのでこっちにつくる
public class User
{
    // privateにできる!
    private readonly Userld id;

    public Circle CreateCircle(CircleName circleName)
        => new Circle (id, circleName)
}

これら以外にも、コンストラクタで別のオブジェクト作ってしまってたり、処理が複雑すぎる場合にはFactoryとして切り出せばいいみたい

DDDのオブジェクトではないけど:整合性(Consistency)

Validationではなくてトランザクション周りの話

トランザクションをどこで張る?
→ Serviceで張りたい → でもそうするとServiceがDB(低レイヤ)に依存してしまう

なのでC#ではTransactionScope

using (TransactionScope scope = new TransactionScope())
{
    // この中の処理でコネクションを開くと自動的にトランザクションを張ってくれる
}

Javaとかでは @Transactional を利用する(効能はC#のものと同じ)

あるいはユニットオブワークというパターンを使えばいいらしいけど、これはキツそうなので見なかったことにした

参考:https://nrslib.com/bottomup-ddd-2/#outline__3_5

DDDのオブジェクトではないけど:集約(Aggregation)

こんな感じにオブジェクトをまとめて、集約に含まれるオブジェクトに対する外部からの操作をすべて集約ルートを通じて行うようにする感じの話

集約の境界内に存在するオブジェクトを外部にさらけ出さないことで、集約内の不変条件を維持できるようにしているのです。

関連:デメテルの法則

集約は例えば以下のような感じ

Circle ------- 0..n -------User
 ├ CircleId                ├ UserId
 └ CircleName              └ UserName

 CircleとUserがそれぞれ集約ルート(AggregationRoot)

上記に対して...

user.Name  new UserName("hoge");       // NG
user.ChangeName(new UserName("hoge")); // OK

ということでこうなると、集約ルート以外のgetterを完全に隠蔽したくなる。
が、そうするとRepositoryで困ったりする

public class EFUserRepository : IUserRepository
{
    public void Save(User user)
    {
        // DataModelを作る際、DomainObjectのgetterを使ってしまっている!
        UserDataModel userDataModel = new UserDataModel
        {
            Id   = user.ld.Value,
            Name = user.Name.Value
        };
        context.Users.Add(userDataModel);
        context.SaveChanges();
    }
}

この対応として素直にgetterを許すと変なとこで使われる様になってしまうかも...
→ そういう場合、通知オブジェクト(IUserNotification)ってのを使うといいよ。ってことだけど、コレを使うのは大変そうなのでここでもスルーする...

参考:https://nrslib.com/bottomup-ddd-2/#outline__5_5_3

集約の切り方について

上の例でいくとCircleの集約の中でUserを変更するのはだめ

public class Circle
{
    private List<User> members;

    // User集約の外からUserの中身を触るのはNG
    public void ChangeMemberName(Userld id, UserName name)
    {
        User target = members.FirstOrDefault(x => x.ld.Equals(id));
        if (target != null) { target.ChangeName(name); }
    }
}

そもそもこういった操作を防止するためにインスタンス(User)を保存するのではなくて、識別子(UserId)を保存するとよい

// こうじゃなくて
// public List<User> Members {get; private set;}
// こう
public List<UserId> Members {get; private set;}

思ったこと:最終的にオブジェクトをViewModelとかに渡した後でView側で使う時に面倒くさくなりそう
例えば上記Membersをループしてメンバーのリストを表示する時とか、ワザワザ別途List<User>からもってこないといけない ここでそもそもViewModelに渡すのはDomainObjectじゃなくてDataModelを使えばいいのでは?とかそういう話が出てくるのかな

仕様(Specification)

仕様そのものをドメインのオブジェクトとして切り出す

ちょい上の「集約の切り方について」の話のままいくと、CircleはメンバーのリストとしてUserIdを保持するが、Userのインスタンスは保持しない作りになっている

そこでCircle側に「プレミアムユーザの数が10人以上ならメンバー数上限増やすよ」みたいな仕様が増えると大変
Circle側でユーザがプレミアムユーザか判定するために、CircleのDomainObjectがRepository使うのもおかしい
→ という時にこの仕様オブジェクトを作るとよいとのこと

こんな感じ

public class CircleFullSpecification
{
    private readonly IUserRepository userRepository;

    public bool IsSatisfiedBy(Circle circle)
    {
        List<User> users = userRepository.Find(circle. Members);
        int premiumUserNumber = users.Count(user => user.lsPremium);
        int circleUpperLimit = premiumUserNumber < 10 ? 30 : 50;
        return circle.CountMembers() >= circleUpperLimit;
    }
}

// 使う
public class CircleApplicationService
{
    public void Join(CircleJoinCommand command)
    {
        CircleId circleld = new Circleld(command.Circleld);
        Circle circle = circleRepository.Find(circleld);
        CircleFullSpecification circleFullSpecification = new CircleFullSpecification(userRepository);
        if (circleFullSpecification.lsSatisfiedBy(circle))
        {
            throw new CircleFullException(circleld);
        }
    }
}

仕様クラスでRepository使うことについて

仕様もDomainObjectから切り出したものなので、その中でRepositoryを使うのはイヤ...という話もある
→ そういう場合はファーストクラスコレクションというものを使うと良い → が、面倒くさそうなのでここもスルー

仕様を元に検索する

こうした仕様を元に検索をする場合、仕様オブジェクトをRepositoryに渡して絞り込むといい
でもそれって結局、全件( or ページングで区切った分だけ)読み出して、そこからLINQのWhereっぽく1つ1つのインスタンスに対してフィルタをかけることになってしまう
→ そうするとパフォーマンスの問題が発生しがち。なので直接クエリを発行するといい

これがCQRSの概念につながるけど本書ではパスしている

public class CircleQueryService
{
    public CircleGetSummariesResult GetSummaries()
    {
        var connection = provider.Connection;
        using (var sqilCommand = connection.CreateCommand())
        {
            sglCommand.CommandText = @"
SELECT
circles.id as circleld,
users.name as ownerName
FROM circles
LEFT OUTER JOIN users
ON circles.ownerld = users.id
ORDER BY circles.id
";
            using (var reader = sqglCommand.ExecuteReader())
            {
                // readerから結果を読み取って変換してreturn
                return new CircleGetSummariesResult(summaries);
            }
        }
    }
}

DDDにまつわるアーキテクチャ

DDDで重要なのはドメインが分離されること
どのアーキテクチャでなければならないというような縛りはない

レイヤードアーキテクチャ

エリック・エヴァンスのやつ

UI             : MVCのController(UIってかpresentation?)
Application    : ApplicationService
Domain         : DomainObject, DomainService. DomainをサポートするFactoryやRepositoryのIFもここ
Infrastructure : Repository, ORM

上の層がそれより下の層を利用するイメージ

ヘキサゴナルアーキテクチャ

話の中心のアプリケーションとそれ以外のIF(DBとかWebとか)を付け外せるように。という考え方

ポイントはIFを利用した依存関係の整理にある
レイヤードアーキテクチャはあくまでも層分けでIFは任意。でもレイヤードアーキテクチャでも自然とIF使うよね。ってことで今ではここの垣根はほぼ無いみたい

クリーンアーキテクチャ

ヘキサゴナルアーキテクチャ:取り外しOKっていう方針のみ クリーンアーキテクチャ:具体的な実装まで指示

TODO クリーンアーキテクチャの理解は重いのでもっとまともに勉強してから書いたほうが良さそう

ソリューション構成について

言語やFWによって方法は変わるけどC#だとひとまずこんな感じでいいみたい

フォルダ構成について

Domain
    Models
        Circles
            Circle.cs
            CircleId.cs
            CircleName.cs
            ICircleRepository.cs
            RecommendedCircleSpecification.cs
        Users
            User.cs
            UserId.cs
            UserName.cs
            IUserRepository.cs
            IUserFactory.cs
    Service
        UserService.cs
    Shared
        ISpecification.cs

Application
    Circles
        CircleApplicationService.cs
        CircleCreateCommand.cs
        CircleGetCommand.cs
        CircleGetResult.cs
        CircleData.cs
    Users
        UserApplicationService.cs
        UserCreateCommand.cs
        UserGetCommand.cs
        UserGetResult.cs
        UserData.cs

Infrastructure
    Circles
        EFCircleRepository
        InMemoryCircleRepository
    Users
        EFUserRepository
        InMemoryUserRepository
        EFUserFactory
        InMemoryUserFactory

ソリューション構成

構成ってか上記のDomain, Application, Infrastructure, そしてPresentationをプロジェクトとしてどうまとめるか?という話

Domainは他によらず使い回せるようにしたほうがいい。ということで、 本書でのおすすめは「全部別」or「アプリケーションとドメインだけ同じプロジェクト」

PresentationとかInfrastructure、つまりユーザとのI/Oとかデータソースは変わる可能性あるし分けるのも妥当
アプリケーションとドメインはそう離れないよね。みたいな感じっぽい

DDDのさわりを勉強した感想

よくあるMVC文脈ではServiceって言葉がとっ散らかってDDDでいうDomainService, ApplicationServiceが全部入りになった挙げ句巨大になったり、ControllerにDomainやServiceの実装が漏れてたり、コネクション作るのがいろんな層に散っていたり、そういう辛い問題に直面したことが多く、DDDに基づいた開発ポリシーを統一してそれら回避できると嬉しいなとは思う

DDDのDomainってつまるところMVCのMなわけでそこを切るだけでもキレイになるはず

しかし「それは確かに理想的だけど開発現場の統制を取るのはしんどい」みたいなパターンが多かった
例えばアクセサでメソッドやgetter/setterの利用を制限しても、「こういう思想で作ってる」って前提をそもそも理解してもらえないとDDDの理想からズレたところに新しいメソッドが定義されるようになったりすると思う
更にメンバーの全員が全員この方面に勉強熱心ってこともないのが世の常で、0からチームを作るわけでもない時にDDDをガッツリ突っ込んでいくのは厳しい
(ここまでの通り僕もパターンをスルーしまくってるし人のことは言えない)

というところで本書の中でも言葉が出てきた軽量DDDに逃げることになるんじゃないかなぁ

など書いててお気に入りのスライドを思い出した

www.slideshare.net

残課題

ほんとはReactとか触りたいけど、僕の中ではこっちをもうちょっと固めないといけない...