クリーンアーキテクチャーは沢山インターフェースやクラスが登場し、その関係をざっと把握するのが難しいです。
そこですべての要素が入ったなるべく短いコードを書いてみました。XCodeのPlaygroundなので、そのまま実行できます。githubのリポジトリはこちらです。なお、今回はざっと理解することを優先するため、クリーンアーキテクチャーについての詳しい説明は割愛します。参考記事をご覧ください。
このコードは、Uncle Bobの書いたClean Architecture 達人に学ぶソフトウェアの構造と設計と、次の記事を参考に書いています。
参考記事:
実装クリーンアーキテクチャ: https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22
クリーンアーキテクチャーの概要図
引用:Clean Architecture 達人に学ぶソフトウェアの構造と設計
概要図と今回のコードとの対応
サンプルコードに現れるインターフェース、クラスをなるべく本家の図と同じ位置に同じ色で配置しています。
この図で、本家と違うのがSomeView
がUserController
にも依存しているところです。理由は、本家はMVC1を前提としていますが、Webフレームワークなどで使われているMVC2に寄せたほうが理解がしやすいと判断したためです。このあたりは『クリーンアーキテクチャの Presenter が分かりにくいのは MVC 2 じゃないから』という記事に解説があります。
コード
実際のコードです。簡単な説明がコメント行に入っています。
// 【Frameworks & Drivers】アプリケーションフレームワークやドライバなど実装の詳細にあたる部分
// DB ORMやDAOなどDBとのやり取りをするAPI。このサンプルでは何もしない
class SomeDB {
static func executeQuery(sql: String, bindParam: [String]) {
// Dummy ここで実際にユーザーを登録する
}
}
// UI UIKitやRailsの表示部分など画面表示をするためのAPI
class SomeView {
var userController: UserController
var viewModel: UserCreateViewModel
init(userController: UserController, viewModel: UserCreateViewModel) {
self.userController = userController
self.viewModel = viewModel
self.viewModel.bind { userName in
print("登録:" + userName + "さん")
}
}
func start(){
userController.createUser(userName: "test user")
}
}
// 【Interface Adapters】Application Business RulesとFrameworks & Driversの型の相互変換
// Controllers 入力をUserCaseのために変換する(入力のための変換)
class UserController {
var userCreateUseCase: UserCreateUseCaseInputPort
init(userCreateUseCase: UserCreateUseCaseInputPort) {
self.userCreateUseCase = userCreateUseCase
}
func createUser(userName: String) {
let input = UserCreateInputData(userName: userName)
userCreateUseCase.handle(input: input)
}
}
// GateWays Frameworks & Driversからのデータを抽象化する
class UserDataAccess: UserDataAccessInterface {
func save(user: UserEntity) {
SomeDB.executeQuery(
sql: "REPLACE INTO USER (USER_NAME) VALUES (?) ",
bindParam: [user.userName]
)
}
}
// Presenters データをViewに適した加工する(出力のための変換)
class UserCreatePresenter: UserCreateUseCaseOutputPort {
var viewModel: UserCreateViewModel
init(viewModel: UserCreateViewModel) {
self.viewModel = viewModel
}
func complete(output: UserCreateOutputData) {
let userName = output.userName
self.viewModel.update(userName: userName)
}
}
class UserCreateViewModel {
typealias CallBackType = (String)->Void
var userName: String
var callBack: CallBackType?
init(userName: String) {
self.userName = userName
}
func bind(callBack: @escaping CallBackType) {
self.callBack = callBack
}
func update(userName: String) {
self.userName = userName
self.callBack?(userName)
}
}
// 【Application Business Rules】 アプリケーションのビジネスルール
// UseCaseと上位層との遣り取りをするためのオブジェクト
protocol UserDataAccessInterface {
func save(user: UserEntity)
}
protocol UserCreateUseCaseOutputPort { // Output Boundaryともいう
func complete(output: UserCreateOutputData)
}
struct UserCreateInputData {
var userName: String
}
struct UserCreateOutputData {
var userName: String
}
// Use Cases ユースケースを表す
protocol UserCreateUseCaseInputPort { // Input Boundaryともいう
func handle(input: UserCreateInputData)
}
class UserCreateInteractor: UserCreateUseCaseInputPort {
var userDataAccess: UserDataAccess
var presenter: UserCreateUseCaseOutputPort
init(userDataAccess: UserDataAccess, presenter: UserCreateUseCaseOutputPort) {
self.userDataAccess = userDataAccess
self.presenter = presenter
}
func handle(input: UserCreateInputData) {
let userName = input.userName
let user = UserEntity(userName: userName)
userDataAccess.save(user: user)
let output = UserCreateOutputData(userName: user.userName)
presenter.complete(output: output)
}
}
// 【Enterprise Business Rules】 ドメイン層
// Entities ビジネスルールをカプセル化したもの
struct UserEntity {
var userName: String
}
// Entry Point このサンプルの実行開始ポイント
let viewModel = UserCreateViewModel(userName: "")
let userDataAccess = UserDataAccess()
let presenter = UserCreatePresenter(viewModel: viewModel)
let useCase = UserCreateInteractor(userDataAccess: userDataAccess, presenter: presenter)
let userController = UserController(userCreateUseCase: useCase)
var ui = SomeView(userController: userController, viewModel: viewModel)
ui.start()
まとめ
短い、といいながら140行ほどになってしまいました。また、なるべく簡単にするためにuserName
だけを持つclass
ばかりとなってしまい、それぞれのclass
の必要性がつかみにくくなってしまったのが残念です。
コードと図を書いていて気がついたのは、UseCase
が依存するインターフェースはすべてUseCase
と同じレイヤーに配置されているということです。このあたりに依存性逆転の原則がしっかり守られていると感じました。