のんラボ

社内でドメイン駆動設計入門の読書会 #9

2020/04/21 2020/04/21 社内でドメイン駆動設計入門の読書会 #9

社内でドメイン駆動設計入門の読書会 #9

こんにちは。Nonです。

今回も会社で読書会をしている話をしようと思います。

内容は控えめに、ディスカッションの内容重視で書いていきたいと思います。

より具体的なコードや内容がみたい!という方は購入しましょう!

前回:会社でドメイン駆動設計入門の読書会 #8

読んでいる本

読んでいる本はこちらのドメイン駆動設計です。

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本


以前の記事でも言っていたように、業務でDDDを利用して開発することが多くなったのですが、DDDに精通している人が少ないという問題がありました。

そこで、その精通している人が読書会をしようかと誘ってくださいまして、是非にと参加させていただきました。

進行方法

読書会の進行方法は

  1. 今回読書の対象にする章を決める。
  2. 10〜20分間その章を読む
    1. 読み終わってしまった人は、もう一周読み直すか、次の章に進んでもらう
  3. その後40分間で、その章に対する疑問や考え方をディスカッションする
  4. 1〜3を毎週定期的に行う

という進行方向となっています。

社内で読書会をするのはこれが初めてなので、進行方法はもっといいのあったら教えて下さい。

今回読んだ内容

  • 複雑な生成処理を行う「ファクトリ」
  • ファクトリの目的
  • 採番処理をファクトリに実装した例の確認
    • 自動採番機能の活用
    • リポジトリに採番用のメソッドを用意する
  • ファクトリとして機能するメソッド
  • 複雑な生成処理をカプセル化してみよう
  • まとめ

ファクトリパターンをあまり勉強せずにこれまでやってきた私にとって、とても勉強になる章でした。

ディスカッション

自動採番をプログラムで行う

ちなみに、私は採番処理をプログラム側で制御するという発想そのものが新しい価値観でした。

これまで携わった案件のほとんどがRDBを採用したプロジェクトでしたので。RDBを採用しないアプリはそもそもデータストアが必要ないというプロジェクトでした。

ファクトリってどういうときに使えばいいの?

バックエンドで多いのはEntity作成時じゃない?

Entityを作成する = データストアにレコードが存在する。
なぜならEntity作成時にレコードが存在しないと、そのまま永続化しない場合、IDに揺らぎが起きてしまうから。これは第3章でも上がっていた内容だったはずです。

ということはEntity作成時はレコードを登録する必要があるかもしれません。

copied.class ProductEntity
{
    private $id;
    private $name;
    private $price;

    public function __construct(string $name, int $price)
    {
        $product = (new Product())->newQuery()->create([
            'name' => $name,
            'price' => $price,
        ]);

        $this->id = (int)$product->getAttribute('id');
        $this->name = (string)$product->getAttribute('name');
        $this->price = (int)$product->getAttribute('price');
    }
}

嘘やろ?といった感じのコードですね。
ちなみに自動採番機能もMySQLに依存しています。これを許容するにしても、なんとなく気持ち悪いです。

この__constructで行っている生成過程をFactoryに移すとこうなります。

copied.class ProductEntityFactory
{
    public function newProductEntity(string $name, int $price): ProductEntity
    {
        $product = (new Product())->newQuery()->create([
            'name' => $name,
            'price' => $price,
        ]);

        return new ProductEntity(
            (int)$product->getAttribute('id'),
            (string)$product->getAttribute('name'),
            (int)$product->getAttribute('price')
        );
    }
}

class ProductEntity
{
    private $id;
    private $name;
    private $price;

    public function __construct(int $id, string $name, int $price)
    {
        $this->id = $id->getAttribute('id');
        $this->name = $name;
        $this->price = $price;
    }
}

こうすることでEntityの作成処理はFactoryが行うことになるので、Entityのコードもきれいになりました。

このような効果がありそうです。

大量INSERTなどは?

高速化を目的としたときは素直にバルクインサートしたほうが良いかも。

大量データの登録はCSVの情報をそのまま登録するようなドメインの薄いユースケースでしょうし、問題ありません。ただ、サービス化はしておいて変更に強いように設計する必要はありそうですが……

もし複雑な変換処理がある場合は、EntityではなくCollectionにして、Collectionのふるまいで値の加工をします。

その後でtoArray()などで配列にしバルクインサート、などの手法も考えることができますね。

もちろんファクトリにもInterfaceクラスを実装する

前章で抽象クラスに依存せよ。とありましたが、ファクトリもこれに従ったほうが良いです。

copied.class ProductApplicationService
{
    private $factory;

    public function __construct(ProductEntityFactoryInterface $factory)
    {
        $this->factory = $factory;
    }

    public function process():void
    {
        $product = $this->factory->newProductEntity('おにぎり', 100);
    }
}

interface ProductEntityFactoryInterface
{
    public function newProductEntity(string $name, int $price): ProductEntity;
}

class ProductEntity
{
    private $id;
    private $name;
    private $price;

    public function __construct(int $id, string $name, int $price)
    {
        $this->id = $id->getAttribute('id');
        $this->name = $name;
        $this->price = $price;
    }
}

こうするとnewProductEntity()の実装は自由自在ですね。

最後に

ファクトリパターン難しそうなので敬遠していたのですが、一度違う本でも呼んでみるとスッと入ってきました。

これまでconstructにダラダラ書いていたのは何だったのか……

次回はデータの整合性を保つです。
また記事にしますので、その時はよしなに。

.