概要
こんにちは。サーバーサイドエンジニアの窪田です。 ヤプリのプロダクトではよりメンテナンス性の高いシステムを作るために、DDDを採用しています。 しかし、歴史が長い分一部のコードでは開発者によって異なる書き方をしたり、教科書的な書き方から大きく逸れた書き方をしている部分もあります。
そこで、サーバーチームではより良い設計のために開発者が統一的なコードを書けるように戦術的DDDの側面でのコーディング規約を作る取り組みを始めました。 現段階では、網羅的な規約にはなっておらず、運用もこれからしていく予定ですが、 何もないところから、全員で議論してルールを考えていくという活動には色々なメリットがあったのでその事例を紹介します。
取り組みの内容
目標を立てた
お察しの通り、エンジニアが設計の改善をし始めると永遠に終わらなくなります。 そこで、スコープを絞った目標設定をしました。 具体的には
- repositoryのみ
- 新規開発するコードのみ
という領域に絞りました。 他にも、domainモデル, domain サービス, useCase等に課題はありましたが、
- repositoryの課題が一番大きかった
- ドメインルールと関係なく比較的統一しやすい
という理由から最初の取り組みとしてrepositoryを選択しました。 また、ルールの試行錯誤をするために既存のコードのリファクタをいきなり始めるのではなく、 新規のコードに限ったルールを決めることを目標にしました。
議論した
議論した内容の一部を紹介します。
repositoryの取得系メソッドの名前について
DDDの本には、FindByIDやSaveなどのメソッドが登場しますが、これらの例では簡単なCRUD処理が紹介されている場合がほとんどです。 実務上では、いろいろなuseCaseがありDB上から様々な検索をすることになります。 例えば、ID, Name, EmailそれぞれでUserを検索する場合、User repositoryは以下のようになります。
type UserRepository interface { FindByID(ctx context.Context, tx adaptor.DBTx, id int) (*User, error) FindByName(ctx context.Context, tx adaptor.DBTx, name string) (*User, error) FindByEmail(ctx context.Context, tx adaptor.DBTx, email string) (*User, error) ... }
省略していますが、実際はあらゆるrepositoryで大量のメソッドが定義されていました。 これは A Philosophy of Software Design, 2nd Edition: Ousterhout, John: 9781732102217: Amazon.com: Books で主張されている「deep module」を大切にする考えに反します。
deep moduleとはインターフェイスはシンプルで実装がリッチなmoduleのことを指しています。 内部実装自体は複雑な処理をしていて多くの機能を備えているが、そのmoduleのユーザー(開発者)は簡単に呼び出すことができるというメリットが本文の中では述べられています。
「インターフェイスはシンプル」という側面で言えば上の例はあまり良くありません。useCaseを実装する開発者はどのメソッドを使ってUser entityを再構築するか迷います。
もう一つの案としては以下のようにメソッドを一つにし、paramsを指定することによってuseCaseに対応させようというものがありました。
type userFindParams struct { id *int name *string email *string } type UserRepository interface { Find(ctx context.Context, tx adaptor.DBTx, params userFindParams) (*entity.User, error) }
これは取得系に関してFindメソッドのみでシンプルなインターフェイスにすることができます。 そして、初見の開発者がこのrepositoryがどういう振る舞いをするのかをすぐに把握できます。
このようにいくつかの案をサーバーチームのメンバーで意見を出し合い、それぞれどのようなメリット、デメリットがあるかということを議論しました。 この議論については前者の案が採用されました。 理由としては、
- Yappliのシステム上では多くのUseCaseが存在し、それに合わせてrepositoryのメンテナンスも頻繁に行われるため
- メソッド数を減らしても結局parameterのパターンは同じ数だけ存在し、認知負荷を減らせるというメリットがそれほど大きくなかったため
等がありました。
repositoryの更新系メソッドの名前について
更新系についても似たようなことがいえます。 本によくSaveというメソッドが出てきますが、これをそのまま使おうとすると
type UserRepository interface { Save(ctx context.Context, tx adaptor.DBTx, *entity.User) (*entity.User, error) }
のようなインターフェイスになります。 これはDDDの永続化という概念をよく表していて、生成したentityのインスタンスをそのまま保存するという振る舞いがよくわかります。
一方で、
type UserRepository interface { Create(ctx context.Context, tx adaptor.DBTx, *entity.User) (*entity.User, error) Update(ctx context.Context, tx adaptor.DBTx, *entity.User) (*entity.User, error) }
というインターフェイスも議論の対象となりました。 これについては、取得系と同じ議論ができ最終的に
- Create, Update, Deleteというメソッドだけ使うことを原則にする
という結論を出すことができました。
repositoryのインターフェイスと実装について
DDDにおいて、依存方向はインフラ層→ドメイン層です。 これはドメインに全てを依存させることで、ビジネスロジックをまとめつつ設計の中心にすることを実現できます。 まさにドメイン駆動です。
これにより、ドメイン層のテストは柔軟に書けるというメリットも生じます。 インフラに関わるmoduleは差し替え可能になるという実益があることは本にもよく書いてあります。
一方で、Yappliのシステムの一部では歴史的経緯があり、この依存関係を守れていない部分がありました。 例えば以下のような実装です。(実際のコードは出せないので、雰囲気を読み取っていただければと思います)。
// domain/repository/user_repository.go package repository type UserRepository interface { FindByID(ctx context.Context, tx adaptor.DBTx, id int) (*entity.User, error) } type userRepository {} func (r *userRepository) FindByID(ctx context.Context, id int) (*entity.User, error) { db, err := r.dbAdaptor(ctx) if err != nil { return nil, err } res, err := mysql.NserUserMySQL(db).FindByID(ctx, id) // infra層の関数呼び出しを直接行っている if err != nil { return nil, err } return res, nil }
このようにインフラ層にドメインが依存している状態になっていると先ほど述べたメリットが享受できません。 特にテストを書くときにDBの差し替えが困難になり結局MySQLを自前で用意して接続するコードを書かざるを得なくなります。
理想として、
// domain/repository/user_repository.go package repository type UserRepository interface { FindByID(ctx context.Context, tx adaptor.DBTx, id int) (*entity.User, error) }
// infrastructure/mysql/user_repository_impl.go package mysql type userRepository {} func NewUserRepository() repository.UserRepository { return &userRepository{} } func (r *userRepository) FindByID(ctx context.Context, id int) (*entity.User, error) { // db接続など }
のようにドメイン層にはインターフェイスのみを置き、 それを満たす実装はインフラ層で用意すればrepositoryの実装自体は いくらでも差し替えることができます。
この辺りは、エンジニア間でも比較的すぐ方向を合わせられた事例でした。
合意した
紹介したもの以外にも多くの論点で設計について議論しました。 そして、議論する上で結論が出たものと、白熱しすぎて時間内に結論が出しきれなかったものがありました。 結論が出たものについてはその場で合意しました。 また、決着がつかないものについても細分化し部分的に規約に取り入れることもしました。
所感
コーディング規約を作ってよかったと感じたこと
もちろんより良いシステム設計ができる点が挙げられます。 また、それ以外に副次的な恩恵もあるように感じました。
具体的には
- 議論することで話が広がり新たな課題を抽出できる。
- お互いの考えがわかるのでチーム感が出る。
等は元々期待していた効果ではなかったですが実感できました。
コーディング規約を作った上での課題感
コーディング規約は作ることができましたが、その運用は難しい点であると感じます。 今回は新規コードに絞ったため、古いコードについてはリファクタしきれないうちは付き合っていくことになります。 また、ドメイン層がインフラ層に依存していることを防ぐようなlintルールなどは作れそうですが、 この辺りのチェックの自動化もしていく必要が出てきます。 その他、コーディング規約が浸透するまでの間、コードレビューがレビュアーによって大きくブレるという懸念もあります。 これらの課題については今後も継続的に改善していく方針です。
まとめ
今回は、戦術的DDDの側面でコーディング規約を作るという取り組みを紹介しました。 設計を改善する以外にもエンジニアのメンバーと議論できたことでチーム感も出て楽しかったです。 作成したコーディング規約にもまだまだ課題はいくつかあるので、実際に運用する上で改善を続けていこうと思います。
ヤプリではシステムの設計について考える場面はたくさんあり、その領域に興味をもつエンジニアのメンバーも多くいます。 一方で課題も複数あり、日々チームで取り組んでいます。
設計や、どうプロダクトを洗練していくのかということに興味のある方は是非カジュアル面談にお越しください。