実装まで書き込むと想定以上に記事 & 執筆時間が長くなりそうだったのでフォルダ構成までとしました。。
いずれ完成させたいと思います。。
前置き
先日Twitterの海をさまよっているとこんなツイートを見かけました。
積読している本を読むきっかけのアプリ欲しい。
— DAI (@never_be_a_pm) December 21, 2020
①ユーザーは積読している本を追加できる
②ユーザーは読書会を開催できる
③積読している本に通知を送る
④ユーザーは読書会に参加できる
⑤ユーザーはログを残せる pic.twitter.com/tcfTGazk3V
いい感じの題材だなと思ったので、ある程度の解釈・簡略化を挟みつつ実装していこうと思います。
ユビキタス言語・ユースケースを考える
まずはざっくり考えていきます。
重要な概念を抽出する
引用ツイートではさくっと必要機能を書き出してくれています。
①ユーザーは積読している本を追加できる
②ユーザーは読書会を開催できる
③積読している本に通知を送る
④ユーザーは読書会に参加できる
⑤ユーザーはログを残せる
これらから見いだせる重要そうなキーワードはとりあえず下記のような感じでしょうか。
- ユーザー => user(s)
- 積ん読 => piled_book(s)
- 本 => book(s)
- 読書会 => reading_group(s)
- ログ => reading_group_discussion(s)
おそらくこれらのキーワードがアプリケーションのコアになってくるでしょう。
さらに、よくよく考えてみるとアプリケーションとして動作するにはまだ言及されていない機能要件がありそうです。
細かいユースケースを考える
例のツイートにはすばらしいことに画面例までついていました。
画面を見るに、ユーザーの詳細なユースケースは下記のようになりそうです。
※ 僕が想像して追加したものもあります
アカウントを作成できる
本を検索できる
本を積むことができる
自分の積ん読リストを確認できる
積ん読を読了ステータスにすることができる
読書会を開催できる
読書会を探せる
読書会に参加できる
読書会から退出できる
参加した読書会のディスカッションに投稿できる
ユースケースをコンテキストに応じて分割する
DDDの主要概念の一つに「境界づけられたコンテキスト = Bounded context」というやつがありますね。
正直僕もBounded contextの明確な分類手法を知っているわけではないんですが、上記のユースケースを見る限り下記のように分割できそうです。
- identity-access
アカウントを作成できる
- book-management
本を検索できる
本を積むことができる
積ん読リストを確認できる
積ん読を読了ステータスにすることができる
- reading-group
読書会を開催できる
読書会を探せる
読書会に参加できる
読書会から退出できる
参加した読書会のディスカッションに投稿できる
各コンテキスト内で現れる(であろう)モデルを書き出す
今わかる範囲で書き出します。
- identity-access
User: ユーザーアカウント
- book-management
User: 本を検索したり、積ん読を管理したりするユーザー
Book: 本。基本的にユーザーは本を検索し、場合によって積ん読に追加する
PiledBook: ユーザーの積ん読。
- reading-group
Organizer: 読書会の開催者
Member: 読書会の参加者
ReadingGroup: 読書会
Discussion: 読書会内のディスカッション
Post: ディスカッション内の投稿
こんな感じになるでしょうか。
以下が本題です。
CleanArchitecture with TypeScriptでのフォルダ構成
今でも自分が完全に納得する正解にはたどり着いてないですが、僕が現状の知識でこの積ん読アプリを作るなら下記のような構成になります。
tsundoku (root)
├── package-lock.json
├── package.json
├── tsconfig.json
└── src
├── app.ts
├── edge
│ ├── api
│ │ └── express
│ │ ├── index.ts
│ │ └── routes
│ │ ├── book-management
│ │ │ └── index.ts
│ │ ├── identity-access
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ └── reading-group
│ │ └── index.ts
│ └── datastore
│ └── rdb
│ └── mysql
│ └── typeorm
│ ├── entities
│ │ ├── base
│ │ │ └── base-model.ts
│ │ └── book.ts
│ ├── index.ts
│ └── migrations
└── services
├── book-management
│ ├── edge-adapter
│ │ ├── controller
│ │ │ └── index.ts
│ │ ├── presenter
│ │ │ └── index.ts
│ │ ├── query
│ │ │ └── book-query-service.ts
│ │ └── repository
│ ├── interactor
│ │ ├── book-query-service.ts
│ │ └── index.ts
│ └── model
│ ├── book
│ └── user
├── identity-access
│ ├── edge-adapter
│ │ ├── controller
│ │ │ └── index.ts
│ │ ├── presenter
│ │ │ └── index.ts
│ │ └── repository
│ ├── interactor
│ │ └── index.ts
│ └── model
│ └── user
├── reading-group
│ ├── edge-adapter
│ │ ├── controller
│ │ │ └── index.ts
│ │ ├── presenter
│ │ │ └── index.ts
│ │ └── repository
│ ├── interactor
│ │ └── index.ts
│ └── model
│ ├── discussion
│ └── group
└── z-service-ifaces
├── edge-adapter
│ └── repository
│ ├── base-repository.ts
│ ├── db-handler.ts
│ └── transactional-handler-store.ts
└── model
├── entity.ts
├── repository.ts
└── value-object.ts
さすがに分かりづらいので下記で噛み砕いていきます。
ディレクトリのネーミングと意味
下記は上記のツリーからディレクトリのみを抜き出したものです。
.
└── src
├── edge
│ ├── api
│ │ └── express
│ │ └── routes
│ │ ├── book-management
│ │ ├── identity-access
│ │ └── reading-group
│ └── datastore
│ └── rdb
│ └── mysql
│ └── typeorm
│ ├── entities
│ │ └── base
│ └── migrations
└── services
├── book-management
│ ├── edge-adapter
│ │ ├── controller
│ │ ├── presenter
│ │ ├── query
│ │ └── repository
│ ├── interactor
│ └── model
│ ├── book
│ └── user
├── identity-access
│ ├── edge-adapter
│ │ ├── controller
│ │ ├── presenter
│ │ └── repository
│ ├── interactor
│ └── model
│ └── user
├── reading-group
│ ├── edge-adapter
│ │ ├── controller
│ │ ├── presenter
│ │ └── repository
│ ├── interactor
│ └── model
│ ├── discussion
│ └── group
└── z-service-ifaces
├── edge-adapter
│ └── repository
└── model
src配下はedge
とservices
に分かれているのがおわかりかと思います。
さらにservices配下は、前段で決めた「境界づけられたコンテキスト」の名前がついたディレクトリが並んでおり、それぞれの配下にedge-adapter
、interactor
、model
の3つが格納されています。
また、コンテキストとは別にz-service-ifaces
というディレクトリもあります。
下記でそれぞれの役割を説明していきます。
src/edge
このディレクトリには、このアプリケーションが外界と触れる部分を実装します。
外界との「縁」なのでedgeです。
src/edge/api/express
expressを使ったAPIインターフェースが実装されています。
src/edge/datastore/rdb/mysql/typeorm
typeormによるMySQLテーブル定義が格納されています。
src/services
このディレクトリは境界づけられたコンテキストごとに実装されたサービス群が格納されています。
それぞれが完全に独立したサービスとなっており、やろうと思えばあるサービスを別のAPIサーバで動かすことも容易であるように作ります。
src/services/z-service-ifaces
各サービスで利用する共通インターフェース群を格納してあります。
z-
としてあるのはservicesディレクトリ内で一番最後に置いておきたかったがための苦肉の策です。。
src/services/[service-name]
境界づけられたコンテキストの実装です。
edge-adapter => interactor => model の順に依存関係となっており、alphabetical orderでもその順になるようにネーミングしてあります。
一番外側をedge
としているのもこのためだったりします。地味に。
edge-adapter
controller: 全ての処理の起点であり、edge
から呼ばれ、interactor
を呼び出す役割
presenter: interactor
からのレスポンスをedge
に返却する前に、APIレスポンスとしての体裁を整える役割repository: interactor
内で利用され、DBからデータの取得、モデルへの変換の役割
query: interactor
内で利用され、repository
ではカバーしきれない柔軟なクエリを実行する役割
interactor
controller
によりインスタンス化され、同時にpresenterやrepository、query-serviceなど、アプリケーションロジックの実装に必要なadapter群を渡される。
presenter及びquery-serviceのインターフェースはこのディレクトリ内にあり、edge-adapter内で実装されている。
model
コンテキスト内で必要とされるドメインモデル群。
必要に応じてドメインサービスの定義もここで行う。
ドメインサービス内では別ドメインモデルの取得などが必要とされる場合もあるため、repositoryのインターフェースはこのディレクトリ内に置いておき、実装はedge-adapter内で行う。
最後に
尻切れとんぼな記事になり申し訳ありません、いずれアプリケーションとして完全動作するサンプルをgithubに上げるつもりでおりますので、気長に見ていただけると幸いです。。
とりあえず本記事がTS & DDDのフォルダ構成に迷っている方の参考になればいいなと思います。