はじめに
- クリーンアーキテクチャとはよく聞くけどいまいちわからないと思い調べてみた時のメモです
- 自分とは同じようにクリーンアーキテクチャについて聞いたこと、調べたことはあるけどいまいち理解できなかったという人の理解の助けになれば嬉しいです!
- 実装例はGo言語を使用しています
この記事から得られる内容
- アーキテクチャとは何か
- 一般的なアーキテクチャ、その課題と解決策
- クリーンアーキテクチャについて、各レイヤーの責務
アーキテクチャについて
アーキテクチャとはなにか?
- ソフトウェア開発におけるアーキテクチャとは、システムの根幹となる構造を定義する「設計図」の役割です
- アーキテクチャは、システムを異なるパーツに分け、それぞれのパーツがどのように組み合わせられるかを決めます
- これがプログラミングの基本原則の一つである「関心事の分離」で、具体的には、各機能や役割を独立したモジュールやクラスにまとめることで、コードを他の部分と明確に分けることができます
アーキテクチャの目的
- アーキテクチャの目的は、ビジネスニーズ、市場の動向、技術の変化に柔軟に対応できる「変更しやすいソフトウェア」を創ることで長期的なビジネス価値を提供することです
- 具体的には、フレームワークやデータベースなどの外部リソースを交換可能に設計し、仕様変更に強く、エンジニアが効率的に開発を進められるようにすることです
- ただ一方で、アーキテクチャにこだわりすぎず、ビジネスの目的に合わせた適切な手段を選択する柔軟性も重要です
- アーキテクチャはビジネス価値を提供するための手段であり、その重要性は状況によって変わることを認識する必要があります
一般的なアーキテクチャとその問題、解決策
レイヤードアーキテクチャ
レイヤードアーキテクチャとは主に、
- UIを担当するプレゼンテーション層
- ビジネスロジックを担当するドメイン層
- DBや外部サービスとの通信を担当するインフラストラクチャ層
で構成されるアーキテクチャのことです。
レイヤードアーキテクチャの問題点
ビジネスロジックはインフラに依存しているため以下のような問題があります。
- 依存先の変更がビジネスロジックにも影響する問題
func SaveTodoUseCase() {
// todoRepo := NewTodoMySQLRepository()
todoRepo := NewTodoPostgreSQLRepository()
todoRepo.Save()
}
上記のように使用するDBがMySQLからPostgreSQLに変更となった場合、DBアクセスしている関数全てに対して変更が必要になります。
- 依存先のコードができるまでは開発ができない問題
func SaveTodoUseCase() {
todoRepo := NewTodoRepository()
todoRepo.Save()
}
SaveTodoUseCase
はNewTodoRepository
の結果を利用しているのでNewTodoRepository
が実装でやデータベースの準備ができていないとSaveTodoUseCase
の開発を進めることができません。
このようにビジネスロジックがインフラストラクチャに依存すると開発の進行が難しくなる可能性があります。
問題点の解決 ~依存性の逆転の原則~
レイヤードアーキテクチャの問題を解消するために依存性の逆転の原則が導入されました。
依存性の逆転の原則とは依存の向きをコントロールすためのテクニックで2つの重要なガイドラインから成り立っています。
- 上位モジュールは下位モジュールに依存してはならない。両者は抽象に依存すべきである
- 抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである
ポイントとなるのは抽象に依存させるという点です。
usecase
がrepository
へ依存している場合に、
repository
のインターフェースを用意することでusecase
はrepository
のインターフェースに依存しrepository
はインターフェースに合わせて実装するようになり依存関係を逆転させることができました。
この変更によりレイヤードアーキテクチャの問題として前述した問題を解決することができます。
- 1. 依存先の変更がビジネスロジックにも影響する問題
type TodoRepository interface {
Save()
}
func NewTodoRepository() TodoRepository {
// ここで環境や設定に基づいて具体的なリポジトリを選択する
return NewTodoPostgreSQLRepository() // 例えば環境変数に基づいて切り替え可能
}
type TodoPostgreSQLRepository struct {}
func (repo *TodoPostgreSQLRepository) Save() {
// PostgreSQLに保存するロジック
}
type TodoMySQLRepository struct {}
func (repo *TodoMySQLRepository) Save() {
// MySQLに保存するロジック
}
SaveTodoUseCase
は具体的なデータベース技術の詳細から切り離され、どのデータベースを使用しているかを意識する必要がなくなります。
func SaveTodoUseCase() {
todoRepo := NewTodoRepository()
todoRepo.Save()
}
- 2. 依存先のコードができるまでは開発ができない問題
インターフェースを先に定義することで具体的な実装が完成していなくてもインターフェースに基づいてモックを使用して開発を進めることができるようになります。
type TodoRepository interface {
Save()
}
// モックの実装
type MockTodoRepository struct {}
func (repo *MockTodoRepository) Save() {
// テストや初期開発段階での仮実装
}
モックを使って、SaveTodoUseCase
をテストまたは開発することができるようになりました。
func SaveTodoUseCase(repo TodoRepository) {
repo.Save()
}
func TestSaveTodoUseCase(t *testing.T) {
mockRepo := &MockTodoRepository{}
SaveTodoUseCase(mockRepo)
// 必要なアサーションを行う
}
依存性の逆転の原則を導入したアーキテクチャ
上記の記事が非常にわかりやすかったため今回それぞれの説明は割愛します。
クリーンアーキテクチャについて
クリーンアーキテクチャとは
2012年にロバート・C・マーティン氏によって提唱されたアーキテクチャで、Clean Architectureでは関心事の分離によって以下のことを達成できると書かれていました。
-
フレームワーク非依存
- アーキテクチャは、機能満載のソフトウェアのライブラリに依存していない。これにより、システムをフレームワークの制約で縛るのではなく、フレームワークをツールとして使用できる
-
テスト可能
- ビジネスルールは、UI、データベース、ウェブサーバー、その他外部の要素がなくてもテストできる。
-
UI非依存
- UI は、システムのほかの部分を変更することなく、簡単に変更できる。たとえば、ビジネスルールを変更することなく、ウェブ UI をコンソール UI に置き換えることができる
-
データベース非依存
- Oracle や SQL Server を Mongo、BigTable、CouchDB などに置き換えることができる。ビジネスルールはデータベースに束縛されていない
-
外部エージェント非依存
- ビジネスルールは、外界のインターフェイスについて何も知らない
上記を踏まえた上でアーキテクチャの各レイヤーがどのような責務を持っているかみていきたいと思います。
各レイヤーの責務
エンティティレイヤー
- アプリケーションの中心に位置し、ビジネスの核心的なルールやロジックを含むレイヤー
- エンティティレイヤーにはドメインオブジェクトやドメインサービスが配置されメソッドを持つオブジェクトであったり、単純なデータ構造と関数で構成されることもある。また、ドメインオブジェクトの永続化を扱うインターフェースもここに含まれます
- エンティティレイヤーの特徴は、他のどのレイヤーにも依存しない独立性を持っている点で外側のレイヤーに何か変更があったとしても、エンティティレイヤーはその変更に影響されることがない
ユースケースレイヤー
- エンティティレイヤーの上に位置し、アプリケーションの具体的なビジネスロジックを担当するレイヤー
- ユースケースレイヤーではインターフェースアダプターレイヤーから受け取った入力を使い、エンティティレイヤーで定義されたビジネスルールを実行するとともに、アプリケーション特有の処理(ページネーションやソート、データ集計など)を行います
- このレイヤーはフレームワークやデータベース、UIの変更から独立しており、そうした外部要素の影響を受けない設計になっています
インターフェースアダプターレイヤー
- 「内側のビジネスロジック」と「外側の世界(フレームワークやデバイスなど)」をつなぐ役割を果たすレイヤー
- 外部から受け取ったデータを、アプリケーションが扱いやすい形(例えば、ビジネスロジックが要求するオブジェクト形式)に変換したり逆にアプリケーション内部で扱われたデータを外部向けに適切な形式に変換します(いわゆるHTTPリクエスト&レスポンスの部分)
- またユースケースレイヤーやエンティティレイヤーからのデータを、DTO(Data Transfer Object)という特定の形式を用いて外部に渡すことで、レイヤー間の依存性を管理し、アーキテクチャのルールを守ることができます
フレームワーク、ドライバーレイヤー
- 最も外側のフレームワークやツールに関するレイヤー
- 外部のリソース(データベース、Web など)との接続を担当する部分がこのレイヤーに配置されます
- ライブラリやORMなどがここに属します
まとめ
今回はアーキテクチャの発展からクリーンアーキテクチャの特性、各レイヤーの責務まで触れました。
理論的な部分で少しは理解できましたがやはり具体例がないとより深く理解することは難しいと感じたので次回はGo言語で実際に実装しながら理解を深めていこうと思います!
今回参考にさせていただいた素晴らしい書籍達
-
Go言語で構築するクリーンアーキテクチャ設計
- Go言語でクリーンアーキテクチャの解説をしている技術書
- 普段Goをよく使用するので大変参考になりました
-
Clean Architecture 達人に学ぶソフトウェアの構造と設計
- 体系的にまとまった技術書
- 教科書のような形でこれから何度も参照したいと思います