株式会社LIFULLのデータサイエンスGに所属している岩﨑といいます。今回は、LIFULLでエンジニア職の2年目社員を対象に行われる研修でクリーンアーキテクチャを触ったので、それについて学んだことを軽く紹介します。
クリーンアーキテクチャ
クリーンアーキテクチャの詳しい説明は以下を読んでいただくのが良いと思います。
自分の理解だと、クリーンアーキテクチャはシステムの設計方法の一種で、ビジネスロジックを中心として依存関係を一方向に制限するように4つの層に分割します。
- ビジネスロジック(Enterprise Business Rules)
- アプリケーションロジック(Application Business Rules)
- インターフェース(Interface Adapters)
- インフラストラクチャー(Frameworks & Drivers)
基本的には箇条書きの上方向に依存するようになっているので、ビジネスロジックはどこにも依存しない形となります。
各層に具体的に以下のような処理を置きます。
(例としてアカウント登録に関して書いてみます。)
- ビジネスロジック:アカウントが持つ情報や制約(アカウント名は5文字以上など)
- アプリケーションロジック:アカウントを登録する処理
- インターフェス:入出力に関する処理(APIであれば入出力形式を適宜変更したり)
- インフラストラクチャー:DBへアカウントを保存する処理
1から3までは処理の流れはイメージしやすいですが、4のインフラストラクチャーは「アカウントを登録する処理に依存されるのでは?」と思うかもしれません。今回の研修では4は1に置かれている抽象クラスを実装する形となり、依存性注入(Dependency Injection, DI)と呼ばれる方法でその抽象クラスに実装を差し込みます。そのため、アプリケーションロジック(アカウントを登録する処理)などがインフラストラクチャーに依存するのを防ぐことができます。
ディレクトリ構成
今回作成したのはインスタグラム風の画像投稿サイトですが、サービスやそのUIにはあまりこだわっておらず、主にバックエンドAPIの設計に注力しました。ディレクトリ構成としては以下のようになっています。(言語はTypescriptで、フレームワークはExpressを使用しています。)
src
├── app.ts
├── domain <- ビジネスロジック
│ ├── model
│ ├── repository
│ └── service
├── application <- アプリケーションロジック
│ ├── dto
│ └── service
├── interface <- インターフェス
│ ├── controllers
│ ├── middleware
│ └── routes
└── infrastructure <- インフラストラクチャー
├── database
└── repository
これらの構成で実際に開発を進めていくことで、以下のメリット・デメリットが見えてきました。
- メリット
- コードの変更が容易になった
- 単体テストが書きやすくなった
- デメリット
- 小規模のサービスでは恩恵が感じづらい
それぞれについて簡単に例を交えながら説明していきます。
メリット
コードの変更が容易になった
まず、一つ目のメリットとしてあげられるのが「コードの変更が容易になった」ことです。
これは
- 依存関係がシンプル
- 基本的に抽象クラスに依存している
という2点が主な要因となっていると考えます。
例としてアカウント登録のアプリケーションロジック部分を挙げて説明します。
public async createAccount (reqDto: CreateAccountRequestDTO): Promise<CreateAccountResponseDTO> {
// アカウント・プロフィールの作成
const id = uuidv4()
const profile = new Profile(
new ProfileName(),
new ProfileImagePath()
)
const account = new Account(
new AccountId(id),
new AccountName(reqDto.name),
new Email(reqDto.email),
new AccountPassword(reqDto.password),
profile
)
// アカウントの重複確認
await this.accountDomainService.checkAccountDuplication(account)
// アカウント作成
await this.accountRepository.create(account)
const resDto = new CreateAccountResponseDTO(
'Account created successfully'
)
return resDto
}
上の処理では、ビジネスロジック層にあるものみに依存していているため、この処理を書く際にはビジネスロジックのみに気を配っていれば良いと言うことになります。これにより、コード作成・変更の際に影響する範囲が限定されて作業をしやすくなります。(注意点として、accountRepositoryという一見インフラストラクチャー層にありそうなものも、実は抽象クラスがビジネスロジック層に存在しています。)
また、前述の通りaccountRepositoryは抽象クラスであり、DBの細かい仕様などには依存していないため、外部リソースと切り離して実装を進めることができます。(もしくは、外部リソースが決まりきっていない状態でも実装が可能です。)
上記の2点により、コードの作成・変更が非常にしやすいと感じました。
単体テストが書きやすくなった
もう一つのメリットは、「単体テストが書きやすくなった」ことです。
こちらは先ほど挙げた
- 基本的に抽象クラスに依存している
と言うところが要因で、下のコードのように実装クラスを簡単に切り替えることができるようになっています。テストの際には、実際のDBではなくインメモリDBのクラスやモッククラスを使うといったことが簡単にできるような設計です。
describe('Account CRU(D) function', () => {
// Inject Class
let accountApplicationService: AccountApplicationService;
let mockAccountRepository: IAccountRepository;
// DTO
let createAccountReqDTO: dto.CreateAccountRequestDTO;
let getAccountReqDTO: dto.GetAccountRequestDTO;
let updateAccountReqDTO: dto.UpdateAccountRequestDTO;
beforeAll(() => {
accountApplicationService = container.get<AccountApplicationService>('AccountApplicationService')
mockAccountRepository = container.get<IAccountRepository>('IAccountRepository');
createAccountReqDTO = new dto.CreateAccountRequestDTO(
testData1.email,
testData1.password,
testData1.name,
)
getAccountReqDTO = new dto.GetAccountRequestDTO(testData1.name)
updateAccountReqDTO = new dto.UpdateAccountRequestDTO(
testData1.name,
testData2.name,
testData2.email,
testData2.password
)
})
上で使用しているmockAccountRepositoryはクラス内に配列を持っていて、アカウント情報をその配列のみに保存しているシンプルなクラスです。ただ、accountRepositoryという抽象クラスから実装しているため、容易に差し替えることが可能となります。
デメリット
小規模のサービスでは恩恵が感じづらい
ここまでで2つ実感したメリットを説明しましたが、逆にデメリットも感じていました。
それが、小規模なサービスでは恩恵が感じづらく、制約の部分が少し負担になっていることです。
上で挙げたメリットはどちらも、複雑なサービスで起こる課題に対しての解決策になっていると思うのですが、そもそも”コードの変更が難しくない”、”テストが書きやすい”ような単純で小さなサービスではクリーンアーキテクチャの採用は少しだけ過剰に感じました。
まとめ
普段は機械学習のモデル構築・改善なども主な業務として行なっており、バックエンドも軽く触る程度です。そのため、今回の研修ではバックエンドの基礎的な部分から、クリーンアーキテクチャの感触までを広く学ぶことができたと思います。
また、クリーンアーキテクチャを実際に触ってみて普段行なっている実験管理のコードとも相性が良さそうに感じたので、今回の学びを生かしていきたいと思っています。