「奥野さんと社員のリファクタリング部屋」は、リファクタリングに励むトレタの社員と技術顧問の奥野さん ( @okunokentaro ) の間で実際に行われた会話を切り取った開発現場実録コンテンツです。
技術顧問: 奥野さん 三度の飯よりリファクタリングが好き
今回の質問者: 武市さん トレタ在籍2年。沖縄在住のフロントエンジニア
今回の質問
前回に引き続き、Webアプリケーション (Next.js) のプロダクトのリファクタリングを進めている武市さんから、ディレクトリ構造のリファクタリングについての質問です。
前回の指摘も踏まえて、新しいディレクトリ構造とその定義を考えてきました。 ┗ server ┃ ┣ boundary -- 外部システムとのやり取りを行うためのエンドポイント。 ┃ ┃ ┗ mo ┃ ┃ ┃ ┗ moClient.ts ┃ ┣ handlers -- リクエストやエラーのハンドリングを行う。具体的には、HTTPリクエストを受け取り、適切なユースケースを呼び出して処理結果を返す。 ┃ ┃ ┗ item ┃ ┃ ┃ ┣ delete ┃ ┃ ┃ ┃ ┗ deleteHandler.ts ┃ ┃ ┃ ┣ get ┃ ┃ ┃ ┃ ┗ getHandler.ts ┃ ┃ ┃ ┗ put ┃ ┃ ┃ ┃ ┗ putHandler.ts ┃ ┣ models -- BFF固有のオブジェクトやエラーを定義する。 ┃ ┃ ┗ conflictError.ts ┃ ┣ repositories -- データの永続化層とやり取りを行う。データベースや外部APIとの通信を担当。 ┃ ┃ ┣ daleteItem ┃ ┃ ┃ ┣ adaptResult ┃ ┃ ┃ ┗ delete.ts ┃ ┃ ┣ getItem ┃ ┃ ┃ ┗ get.ts ┃ ┃ ┗ updateItem ┃ ┃ ┃ ┣ adaptResult ┃ ┃ ┃ ┗ update.ts ┃ ┗ useCases -- ドメインロジックを含む。ビジネスルールを実装し、リポジトリと連携してデータの取得、更新、削除を行う。 ┃ ┃ ┗ item ┃ ┃ ┃ ┣ deletItem.ts ┃ ┃ ┃ ┣ getItem.ts ┃ ┃ ┃ ┗ updateItem.ts
ディレクトリを分ける目的を考える
奥野
まず、「ディレクトリを細かく作りたいのはなぜか」という本質に立ち替えってみましょう。例えば、src
っていうディレクトリに全てのファイルが全て兄弟で並んで、何百個というファイルがフラットに並んでいても別に構わないし、そういった構造をとることが可能か不可能かでいったら、可能ですよね。
でも、なぜそうはしたくないのか、なぜファイルを分けたくなるのか、というところを考えてみましょう。それもファイル名のアルファベット順で分けることだってできるのに、なぜboundary
に分けてhandlers
で分けるのか、というところですね。
こういった分け方をするのであれば、ディレクトリを分けるルールを決めるだけではなく、そのルールに従い続けられる環境をどうやって構築して、その環境を維持するかも一緒に議論しないといけません。
つまり、ディレクトリを分けたら終わりではなくて、今後作るファイルをディレクトリ分けのルールに従わせ続けることが目的ですよね。ディレクトリの分け方を決めて満足してはダメで、1年後、2年後まで維持し続けられるのか、どうやってルールの適用を自動化するかを考えましょう。そう考えた時に、このディレクトリのルールをどうやって守り続けますか?
武市 これは奥野さんのプロジェクトを参考にさせていただいてますが、参照可能なディレクトリの範囲を決めて、それ以外を参照してた場合にはCIの時点で落とすっという処理が、僕も適切だと思っています。(編注:eslintなどのルールを組み合わせることを指しています。)
今回で言うと、例えばここではserver
の配下にmodels
というものがありますが、このserver
配下のmodels
内のファイルはserver
より上のディレクトリのファイルは参照してはいけない、というルールは簡単に導入できるなと考えていました。
奥野 そうですね。 その場合、依存のルールを厳しめに作り、依存の方向に制約をかけることでなにが嬉しいかというと、例えばテストを作る時の負担が減りますね。「このレイヤーにテストを書きたい」と考えた時に、「どのレイヤーはモックにしよう」という方針をたてやすくなる。
守られていなかったら、ファイルごとにものすごく激しい密結合を生んでいるファイルもあれば、ちゃんと疎結合になってるファイルもあればで、ファイルの実装を一個一個読んでいかないとテストの方針が立てられない。依存のルールが決まっていることによって、このディレクトリに入っているファイルだったら依存のルールが統一されているお陰で、一つのテスト方針でディレクトリ内全てのファイルに対してテストが同じように繰り返し作成できる、という流れに繋げられる。
モックをどういう粒度で作っていくか考える時に、依存の粒度が揃っているのはすごくテストを書く上で扱いやすくなりますね。
それって本当にリポジトリ?
奥野 こういうディレクトリに分けをするときは、「分けて満足してはダメで、分けた後にそれを維持し続ける仕組みをどうやってメンテナンスするか」がとても重要になってくる。
そう考えると、今回の分け方で妥当なのかっていうと、boundary
以外のhandlers
models
repositories
useCases
はちょっと怪しいなって思っています。特にrepositories
で分ける必要に本当にあるのか、という部分。
useCases
とboundary
が分かれていればそれで十分なのではないかという考え方も自分は持っていて。repositories
で分けたい理由が明確になってないうちにrepositories
というディレクトリを置くルールに決定してしまうと、そこを中心にテストでモックを準備したり、なにかのファイルを作成するときに要らぬ手数が増えたりすることになるのではないかという懸念があります。
useCases
とboundary
で十分なところに、わざわざリポジトリを設けることで迂回して遠回りすることになりかねないから、そのrepositories
で分けたい理由はちゃんと聞いておきたいです。
武市
はい、useCases
はバックエンドから返ってくるものを扱うので、モックするのが正直面倒だと思っています。useCases
はビジネスロジックを中心に扱いたいので、バックエンドからどんな値が返ってくるかまでをモックしたくないな、と考えました。
奥野
なるほど。でもuseCases
のテストでバックエンドをモックをしたいなら、この場合boundary
をモックすれば十分とも見られます。なのでuseCases
とboundary
の間にrepositories
を置きたいという価値はちゃんと議論しましょう。
武市
useCases
でやることの中には、repositories
のgetItem
とかdeleteItem
を両方呼ぶ可能性もあります。
例えば、updateItem
のユースケースの場合、名前がすでに使われていないかデータベースに一回取得しにいくので、updateItem
のユースケースの中ではリポジトリのgetItem
も呼ぶ必要があります。
そのような場合にリポジトリを分けておけば、汎用性高く使いまわすことができるというのがuseCases
とrepositories
を分けるメリットかなと思ってます。
奥野
それはリポジトリという名前で呼んではいるけど、Repository Patternの踏襲にはなっていなくて、あくまでもuseCases
の中の共有処理に過ぎないのでは、と感じました。
全体のユースケース = シナリオのように扱える部分があって、粒度の細かいユースケースを組み合わせて一つの大きなシナリオを組み立てているという感じですね。なので今のディレクトリ構成案では、repositories
が最小のユースケースであり、useCases
と今呼んでいるものはもっと大きいシナリオのような粒度ではないかと思いました。
ということは「ユースケース」という言葉に対する認識が自分と武市さんの間で揃っていないし、「ユースケース」という言葉の粒度を定義しないとチームの他のメンバーにも伝わりにくくなってしまいます。
意味が伝わりやすい名前にする
奥野 たとえば、Clean Architecture *1の本に書いてあったとか、達人プログラマー *2に書いてあったとかであれば、「あの本でいうユースケースです」みたいに説明すればいいと思うんですが、色々なところからつまみぐいを繰り返している結果、全体的によくわからないアーキテクチャになってしまっている気配があります。
リポジトリというものはDDD(ドメイン駆動設計)*3の言葉ですよね?Clean ArchitectureとDDDを両立することもできるけど、分かって組み合わせないと、かいつまんだだけの全体的に何がしたいか伝わりづらいアーキテクチャになってしまう。今回の場合、このアプリケーションではDDDを実践していないので、そこで無理にrepositories
という言葉を使う必要もないと思っています。
あくまでも複数のユースケースを連ねるものだから、ユーススケースの粒度を細かくしたものにすぎなくて、ドメイン駆動設計としてのリポジトリではない。言葉だけ拝借してるけどルールがちゃんと定まってないから、「ユースケース」や「リポジトリ」という言葉では武市さんの説明を聞いても自分はあんまりピンときてなかったんですよ。
これは、武市さんのやりたいこと、やろうとしてることを否定したいのではなく、やろうとしてることはわかるんだけど、語彙からそれが伝わってこない、っていうところがポイントですね。
例えば、さっきのupdateItem
というユースケースを例にすると、getItem
で商品情報を取得し、取得した後にupdateItem
を行う…というようにデータベースの操作と密にもなりつつ、ドメインロジック・ビジネスロジックとしてもこう取り扱っておきたい、みたいな粒度がありますよね。
そんな複数の処理のコンビネーションをなんと呼ぶべきか、そしてその一個一個をなんと呼ぶべきかというところを整理していくと、もうちょっと納得感のある切り方になると思います。自分は、ドメイン駆動設計を実践しておらずモノリシックな処理をとにかく細分化したいという今の段階において、ここに凝った名前をつける意味はあんまりないと思っていて、自分が担当しているプロダクトではhandlers-shared
と名付けています。そこではHTTPハンドラがいっぱいあって、そのハンドラが一つ一つ細かい操作を決めるんだけど、複数のハンドラで共有したい概念があったらそれはhandlers-shared
に置いてます。
自分の場合はhandlers
の中で共有したいものなのでhandlers-shared
に置くけど、武市さんの例の場合だったらuseCases/shared
とかにするとよいかと思います。useCases
の粒度であることに変わりはないけど、useCases
の中で単独で使われるか、共有で使われるかというところの違いでしかありません。
useCases
とuseCases/shared
で分けるのがしっくりこなければ、極端な話、全部useCases
に突っ込んでuseCases
とboundary
だけでいいんじゃないかというのが自分の最初の主張なんですよ。
最初にboundary
以外全部しっくりこないって言い方をしましたが、boundary
とuseCases
とmodels
の3つでも十分回ると思ってます。大切なのは名前が何かではなく、ファイル間の依存関係がスパゲティのように絡まないかということです。
ルールを納得させる
武市 確かにそうですね。自分の考えとしては、ユースケースはデータベースのことを知らないようにしたくて、ディレクトリを分けた理由としてリポジトリはDBのことを隠す役割ために分けたいからと考えていました。
奥野 どのシステムに繋ぐかを隠してその操作の抽象を取り扱ってるという考え方は合っていると思うけど、武市さんの感覚でリポジトリと名付けて「でも世でいうリポジトリとは異なります」だと、今後チームに参入する人に納得してもらうには武市さんの話術にかかってくると思うんですよ。世でいうリポジトリとは異なるのはなぜなのか?を毎回説明することになる。それだったら説明しづらい言葉を入れるよりも、もうちょっとフラットに通じる名前にしたらいいんじゃないかっていうのが自分の考え方ですね。
ちなみに自分のとこではside-effect(副作用)って呼んでます。
外部に対して何らかの変更をもたらす、追加したり消したりとか、そうゆうハンドラ関数における副作用ですね。なのでそのハンドラが本来何をしたいのかっというside-effectというものがいくつも並ぶことによって何らかの目的を達成させます。武市さんの言葉を借りるとユースケースですね。
自分の前に携わっていた案件だと一つのハンドラー内はincoming
・side-effect
・outgoing
の3つに分けていました。
┗ handlers ┃ ┣ users ┃ ┃ ┣ get ┃ ┃ ┃ ┣ incoming ┃ ┃ ┃ ┣ side-effect ┃ ┃ ┃ ┗ outgoing
全てのハンドラに対して一様に必ずこの3つに分けていて、リクエストっていうのは必ずバリデーションをしないといけないし、認証認可の文脈でも何らかの処理が必ず発生する。エンドポイントというのは入ってきたものを検閲するポイントが必ず必要になってくる、それがincoming
になってる。
incoming
で合格した値っていうのは、そこから何か処理を呼んだり、書き込んだ消したり、必ず何らかの副作用を起こします。他のシステムに通信を飛ばしたり、そういうハンドラの副作用となる部分を全部side-effect
に入れてます。
さらにそのside-effect
が終わった後の結果っていうのは何らか返したいっていう欲求が絶対ある。それはHTTPのハンドラを実装してる以上、何かしらのHTTPレスポンスを返したいっていうのは当然の振る舞いなので、そこをoutgoing
ディレクトリ内の処理で、レスポンスのバリデーションをしたりとかレスポンスのBodyの構築をしたりとかをする。だから、「入口」・「やること」・「出口」で分けている。
武市 なるほど。
奥野
自分が前に携わっていた案件の言葉を使って、武市さんのこのディレクトを説明すると、今はuseCases
にincoming
・side-effect
・outgoing
が全て詰まっていますね。そのside-effect
の中でもうちょっと分けておきたいなっていう単独化というか抽象化を、武市さんの言葉でいうリポジトリでやってたわけですよ。
このディレクトリに名付ける名前は正直なんでもよくて、みんなが納得してみんなで守れたらそれでいい。武市さんが「リポジトリ」って言葉を使うことを自分は何も禁止しないし、まったく否定する権利もないけど、ちょっとだけ忠告すると「リポジトリ」って言葉にはすでに色が付きすぎているから、「リポジトリ」じゃないものに「リポジトリ」って付けたときに他人に抱かせる抵抗感はそれだけ大きくなるよ、っていうものです。
武市 そうですね、本来の目的からけっこう外れちゃってますね。
奥野 「リポジトリ」と名付けたらRepository Patternを想起させるものでないと、人によっては「リポジトリちゃうやんけ」となる。
これはちょうど「リファクタリングとともに生きるラジオ」のデザインパターンの回 *4でも言ったんだけど、Strategy PatternとかSingleton Patternと、名前を付けるだけですぐ何をしたいのか世界中の人が共通で分かるっていうところがメリットで、Repositoryもそれなりに市民権を得ている。だからそういう言葉ほどそれに忠実であった方が求められる。
バウンダリ(外部システムとの連携部分)を隠蔽したいってのは分かるけど、それはバウンダリを隠蔽してるだけでリポジトリを模してるとは思えなかった。そうでないものに「リポジトリ」って名付けたときに他者を納得させるにはどうすればいいか、ってところで頭を抱えることになると思います。だから自分のとこではRepository Patternを踏襲してはいないため、リポジトリって言葉を意図的に避けていて、あえて使ってないんです。
でも結局やりたいことは、武市さんのここに書いてることと自分のとこも一緒です。それは外部と内部の抽象化をしたいのと、全体的な一連の結合テストにその個別のブラックボックスを取り扱いたくないっていう部分。だからやりたいことは一緒で、あとはそれをどう見せるかと、どうチームを納得させるか。
リードエンジニアに求められるポジションというか素養って、ルールを作る力じゃなくて、作ったルールを納得させる力なんですよ。ルールって作っても従ってもらえなければ破綻するので、ルールを作るだけじゃなくて、これに従ってねっと言って、「OK、従います」って周りのチームメイトに納得させる力なんです。そうなった時にリポジトリっていう言葉を使ってまで危険な橋を渡るのかっていうことです。
武市 めちゃめちゃ納得です!!
奥野 だからこのディレクトリツリーと、そこからくるやりたいことの説明はなにひとつ間違ってないんだけど、勇気がいる行いだと思いました。そこをもうちょっと冷静に考えてみたときに、リポジトリじゃないものをリポジトリと呼ぶのは説得力に欠けると立ち返るのであれば、他の言葉を選んだ方が無難ですね。
武市 そうですね。これから入ってくる人がわかりやすくなるようにディレクトリで役割を持たせよう、というのを目的にしていたのに裏面に出ていました。ありがとうございます!
To Be Continued…
PR
奥野さんがパーソナリティを務める「リファクタリングとともに生きるラジオ」も配信中です。リファクタリングに興味のある方はぜひチェックしてください!