本書を手に取ってくださり、ありがとうございます!本書はGo言語を使用したクリーンアーキテクチャの設計・開発を、具体的な実践例を通して解説していく本です。
保守性や拡張性の高いアプリケーションの実現には、アーキテクチャに基づいて設計・実装を行うことが不可欠です。クリーンアーキテクチャは、このような設計の手法として知名度が高く、多くの現場で採用されています。しかし理論だけでは、具体的な実装のイメージが湧きにくいこともあります。
・レイヤーの役割は理解できても、具体的な実装方法が掴めない
・各レイヤーにおける明確な責務の分担が難しい
・そもそも自分たちのプロジェクトがクリーンアーキテクチャを実現できているのかわからない
・サンプルコードを参考にしようとしても、Go言語に特化したものは他の言語に比べると少ない
このような疑問や課題を、私たち自身も経験しながらクリーンアーキテクチャを学んできました。私たちと同じ疑問や課題を感じられている方は、少なくないのではと思っております。本書ではサンプルアプリケーションを通じて、Go言語におけるクリーンアーキテクチャの具体的な設計と実装方法を詳しく解説します。
第1章と第2章では、アーキテクチャが解決する課題を明確にして、クリーンアーキテクチャの概要と目的を説明します。
第3章と第4章では、具体的なサンプルアプリケーションをもとに、Go言語で実際にクリーンアーキテクチャに則ったアプリケーションを構築していきます。
本書で使用したサンプルコードは、GitHub上で公開されています。ぜひお手元で確認してみてください。
https://github.com/code-kakitai/code-kakitai
本書に記載された内容は、情報提供のみを目的としています。したがって、本書を用いた開発・運用については、必ずご自身の責任と判断によって行ってください。これらの情報による開発・運用の結果について、著者はいかなる責任も負いません。
クリーンアーキテクチャは、様々なソフトウェアアーキテクチャに関するアイデアをまとめて、パターンに落とし込んだものです。そのため、クリーンアーキテクチャを理解するためには、これまでのソフトウェアアーキテクチャを振り返る必要があります。本章にてソフトウェアアーキテクチャがどのように発展していったのか、確認していきましょう。
ソフトウェア開発におけるアーキテクチャは、システムの「設計図」であり、その根幹となる構造を定義します。関心事の分離によってシステムをパーツ分けし、アーキテクチャはそのパーツをどのように組み立てるかの設計図の役割を担います。プリンシプル オブ プログラミング1では、関心事の分離について以下のように述べられています。
「関心」とは、ソフトウェアが担当する機能や役割を指します。その「分離」とは、各機能や役割に関連するコードを一箇所にまとめ、独立したモジュールやクラスとして、他のコードと分けることを意味します。
では、なぜアーキテクチャにこだわる必要があるのでしょうか?次の節でアーキテクチャの目的に関して解説をします。
アーキテクチャの目的は何でしょうか。ここではアーキテクチャの目的は、「変更しやすいソフトウェアを創ることによって長期的なビジネス価値を提供することにある」と考えます。
「変更しやすいソフトウェア」とは、ビジネスニーズ、市場の動向、技術の変化に柔軟に対応できるソフトウェアを指します。たとえば、使用しているフレームワークの開発が停止した場合、他のフレームワークへの移行が必要となります。このような事態に対応するには、フレームワークやデータベース等の外部リソースを交換可能にする必要があります。また、スピーディな仕様変更には、1人あたりのエンジニアが効率的に開発を進められることも重要です。これらのニーズをアーキテクチャを通じて解決することは、ビジネスの長期的な継続に繋がります。
アーキテクチャを目的にしない
アーキテクチャを学習すると、アーキテクチャにこだわりすぎることがあります。厳格に、アーキテクチャ通りに設計しなければならないと考えてしまうのです。あくまで、⻑期的なビジネス価値を提供するための手段であることを忘れてはいけません。ビジネスの目的に合わせて、適切な手段を選択することが重要になります。状況によって、アーキテクチャの重要性が変わることを認識しておきましょう。
さて長期的なビジネス価値を提供するためには、変更しやすいアーキテクチャが重要であると説明しました。では、どのようにして変更可能なアーキテクチャを構築すればいいでしょうか?ここからはアーキテクチャ発展の歴史を振り返り、アーキテクチャのポイントを確認していきましょう。
レイヤードアーキテクチャは、ソフトウェア設計において広く採用されているアーキテクチャのひとつです。図1.1は、その一般的な構成を示しています。
このアーキテクチャは少なくとも3つのレイヤーに構成されており、各レイヤーはそのひとつ下のレイヤーに依存します。主に、UI(ユーザーインターフェース)を担当するプレゼンテーションレイヤー、ビジネスロジックを担当するドメインレイヤー、そしてデータベースや外部サービスとの通信を担当するインフラストラクチャーレイヤーの順に構成されます。このような構造から、ビジネスロジックはインフラストラクチャーレイヤーに依存することとなります。この依存関係により、以下のような問題が生じる可能性があり、ソフトウェアの変更が難しくなることが懸念されます。
・依存先の変更がビジネスロジックにも影響する
・依存先のコードができるまで開発が進まない
・テストの実行が依存先のコードに影響を受けてしまう
これら3点について、具体的に見ていきましょう。
依存先の変更がビジネスロジックにも影響する
アプリケーションで使用するデータベースが、MySQLからPostgreSQLに変更されることを考えてみます。データベースの種類が変わると、データベースへのアクセス方法が変わる可能性があります。以下、サンプルコードです。
func SaveProductUseCase() {
// productRepo := NewProductMySQLRepository()
productRepo := NewProductPostgreSQLRepository() // すべてのユースケースで変更対応が必要
productRepo.Save()
}
データアクセスの関数について、MySQLからPostgreSQL専用のものに変更する必要があります。このように図1.1の依存関係では、インフラストラクチャーレイヤーの変更がビジネスロジックに影響を与え、そのコードも変更を余儀なくされます。
依存先のコードができるまで開発が進まない
データベースに関わる処理の実装が完了していないと、ビジネスロジックの実装が進まない場合があります。以下、サンプルコードです。
func SaveProductUseCase() {
productRepo := NewProductRepository() // NewProductRepositoryが完成するまで開発が進まない。
productRepo.Save()
}
SaveProductUseCase関数は、NewProductRepository関数の処理の結果を利用しています。そのため、NewProductRepository関数やデータベースの準備ができていない場合、SaveProductUseCase関数の開発は進めることができません。
テストの実行が依存先のコード影響を受けてしまう
データベースの処理がビジネスロジックのテスト結果に影響してしまう可能性があります。以下、サンプルコードです。
// 実装
type SaveProductUseCase struct {
productRepo productDomain.ProductRepository
}
func (uc *SaveProductUseCase) Run() (*SaveProductUseCaseDto, error) {
p, err := productDomain.NewProduct()
err = uc.productRepo.Save(ctx, p) // productDomain.Save()の影響を受ける
// 以下実装
}
// テスト
func TestSaveProductUseCase_Run(t *testing.T) {
p := productDomain.NewProduct() // Save()が未実装の場合、テストは失敗する
// 以下テスト
}
SaveProductUseCaseは、ProductRepositoryに依存しています。Run関数と同様に、TestSaveProductUseCase_Run関数でも、依存先のProductRepositoryを使う必要があります。
依存先のコードがないと依存元の開発を進めることができないのと同様に、テストも進めることができません。また、依存先がデータベースや外部APIである場合、依存によってテストに大きく影響を与える場合があります。仮に、テストで使っているデータベースが開発環境の共用データベースと同じだとしましょう。その場合、開発環境で行ったデータの保存・削除によって、テストが壊れる可能性があります。
このようにビジネスロジックがインフラストラクチャーに依存すると、開発やテストの進行に難題が生じる可能性があります。レイヤードアーキテクチャではインフラストラクチャーが依存関係の中心となりますが、実際に最も守るべきはビジネスロジックです。したがって、図1.2のようにビジネスロジックを中心とした依存関係を構築するアプローチが求められます。このような課題に対して、どのようにアーキテクチャが解決していくのか、次節以降で解説していきます。
ヘキサゴナルアーキテクチャ(別名:ポート&アダプター)は、2005年にAlistair Cockburn氏により考案されたアーキテクチャパターンです。ヘキサゴナルと名がつきますが六角形に意味はなく、レイヤーの依存関係を外側から内側に表現したかったのが意図になります。図1.3は、ヘキサゴナルアーキテクチャを円形で表現した図となります。
注目すべきは、依存の向きが中心のアプリケーションに向いている点です。外側にインフラストラクチャーレイヤーを、内側にアプリケーションレイヤーを配置することで、ビジネスロジックが中心となる設計になっています。また、アダプターを挟むことで、直接アプリケーションがインフラストラクチャーに依存していません。この構成は、依存性逆転の原則によって実現しています。
依存性逆転の原則
依存性逆転の原則(Dependency Inversion Principle)は、依存の向きをコントロールするためのテクニックです。重要なポイントは以下2点です。
・上位モジュールは下位モジュールに依存してはならない。どちらも「抽象」に依存すべきである。
・「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである。
ここで注目すべきポイントは、「抽象」に依存させるという点です。たとえば、usecaseがrepositoryに依存している場合で考えてみましょう。
図1.4では、usecaseがrepositoryに依存しています。ここでrepositoryのインターフェースを用意すると、依存関係は図1.5のようになります。
結果、usecaseとrepositoryは両方インターフェース(抽象)に依存しました。usecaseはインターフェースを型として利用することで実装の詳細に依存せず、repositoryはインターフェースに合わせて実装するようになりました。これにより、usecaseとrepositoryは依存関係を逆転させることができました。
依存性逆転の原則のメリットは、モジュールの差し替えが可能になることです。この場合、usecaseのユニットテストを行うにあたり、repositoryのモックを用意して差し替えることが可能になります。その結果、依存先のコードにテストが依存する問題を解決することができます。
ヘキサゴナルアーキテクチャでは、アプリケーションレイヤーが中心となり、その外側にあるインフラストラクチャーとの連携は、ポートとアダプターを通じて行われます。ここでポートは「抽象」、アダプターはその「抽象」に依存した具体的な「実装」を指します。
つまり、ヘキサゴナルアーキテクチャは依存性逆転の原則を体系的にアーキテクチャに取り入れたものといえます。アダプターを変更することで、インフラストラクチャーレイヤーの詳細(データベースやフレームワークの変更)がアプリケーションレイヤー(ビジネスロジック)に影響を与えることなく、システムの変更や拡張を行うことができます。
次に説明するオニオンアーキテクチャでは、具体的にこのアプリケーションレイヤーを細かく分割して、各部分の役割を明確にしています。
オニオンアーキテクチャは、2008年にJeffrey Palermo氏により提唱されたアーキテクチャパターンです。図1.6のような構成をとります。
オニオンアーキテクチャはヘキサゴナルアーキテクチャと同様に、依存性逆転の原則を適用しており、円の外側のインフラストラクチャーから内側のドメインへと依存が向いています。また、ヘキサゴナルアーキテクチャのアプリケーションレイヤーを、アプリケーションサービス、ドメインサービス、そしてドメインモデルに細分化した形をとります。このようにすることで、アプリケーションレイヤーの関心が分離されており、保守性やテストのしやすさが向上しています。
図1.7にて、オニオンアーキテクチャをレイヤードアーキテクチャのように階層で表現しました。上のレイヤーが下のどのレイヤーにも依存できていることが、視覚的にわかります。
以下は、レイヤードアーキテクチャとオニオンアーキテクチャの比較です。
レイヤードアーキテクチャを改めて円の図で表現して、オニオンアーキテクチャと比較してみると、依存の対象が明らかに違います。レイヤードアーキテクチャではビジネスロジックがインフラストラクチャーレイヤーに依存しているため、実装の詳細から変更の影響を受けます。一方でオニオンアーキテクチャは、インフラストラクチャーから内側のドメインへと依存が向いています。
本章では、アーキテクチャの発展とその主要な目的、ポイントについて概観しました。レイヤードアーキテクチャが技術的なレイヤーに依存していたのに対し、ヘキサゴナルアーキテクチャとオニオンアーキテクチャは依存性逆転の原則によって、その課題を解決しています。これらのアーキテクチャは、依存の向きを制御することで特定の技術からビジネスロジックを独立させ、保守性や拡張性を高めています。