はじめまして! これはディップ Advent Calendar 2023の3日目の記事です。
目次
1.はじめに
2.クリーンアーキテクチャの重要なポイント
3.レイヤーの役割と依存性の定義
4.デザインドキュメントをベースとしたチーム開発
5.この開発体制のよかったポイント・大変だったポイント
6.おわりに
1.はじめに
この記事では、実際に私たちがチーム開発の中で実践した、デザインドキュメントを用いた開発の様子をまとめます。
提案手法
- 各開発者は機能を実装する前に、Figmaにクリーンアーキテクチャに沿った設計図をテンプレートに沿って書いてから、実装を開始する。
- プロジェクトメンバーはエンジニア・ディレクターなど役割に関係なく全員がこのデザインドキュメントとレイヤーの役割を意識しながら話すことで、実装との乖離をなくす。
- 修正・追加がある場合は、ドキュメントに修正を必ず加えて、最新の状態をキープする。
イメージとしては、このデザインドキュメントがディレクター・実装エンジニア・レビュアーの共通言語となる感じです。
では2章以降で詳しくまとめていきたいと思います。
こんな人・プロジェクトにおすすめ
- PdM陣とエンジニア陣にコミュニケーションの壁がある
- 機能追加・修正を行いたいが、実装エンジニア次第になってしまっているPdM。
- 逆にディレクター・設計者からの指示がわかりにくく、実装イメージがつかないエンジニア。
- たくさんの開発者をまとめる立場にあり、各開発者が出してくるプルリクの理解に工数を取られているテックリード。
- 納期の短い新規プロジェクトのため、仕様・システム構成が固まり切っていない中でもアジャイルに開発を進めたい。
- なのに仕様が理解できる資料も欲しい。
今回の記事の背景
- バックエンドAPIの開発
- 開発チームの全体像としては、エンジニアが20人弱、データサイエンティスト5人、PdM2人
- 筆者はアーキテクチャ・開発のテックリードを担当
- 新規サービス開発
- 自社サービスに組み込む生成AI系のAPI開発
- 技術
- Python FastAPI
- AWS
- ツール
- Figma
2.クリーンアーキテクチャの重要なポイント
この記事では、クリーンアーキテクチャの詳細の説明ではなく、それを元にした開発手法をまとめるつもりですが、その理解に必要最低限のクリーンアーキテクチャのエッセンスは次の2つです。[1]
- レイヤーに分離することで、関心事の分離を行う
- 依存性は内側だけに向かっていなければならない
デザインドキュメントを作成するにあたり、この2つを意識する必要があります。
関心ごとの分離について、イメージとしては、
- ビジネスロジックを定義するレイヤー
- 外部データ(ファイルやDBなど)にアクセスするためのレイヤー
- 外部サービスにアクセスするレイヤー
などがあり、アプリケーションで実装したい機能の流れを分解すると、これらの役割の組み合わせだと捉えることができます。
この関心ごとの分離や依存性の通りに実装することによって、
- 修正の影響範囲がわかりやすくなる
- 同じ役割のものをまとめておくため、理解・管理がしやすい
などの恩恵を受けることができます。
次章にて私たちが利用したレイヤー構成、依存性について詳細をまとめていきます。
次章はやや開発に踏み込むので、デザインドキュメントを使った開発の様子に興味がある方は4章に飛んでも構いません。
3.レイヤーの役割と依存性の定義
私たちのチームでは7つのレイヤーを定義しました。
色分けはクリーンアーキテクチャの図に合わせていて、デザインドキュメントを作成する上でも大事になってきます。
routers
entities
use cases
services
repository
query_services
gateway
これらはクリーンアーキテクチャの図に登場するレイヤーを全て利用しているわけでもありませんし、逆にこの図に存在しない使い勝手の良いレイヤーを定義することもできます。
ただ、チーム開発の中では、「各レイヤーがどういう役割(関心事)があるのかを明確にする必要があります。
各レイヤーの依存関係
レイヤーの依存関係をまとめました。lower(行)->upper(列)への依存を許容します。
router | use_case | service | repository | query_services | gateway | entity | |
---|---|---|---|---|---|---|---|
router | × | ○ | × | × | × | × | ○ |
use_case | × | × | ○ | ○ | ○ | ○ | ○ |
service | × | × | △ | ○ | ○ | ○ | ○ |
repository | × | × | × | × | × | × | ○ |
query_services | × | × | × | × | × | × | ○ |
gateway | × | × | × | × | × | × | ○ |
entity | × | × | × | × | × | × | ○ |
要するに
横: 何を呼び出して良いのか
縦: どこから呼び出される可能性があるのか
を表しています。
特に、実装の中でよく現れた間違いとして以下のようなものがありました。
- entity層からは、entity層以外のレイヤーを呼ぶことができない
- use_case層からuse_case層を呼ぶことはできない
事例
1について、entityの中では、データのバリデーションを書くことになりますが、その時、DBを参照しないと分からないようなバリデーションを書こうとしてrepositoryを呼び出そうとするなどがありました。このケースに関しては、この記事[2]にもあるように、値の整合性のみをバリデーションで確かめて、repository層を呼び出したuse_case層で例外を書くようにしました。
2について、use_caseが複数になってくると、別のuse_caseで作成したロジックを使いたくなることがあり、use_caseからuse_caseを呼び出したくなることがあります。その他、そもそもuse_caseの責任分解が甘いと、他のuse_caseを呼び出さざるを得ない設計になってしまうなどがあります。ただ、use_case->use_caseの依存を許してしまうと、use_caseの修正範囲が他のuse_caseに及ぶ可能性を考えながら実装をする必要が出てしまうため、絶対にやってはいけません。これについては、共通で使うう部分をservice層に切り出す、またはuse_caseの設計を見直すことで対応しました。
ディレクトリ構成
わかりやすいディレクトリ構成にすることも重要で、実装時にできるだけ迷いのないディレクトリ構成にすることを考えると以下のようなディレクトリ構成になりました。
.
├── Makefile
├── docker-compose.yaml
├── docker
│ └── Dockerfile
├── src
│ ├── __init__.py
│ ├── config.py
│ ├── core
│ ├── db
│ ├── entities
│ ├── gateway
│ ├── gunicorn
│ ├── logger
│ ├── main.py
│ ├── query_services
│ ├── repository
│ ├── routers
│ ├── services
│ └── use_cases
└── tests
├── __init__.py
├── endpoints
├── entities
├── gateway
├── query_services
├── repository
├── routers
├── services
└── use_cases
これであれば、一目瞭然でどのレイヤーのモジュールなのかがわかります。
4.デザインドキュメントをベースとしたチーム開発
デザインドキュメントとは
実装に入る前に理解を深める手法として、デザインドキュメントを書くというのがあります。私たちの開発の流れとして、エンドポイントの実装前に、このデザインドキュメントをFigmaにまとめるルールを作りました。このデザインドキュメントはウォーターフォールで使われる設計書とは違い、短い期間で簡潔に作られ、コードと行き来しながら完成に向かっていくものです。
従来型のUp Front Designとの大きな違いは、これがプログラマのためのものだということだ。第三者のステークホルダーのために書いているのではなく、自分がいいソフトウェアを書く上で、理解して、効率よく開発するためにやっていることであって、誰かのためのものではない。[3]
例としては、下図のようなもので、各レイヤーの関係性がわかるように書いています。
ポイントをまとめます。
- クリーンアーキテクチャのレイヤーの色に合わせたセクションを作成
- リクエスト・レスポンスモデルやEntityには具体的な形式をまとめる
- 関数には自然言語で説明文をつける
- 依存関係に気を付ける
- routers層からはuse_caseにしか矢印を伸ばせない
- use_caseからrepositoryやgatewayに矢印を伸ばしている
- entityはそれらの層を行き来する
- その内容が書かれているファイルパスと名前もこの時点で決める
この粒度でデザインドキュメントができると、実装時の迷いがなくなりますし、この通りに書けば、クリーンアーキテクチャ通りのコードになるというわけです。
Googleなどで利用されているDesign Docというものも参考になります[4]。私たちが書いていたデザインドキュメントでは、あくまでエンドポイントの全体像・アーキテクチャに絞って書いています。
エンドポイントごとにデザインドキュメントを書く
私たちのプロジェクトでは、このデザインドキュメントがエンドポイントごとに書かれており、チームのFigmaではこのようになっています。(詳細はぼかしています)
これを見ると一つのエンドポイントにおいて
- リクエストモデル・レスポンスモデルはどのような形式でどのようなバリデーションがあるのか
- どのようなuse_case(ロジック)がいくつ使われているのか
- どのDBにつながっているのか
などをコードを読まずして理解することができます。
5.この開発体制のよかったポイント・大変だったポイント
よかったこと
- 開発者との認識齟齬の減少
- 機能を追加する場合や修正する場合、「このuse_caseの処理はテーブルAを操作するrepositoryで処理するべきではないか」「このバリデーション処理はentityに移そう」などレイヤー名で会話ができるようになった。
- 実装を元に話すことができるため、認識齟齬が起こりにくい。
- デザインドキュメントが書いてあるから、引き継ぎ、コードレビューが楽
- コードを読む前に、デザインドキュメントで大体の流れ、モジュールの関係を頭に入れることができた。
- 0->1の開発でも、どこに何を書くかが実装前に明確になる。
- デザインドキュメントを作成しないと開発に入れないため、自然とエンドポイントの処理の流れをレイヤーに沿った役割分解をする作業を強制できた。
- ドキュメント作成後は、ディレクトリ構成に沿って、適切な場所に実装するだけなので、スムーズに開発ができた。
大変だったポイント
- デザインドキュメントを常に最新にする意識
- 小さな修正が重なり、デザインドキュメントが古くなることは避けられない。修正前にデザインドキュメントに修正を加えることをルール化するべきだった。
- レイヤーを分けたところで、実装がレイヤーの役割分担ができているかどうかは別
- コードレビューをするときは、デザインドキュメント通りのモジュール構成になっているか、そして、処理が適切なレイヤーに書かれているかを注視する必要があった。
6.おわりに
私がチームの開発リーダーとして特に気をつけていることは、「その人しか理解できない」という状態を作らない(そういうエンジニアにならない)ということです。
よく、あのシステムは〇〇さんに頼まないと運用できない。だから〇〇さんは凄腕エンジニアなんだという風潮があるように思えます。
私は逆だと思っていて、むしろ、誰でも(エンジニア・PdM限らず)わかるような簡潔な仕組み、体制をチームメンバー全員で作りたいと願っていますし、それができるエンジニアが凄腕なのではないかと思っています。
その第一歩として、今回の記事のような開発手法を試してみました。やはりチーム一丸となって開発した方が大きな成果が出せますし、楽しいと感じます。
私は12/22のディップ Advent Calendar 2023も担当しているので、そこではより生成AIプロダクトの開発の実装に寄った記事を書きます。もしこの記事が良かったと思っていただけた方はそちらも合わせてご覧ください。
参考
[1]良いコードとは何か - エンジニア新卒研修 スライド公開
[2]Layered Architecture(Clean Architecture)の勘所
[4] Design Docの書き方