LoginSignup
6
1

More than 3 years have passed since last update.

積ん読アプリケーションをTypeScript, CleanArchitectureでDDDする (フォルダ構成編)

Posted at

実装まで書き込むと想定以上に記事 & 執筆時間が長くなりそうだったのでフォルダ構成までとしました。。
いずれ完成させたいと思います。。

前置き

先日Twitterの海をさまよっているとこんなツイートを見かけました。

いい感じの題材だなと思ったので、ある程度の解釈・簡略化を挟みつつ実装していこうと思います。

ユビキタス言語・ユースケースを考える

まずはざっくり考えていきます。

重要な概念を抽出する

引用ツイートではさくっと必要機能を書き出してくれています。

①ユーザーは積読している本を追加できる
②ユーザーは読書会を開催できる
③積読している本に通知を送る
④ユーザーは読書会に参加できる
⑤ユーザーはログを残せる

これらから見いだせる重要そうなキーワードはとりあえず下記のような感じでしょうか。

  • ユーザー => 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配下はedgeservicesに分かれているのがおわかりかと思います。

さらにservices配下は、前段で決めた「境界づけられたコンテキスト」の名前がついたディレクトリが並んでおり、それぞれの配下にedge-adapterinteractormodelの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のフォルダ構成に迷っている方の参考になればいいなと思います。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1