やさしいクリーンアーキテクチャ
SREホールディングス株式会社の松本です。
プロダクトはリリースしてからが始まりで開発し続けることが当たり前の時代、ソフトウェアは変更や拡張に強く設計しなければなりません。クリーンアーキテクチャはそんな設計を実現する方法の1つですが、名前は聞いたことはあるけど実践したことはない、なんだか複雑で難しそう、という印象を持っている人が多いのではないでしょうか。
クリーンアーキテクチャを詳細に説明している記事は数多くありますので、本記事ではクリーンアーキテクチャを触ったことがない方に良さが伝わるように、やさしく噛み砕いて説明してみようと思います。
対象読者
- クリーンアーキテクチャをこれから学びたい方
クリーンアーキテクチャとは
機能を実現しているコアな部分をフレームワークやDBなどに依存しない状態(関心事の分離)にすることで、他が変わってもコアな部分への影響をなくし、変更や拡張に強くすることができるアーキテクチャです。
変更や拡張に強く設計できると、開発する際に嬉しいポイントがいくつもあります。
- 既存機能の改修
- どこに手を入れればいいか特定しやすい
- 他の機能への影響を減らせる
- 新規機能の開発
- 既存の処理を利用して開発しやすい
- 既存機能への影響を減らせる
- テストコードを書きやすい
- バグ修正
- どこが原因か特定しやすい
- どう修正すればいいか特定しやすい
似たような思想のアーキテクチャとしてはヘキサゴナルアーキテクチャ等いくつかあり、クリーンアーキテクチャはRobert C. Martin氏がSOLID原則をはじめとした設計原則をヘキサゴナルアーキテクチャ等に当てはめた、より詳細な実装パターンとも言えます。
同心円の図
引用元:The Clean Architecture
クラス構成の図
引用元:Clean Architecture 達人に学ぶソフトウェアの構造と設計
この2つの図は、Robert C. Martin氏が著書の中でクリーンアーキテクチャを説明する際に用いている図です。しかし、前提知識なくこの図を見ただけでは、すぐに理解することは難しいと思います。今回は、クラス構成の図を用いながら噛み砕いて説明してみようと思います。
クラス構成を読み解く
置き換える
クラス構成の図を見ると、登場人物が三層アーキテクチャやレイヤードアーキテクチャとは異なる部分が多く、分かりづらさを増しているのではないでしょうか。まずは、同心円の図とマッピングしつつ、登場人物をよくある名前に置き換えてみましょう。
一気に理解しやすくなったのではないでしょうか。変更点は以下の表の通りです。
変更前クラス | 変更後クラス | 説明 |
---|---|---|
Controller Presenter |
Controller | APIのコントローラー |
View Model | Response | APIのレスポンス |
Input Boundary Output Boundary |
Application Service | ユースケースのインタフェース |
Use Case Interactor | Application Service Impl | ユースケースの実装クラス |
Input Data | Function Parameters | ユースケースの関数の引数 |
Output Data | Return Values | ユースケースの関数の戻り値 |
Entities | Domain Service | ビジネスロジックの実装クラス |
Data Access Interface | Repository | リポジトリのインタフェース |
Data Access | Repository Impl | リポジトリの実装クラス |
Controller
とPresenter
はWebフレームワークを使って開発すると、通常は1つのController
になると思います。それに伴いBoundary
、つまりContollers
とUse Cases
の境界もApplication Service
のインタフェース1つになります。
境界を超える際は、同心円の図で表されているように外側から内側への依存になるので、Application Service
のインタフェースとRepository
のインタフェースはUse Cases
の内側になります。
簡略化する
マッピングと登場人物の置き換えで分かりやすくはなりましたが、まだ複雑だと思います。次に、簡略化できる部分がないか探ります。
クリーンアーキテクチャにおいて重要なのは、機能を実現しているコアな部分、つまり赤枠を外側から守ることです。
一方、青枠の部分はどうでしょう。Function Parameters
は数値や文字列の変数1つ2つであればクラス化するほどではなく、直接関数の引数にしてしまってもいいかもしれません。Return Velues
も同様です。インタフェースは同心円の図の内側から外側に向かって境界を超える際に必要ですが、外側から内側に向かって境界を超える分には依存関係として問題ありません。Application Service
が内側のクラスだけで作られていれば、インタフェースは無くても赤枠を守ることには支障はなさそうです。
青枠の部分を除くと、登場人物は三層アーキテクチャやレイヤードアーキテクチャにかなり近くなりました。
では何が異なるかと言うと、コアな部分であるApplication Service
をRepository
の実装であるRepository Impl
に依存させない、という所です。制御はUse Cases
→Gateways
ですが、依存はGateways
→Use Cases
にすること、これが緑枠で実現しているSOLID原則のD(依存性逆転の原則)になります。
実用化する
簡略化したことでクラス構成がシンプルになり、身近に感じられるようになったのではないでしょうか。しかし、実際に依存性を逆転させてコアな部分を外側から守るには少なくとも2つ、必要に応じて3つほど登場人物の追加が必要です。最後に、実用可能なクラス構成に修正します。
また少しだけ複雑になりましたが、一度簡略化しているので理解しやすいと思います。
追加点は以下の表の通りです。
クラス | 必須 | 説明 |
---|---|---|
Domain Object | ◯ | ビジネスロジックの実現に必要なオブジェクト |
Data Access Object | ◯ | DBを参照・更新するためのオブジェクト |
Use Case Object | ユースケースの実現に必要なオブジェクト |
DriverやO/Rマッパーを用いてDBアクセスを実装すると思いますが、Application Service
をRepository Impl
に依存しないようにするには、Repository
のインタフェースにはDriverやO/Rマッパーに関するものは現れてはいけないことなります。
したがって、DriverやO/Rマッパーに関するものはRepository Impl
とData Access Object
だけで扱い、Domain Object
もしくはUse Case Object
に変換してから返す必要がある、ということになります。前述の通り、ここが三層アーキテクチャやレイヤードアーキテクチャとの最大の違いであり、慣れないうちは依存性を逆転させずに実装をしてしまいがちなポイントだと思います。
簡易構成で形作る
次は、JavaとSpringを用いて、商品の税込価格を取得するAPIを簡易構成でサンプルコード化してみます。
パッケージとクラスの構成は以下の通りです。
jp.co.sre
└ sample
├ core
│ ├ domain
│ │ └ TaxDomainService
│ ├ entity
│ │ └ Item
│ ├ repository
│ │ └ ItemRepository
│ └ service
│ └ ItemAppService
├ infra
│ └ repository
│ ├ JdbcItem
│ └ JdbcItemRepository
└ web
└ controller
├ ItemController
└ ItemResponse
なるべく三層アーキテクチャやレイヤードアーキテクチャの構成から大きく変えることなくクリーンアーキテクチャを形作ってみました。
機能を実現しているコアな部分、Use Cases
とEntities
のクラスをcore
以下に、フレームワークに依存するControllers
のクラスをweb
以下に、DBに依存するGateways
のクラスをinfra
以下に置いています。
ここから、クラスの役割をそれぞれ説明していきます。
@RestController
public class ItemController {
private final ItemAppService itemAppService;
public ItemController(ItemAppService itemAppService) {
this.itemAppService = itemAppService;
}
@GetMapping("/item/{itemId}")
public ItemResponse action(@PathVariable long itemId) {
long price = this.itemAppService.getPrice(itemId);
return new ItemResponse(itemId, price);
}
}
ItemController
は、リクエストを受け取り、ユースケースを実行し、レスポンスを返す役割です。ここでは行っていませんが、リクエストの検証を行ったり、レスポンスの分岐も行います。
public record ItemResponse(
@JsonProperty("item_id")
long id,
@JsonProperty("price")
long price) {
}
ItemResponse
は、フレームワークやAPI仕様に合わせて作るクラスです。ここでは、例としてクラスの変数名とJSONの変数名を変えています。
@Service
public class ItemAppService {
private final TaxDomainService taxDomainService;
private final ItemRepository itemRepository;
public ItemAppService(
TaxDomainService taxDomainService,
ItemRepository itemRepository) {
this.taxDomainService = taxDomainService;
this.itemRepository = itemRepository;
}
public long handle(long itemId) {
Item item = this.itemRepository.findById(itemId);
return taxDomainService.calc(item.price());
}
}
ItemAppService
は、ユースケースを実現する役割です。リポジトリから必要な情報を集め、ビジネスロジックを呼び出し、必要に応じて値を返します。引数や集めた情報を元に分岐も行います。ここでは、フレームワークやDBに依存したクラスを利用しません。
@Service
public class TaxDomainService {
public long calc(long price) {
return Math.round(price * 1.1d);
}
}
TaxDomainService
は、ビジネスロジックを実行し、必要に応じて値を返す役割です。ここでは、例として税込金額を計算しています。計算をItemAppService
から切り出すことで、税率を変えてもItemAppService
のコードには影響がありません。
public interface ItemRepository {
Item findById(long id);
}
ItemRepository
は、ユースケースがリポジトリにアクセスするためのインタフェースです。依存性を逆転させて、コアを守るために重要な要素その1です。
public record Item(long id, long price) {
}
Item
は、リポジトリからユースケースに値を返すために利用するドメインモデルです。ここでは、フレームワークやDBに依存したクラスを利用しません。依存性を逆転させて、コアを守るために重要な要素その2です。
@Repository
public class JdbcItemRepository implements ItemRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcItemRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Item findById(long id) {
JdbcItem jdbcItem = this.jdbcTemplate.queryForObject(
"SELECT * FROM item WHERE id = ?",
new DataClassRowMapper<>(JdbcItem.class), id);
return new Item(jdbcItem.id(), jdbcItem.price());
}
}
JdbcItemRepository
は、DBからデータを取得し、ドメインモデルに変換して返す役割です。ドメインモデルに変換してから返すことで、テーブルやクエリの調整、データベースの変更が行なわれても、返すデータの種類に変更がなければItemAppService
やItem
のコードには影響がありません。依存性を逆転させて、コアを守るために重要な要素その3です。
public record JdbcItem(
@Id
long id,
@Column("price")
long price,
@Column("created_at")
LocalDateTime createdAt) {
}
JdbcItem
は、テーブル仕様に合わせて作るクラスです。ここでは、例としてクラスの変数名とテーブルのカラム名をマッピングしています。
まとめ
クリーンアーキテクチャのクラス構成を、3つのステップで読み解き、サンプルコード化して解説してみました。クリーンアーキテクチャに入門する敷居が少しは下がったのではないでしょうか?
簡易的なクラス構成では、SOLID原則のD(依存性逆転の原則)が守れる状態になります。まずは簡易的なクラス構成で実践しながら、次のステップとしてS(単一責任の原則)やO(オープン・クローズドの原則)を意識したり、CQRSなどの他のアーキテクチャも組み込んで行けると、より良い設計に進化していけると思います。
Discussion