前文
フリーランスでエンジニアをしています。(何でもやるけど主にbackend)
比較的短期でいろいろな会社さんのお手伝いをすることが多いのですが、複数の会社で共通的に課題となっている部分について私見をまとめておきます。
※ ちなみに一年位前にもRailsで同じような記事を書いています。今回はその発展版です。
Layered Architectureとは
近年、Clean Architectureの流行で、設計にLayered Architectureを取り入れるプロジェクトが多くなってきています。
自分の勝ちパターンも下敷きにしているのはClean Architecuteです。
Layered Architecture(= 階層化構造)とは、簡単に言えば処理を役割ごとに分解して役割ごとに階層を分離するプログラム設計のことです。
DDDやClean Architectureの登場によって大きく注目されるようになった気がしていますが、この考え方自体ははるか昔からあります。
例えばMVCとかもLayered Architectureの一種です。
backendだけでなくfrontendやインフラの領域でも階層化の考え方は取り入れられており、古くはOSIの7階層モデルなんてものもあります。
backendにおけるLayered Architecture
近年の自分の勝ちパターンとしてはLayerを以下のように分けています。
- controller
- auth
- validation
- presenter
- usecase
- repository
- service
- entitiy
前述の通りClean Architectureを下敷きにしていますが、必ずしも教科書どおりではありません。
Layered Architectureを考える上で重要なこと(この記事の主題)
それぞれのLayerの解説をしていくその前に何故Layered Architecuteを使うのかを考えてみます。
(サブタイトルにも書きましたがこれがこの記事における主題で、これを書くために残りの文章をオマケ的に書いていると言っても過言ではありません。)
Clean Architectureなどの概念を学ぶ際には、多くの人はまずはシステムを教科書どおりに組んでみようと試みます。これは有効なやり方で、実際これだけでもかなりの手応えを感じることができます。
しかし、実装を進めていくとほとんどの場合「このケースでは、どうすれば良いんだろう?」という疑問にぶち当たることになります。
実際のシステムは多種多様で教科書に全ての答えが書いてあるわけではないからです。
その場合、自力でどういう実装にするのがベターかを考えていくことになりますが、それを考える上で何が重要か?という点を考察するのがここでのお話です。
教科書的にはRepositoryを分離する理由として
- データストアとシステムを分離する
- テスト時にRepositoryを差し替えられるようにする
みたいなことが書いてあります。
ですが、個人的にはこれらはそれほど重要ではありません。
理由としては
- 実際のシステムでデータストアのみの差し替えが発生することはまずない
- DB差し替えという判断になる場合はシステム的に他の問題を抱えていることが多いので0ベース再設計となる可能性が高い
- テスト時にはReository差し替えよりもテスト用DBのデータの洗い替え手法を好む
- これは自分の知見的にRepository差し替えによるテストを上手く書けたことがないという個人的な理由もあります。
- ただし、CloudStorageを使う場合などRepository差し替えが必要なケースは存在する。
と考えるからです。
より正確に言えば、これらは重要ではあるのですが「この場合はどうしよう?」という判断をする場合に最重視するポイントではない。と言った方が良いかもしれません。
経験的には教科書偏重してこれらを必要以上に重視してしまった結果(かどうかは正確にはわかりませんが)、責任分界点があいまいになり何をどこでやるかがゴチャゴチャになってしまっているシステムを複数見たことがあります。
多分この状態でも中でずっと実装に関わっていた人が困ることはあまりないです。
そういう作りだと、慣れてしまえばそこまで気になることはないのでしょう。(あるいは気にはなっていても、優先順位的にそこの改善に手を付けることができないというのもあるかもしれません)
ですが、エンジニアの入れ替わりが激しい昨今においてはこれは新しく入る人の参入障壁となりうるのではないかというのが最近の所感です。
閑話休題。話を戻してここで自分が何が最重要と考えているかというと。。。
- 実装時に同時に考えなければならない事柄を減らすこと
この一点につきます。
別の言い方をすれば「Aについて考えている時にはBのことは考えなくて良い」という状態を意図して作り出すことが重要です。
優れたエンジニアはシステム領域の多くの事柄に知見があるものですが、それでも多くのことを同時に考えなければ行けない状況では何かを見落としたり、バグを仕込んでしまったりするものです。これは人間である限りもうどうしようもない。(稀にマシンみたいな人もいますが、そこを目指すのも、そういう人を採用しようとするのもまず徒労に終わります。)
なので、システムが成長して全体としては複雑化していくとしても、ピンポイントにある一部に注目した時に同時に考えなければいけないことは多くはない、という状態を意識的に維持していくことはとても重要です。
そうした状態を作るために有効な手段の一つとしてLayered Architectureがあるわけです。
決してLayerをきれいに分割する事自体が目的ではありません。
これを踏まえて以下に各Layerでの役割を解説します。
controller
controllerは処理の入口兼出口です。
処理の本体はusecaseで行います。
usecaseにパラメータを渡す前に必要に応じて
- 認証/認可のチェック(auth)
- 入力パラメータのチェック(validation)
を行います。
これらのチェックにパスしたらusecaseを実行します。
usecaseはビジネスドメインのentityを返すので、それをクライアントに返却する形のJSONに変換するのはpresenterの役割です。
auth
authではリクエストの認証/認可を行います。
認証/認可の説明はこの文書の範囲外ですが、大雑把に言うと
- 認証 - リクエスタが正しくシステムのユーザであることの確認
- 認可 - 実行ユーザがそのリクエストを実行する権限を持っていることの確認
です。
ここで考えなければいけないのは上記2点のみで、それ以外のことは考える必要はありません。
一般に認証/認可はcontrollerの内部で行われるのではなく、使用するWebフレームワークのmiddlewareで実装されることが多いです。
(がっつりWebフレームワークに依存した処理となるためか、Clean Architectureではあまり触れらていない部分でもあります。)
認証に失敗した場合は401エラーを、認可に失敗した場合は403エラーをすみやかに返します。
ほとんどの場合、認証/認可の過程でRepositoryからUserを取得することになるので後続の処理で再度Userを読み込むことを避けるために、ここで取得したUserは後続の処理に引き渡すようにした方が良いです。
validation
ここでのvalidationでは
- 必須パラメータの不在
- 文字列長オーバー
- 列挙値の不正
などをチェックします。
この段階でエラーが発見された場合はすみやかに400エラーを返します。
DBにアクセスしないと正しいかどうかわからないような種類のvalidationはここでは行いません。
validationで考えることは値の整合性のみで、それ以外のことは考える必要はありません。
presenter
usecaseから返されたビジネスドメインのオブジェクトをJSONに変換するのがpresenterの役割です。
開発言語にもよりますが、ほとんどの場合ビジネスドメインのオブジェクトは純粋JSONではなくクラスとして実装されます。
クラスにはメソッドや内部処理でのみ必要なフィールドが含まれているので、それらを除いたクライアントに返却するためのJSONを生成する仕組みがどこかに必要になるわけです。
クラス側にtoJson(): JSON
のようなJSON変換のためのメソッドを定義するのも割とよくあるパターンで、それで間に合う場合はpresenterは省略可能です。
ただし、省略する場合でも暗黙的にpresenterというLayerが存在することは意識しておいた方が良いです。
処理内容によってクライアントにとって都合の良いJSONの形は異なることも多いし、通信量を減らすためにその処理では不要なフィールドを削ってJSONサイズを圧縮することもあります。
それ以外にも、一般ユーザと管理者ユーザでレスポンスを出し分ける(一般ユーザ向けには一部の情報を伏せる)、みたいな処理もpresenterで行います。
いずれにせよ、こうした事柄を考えるのはpresenterを実装しているときだけです。
逆に言えばpresneterが存在することによって、usecase実装時に最終的なJSONをどういう形にするかを考える必要がなくなります。
usecase
usecaseはビジネスドメイン側の処理の本体です。
ここに記述されるロジックはビジネス側のロジックです。
基本的には1つのユースケースを1ファイルで作成します。
通常ユースケースは日本語的には「[主語(=Actor)]が[動詞]する」という形式で表現されます。
例えば掲示板的なシステムの場合は
- 一般ユーザが投稿一覧を見る
- ゲストユーザ(非ログインユーザ)が投稿一覧を見る
- 一般ユーザが投稿する
などがユースケースになります。
上の例ではわざと「投稿一覧を見る」を一般ユーザとゲストユーザでわけましたが、このようにActorが違う場合は基本的には別のユースケースと考えます。
なので、実装時にはファイルを分けて別々に実装した方が良いんですが、現実的には掲示板の「投稿一覧を見る」という処理においては一般ユーザとゲストで全く処理内容が同じだったりします。
昔はこの場合でもファイルを分けた方が良いだろうかと迷ってましたが、最近は必要に迫られるまでは同一のユースケースとして1ファイルで扱えば良いと思っています。
一般ユーザ側にTwitterのMuteとかBlockみたいな機能が追加された時などが分割を検討するタイミングでしょう。
程度によっては分岐でやっても良いと思いますが、一般ユーザの処理とゲストの処理を同時に考えるのが辛いと感じ始めたなら分割すべきです。
何度でもいいますが、同時に考えなければならない事柄を減らすことが質の高いプログラムを作るための原則です。
こうしたリファクタリングを気軽に行えるのもLayered Architectureの強みでもあります。
repository
repositoryはビジネスドメインのオブジェクトの保管庫です。
ただし、実際に保存されるのはRDBやS3などのクラウドストレージだったりするので、ここに記述されるロジックはシステム側のロジックです。
ロジックはシステム側だけど出力はビジネスドメインのオブジェクトというのがポイントです。
通常repositoryは
- list
- get
- create
- update
- delete
というごくシンプルなオブジェクトの出し入れのメソッドだけを持ちます。
repository実装時に考えることは純粋に対象となるEnittyの取得/保存だけで、権限によって取得できる範囲が異なる、みたいなフィルタリングはここでは行いません。
repositoryを実装する時にはビジネスドメインのロジックを考慮する必要はありません。
service
serviceはusecaseとrepositoryの中間的な存在です。
usecaseとrepositoryだけでもシステムは作れますが、複数のrepositoryにまたがる同じような処理を複数のusecaseで行わなければならないケースは多々あります。
この場合serviceを作成して処理をまとめたメソッドを実装します。
個人的には設計の初期からserviceとして何を作ろう?ということを考えることはほとんどなく、最初はusecaseとして実装して同じ処理を前にもやったな、と気づいた時にそれを切り出してserviceにします。
usecaseから別のusecaseを呼び出すということはしません。
何故ならそれを許すとusecaseの改修時に「このusecaseは別のusecaseから呼び出されているかもしれない」という可能性を考慮しなければならなくなるからです。(「同時に考えることを減らす」という原則に反します。)
逆に言うとservice改修時には、それがどこから呼び出されているかを精査する必要があるので、あんまり作りすぎるのも良くはないと思っています。
この辺はバランス感覚が必要なところです。
扱い的にはユースケースの共通処理なので、ここに記述されるロジックはビジネス側のロジックになります。
entitiy
entityはビジネスドメインにおけるモノや概念を表すオブジェクトです。
一般的にentityの保存先はRDBになることが多いので、なんらかのORMを使うことが多いと思いますが、ORMのモデルとentityは全くの別物であり分けて定義した方が良いです。
何故ならRDBにデータを保存する場合、一つのentityを表すデータが複数のテーブルにまたがって保存されるのが普通だからです。
例
- 請求書(請求書ヘッダ、明細行、取引先情報、担当者情報など)
- ユーザに付随する1対多の属性が多数ある場合(プロジェクト、ロールなど)
請求書がまさに典型的な例だと思いますが、通常請求書テーブルには取引先や担当者の情報はそれぞれのテーブルのidしか含まれていないので、それらをJOINしてentityを生成します。(この辺がrepositoryの役割です。)
請求書entityでは明細行も常に不可分です。
一覧画面などで明細情報が不要なケースがある場合には請求書ListItemのようなentityを別に定義します。
このように一つのテーブルから複数のentityを定義するケースも普通にあります。
(他にも例えばkind
のような種別を表すフィールドを持つテーブルに対してkind
毎にentityを定義するケースもあります。)
entityにはビジネス側のロジックを表す各種メソッドを追加して構いませんが、save
のようなシステム側のロジックは含めません。
(enitity実装時にはシステム側のロジックは考える必要がありません。)
また、entityは原則immutableなオブジェクトとします。
immutableであれば処理の途中でその中身が変化した可能性を排除できるので、これも実装中に考えなければいけないことを減らす効果があります。
(RailsのActiveRecordに代表されるORMモデルはほとんどimmutableに出来ないこともentityとORMモデルを分けた方が良い理由の一つです。)
entityの関連をどこまで取るか問題
主題からは少しそれますが、entityの関連をどこまでまとめて取得するべきか?という点についても少し書いておきます。
請求書のような明確なentityであれば、その関連情報は常に全部取得する、で問題ありません。
しかし、User entityに多数の付随物があるようなケースではそれらの情報をどこまで取得するかを迷うことがあります。
例えば、なんらかの経験値の積み上げによってバッジが付与されるようなシステムの場合、取得済みのバッジはユーザの属性情報ですがほとんどのユースケースではその情報は不要だったりするわけです。
こうした情報を毎回取得するのは無駄が多いので、自分の場合はget(list)メソッドでのoptionでどこまでの情報を含めるかを指定できるようにしています。
type UserFindOptions = {
includeBadges?: boolean, // Badge情報が必要な場合はtrueを指定する
includeXXX?: boolean,
...
}
class UserRepository {
public get(id: UserId, options: UserFindOptions) {
...
}
}
この場合、User entityの定義でも情報の未取得と空を明確に区別できるようになっている必要があります。
class User {
constructor(
public readonly id: UserId,
public readonly name: string,
public readonly badges: Badge[] | undefined // 空配列は取得バッジなし、undefinedはバッジ情報未取得
)
public hasBadge(badgeId: BadgeId): boolean {
if (!this.badges) { // 無くても次の行でNPEになるが開発者への情報量を増やすために自前のErrorを投げた方が良い
throw new Error("Badge information are not retrieved!")
}
return Boolean(badges.find(v => v.id === badgeId))
}
}
badge情報を取得していないのにそれにアクセスしようとしたらエラーになるので開発者はその時点で自分の間違いに気がつくことができます。
また、関連の先の関連は基本的には扱いません。
例えば請求書の担当者情報から、その担当者に付随する情報がさらに欲しい場合には別途担当者Repositoryから情報を取り直します。
ここで「担当者の主情報は既に取得済みだから関連情報だけを取得するメソッドをRepositoryに追加しよう」みたいなことを考えるのはあまりオススメしません。それをやり始めるとRepositoryのメソッドが増えすぎてよくわからないことになる可能性があるからです。(ただ、それが有効なケースもあるので一概に禁止とも言い切れませんが)
基本的には関連の取得は主キーでのアクセスになるので、N+1問題にさえ気をつけていればこれでも大きなパフォーマンス問題は発生しません。
まとめ
以上、Layered Architectureの近年の自分の勝ちパターンについて、それぞれのLayerで何をすべきかよりも何を考えなくて良いか(何をしてはいけないか)に重点をおいて解説してみました。
全てに同意することはできなくても少しでも拾えるものがあれば幸いです。
大事なことなのでもう一度言いますが、システム開発における最重要な基本戦略は同時に考えなければいけなことを減らすことです。
これで絶対にシステムの質と開発速度の両方が改善します。
実際のところ、Clean Architectureを採用すれば半ば強制的にこうした関心の分離が強制されるので、既に多くの人がこの効果を体感していると思います。
今回はbackend側の話に終始しましたが、frontend側でも考え方は同じでReactやVueなどのフレームワークでも根底に「同時に考えることを減らす」という思想が流れているのでそれを軸に設計を見つめ直すことはできるはずです。(自分の中で言語化できる程まとまってませんが、普段から無意識にそれを考えながらコードを書いている気はします。)
意識しているかどうかは別にして普段から無意識に「同時に考えることを減らす」を実践しているエンジニアは割とたくさんいると思っていますが、それを意識した上で改めてコードを見てみれば今まで気が付かなかったことが見えてくるかもしれません。
余談
ここではシステム開発目線での解説を行いましたが、Layered Architectureはこれからのエンジニア組織を考える上でも重要な役割を担う可能性があると思っています。
現状、社会全体でエンジニアの数が慢性的に足りておらず、どの会社もエンジニアの確保に苦労しているので、自社のシステム開発において、
- ジュニアレベルのエンジニアが活躍できる余地を残す
- 副業、またはパートタイムの業務委託エンジニアがJOIN後すぐに手を付けられるタスクを用意する
ことが必要になってくると思っています。
usecaseはシステムよりの深い知識がなくても開発可能なのでジュニアにあてがうには最適です。
また、独立性の高いusecaseは納期だけを決めてパートタイムエンジニアに任せることもできると思います。(逆にシステム領域に強いエンジニアであれば、ビジネス理解をすっ飛ばしていきなりrepositoryをやってもらうこともできるかもしれません。)
以前にもどこかで書きましたが、
- ビジネスにコミットするエンジニア
- 技術にコミットするエンジニア
は、まったく別の職種だと思っているのでそれぞれに適切な名前をつけて区別した方が良いと思っています。
そして後者に関してはもはや一つの会社に長く拘束することは無理です。(金銭面の問題もありますが、技術を極めたいと願うなら一つの会社に長くいるのは不利です。)
なので、エンジニアの回転が速いことを前提としたシステム開発組織を作ることが必要な時代が来ており、Layered Architectureではそれが実現できる可能性があると思っています。