10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

クリーンアーキテクチャーをざっと理解できる短いコードを書いた

Posted at

クリーンアーキテクチャーは沢山インターフェースやクラスが登場し、その関係をざっと把握するのが難しいです。
そこですべての要素が入ったなるべく短いコードを書いてみました。XCodeのPlaygroundなので、そのまま実行できます。githubのリポジトリはこちらです。なお、今回はざっと理解することを優先するため、クリーンアーキテクチャーについての詳しい説明は割愛します。参考記事をご覧ください。

このコードは、Uncle Bobの書いたClean Architecture 達人に学ぶソフトウェアの構造と設計と、次の記事を参考に書いています。

参考記事:
実装クリーンアーキテクチャ: https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22

クリーンアーキテクチャーの概要図

この2つの図が有名かと思います。
image.png

image.png
引用:Clean Architecture 達人に学ぶソフトウェアの構造と設計

概要図と今回のコードとの対応

サンプルコードに現れるインターフェース、クラスをなるべく本家の図と同じ位置に同じ色で配置しています。

image.png

image.png

この図で、本家と違うのがSomeViewUserControllerにも依存しているところです。理由は、本家は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と同じレイヤーに配置されているということです。このあたりに依存性逆転の原則がしっかり守られていると感じました。

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?