🕺

Swiftでビジネスロジックを実行するUseCaseのprotocolを作りたくなったときのための話 2022

2022/07/08に公開

はじめに

Swift 5.7からprotocolもジェネリクスが使えるようになった。そのため、ビジネスロジック的なものを入出力が抽象化された型にするとき、UseCaseというプロトコルを考えやすくなった。

UseCase

具体的にUseCaseというプロトコルを考える

protocol UseCase<Input, Output> {
    associatedtype Input
    associatedtype Output

    func execute(_ input: Input) async -> Output
    func cancel()
}

何に使うんだッピ?という感じなので、これに準拠する型を示すと次のような感じ

class GitHubSearch: UseCase {
    struct Request {
        let text: String
    }

    var task: Task<Output, Never>? {
        didSet {
            task?.cancel()
        }
    }

    func execute(_ input: Request) async -> [String] {
        task = Task {
            ["Swift", "SwiftLint"]
        }

        return await task!.value
    }

    func cancel() { task = nil }
}

class MockGitHubSearch: UseCase {
    func execute(_ input: GitHubSearch.Input) async -> GitHubSearch.Output {
        ["mock"]
    }
    var cancelCallCount = 0
    func cancel() { cancelCallCount += 1 }
}

さらに利用例

@MainActor
class Presenter {
    private let gitHubSearch: some UseCase<GitHubSearch.Input, GitHubSearch.Output>

    init(gitHubSearch: some UseCase<GitHubSearch.Input, GitHubSearch.Output>) {
        self.gitHubSearch = gitHubSearch
    }

    func search(_ text: String) async {
        let response = await gitHubSearch.execute(.init(text: text))
        print(response)
    }
}

Task { @MainActor in
    let presenter = Presenter(gitHubSearch: GitHubSearch())
    await presenter.search("swi")
}

おわりに

実際このようなUseCase型をつくらなきゃいけないとは現状考えてはおらず、好きにしたらいいじゃない。とは思います。

なぜかというと、asyncメソッドにしてるけど必ずしも非同期で返さない場合に少し気持ちが悪い。非同期じゃないのにawaitする場合がある。でもそれは例えばclassではなくactorを使った際にメソッドが自動的にasync扱いになるので、actorを使った際にこちらが明示的に非同期処理をしなくても処理的に非同期処理なのでawaitは必要になる。なので、気にしないでもいい気もする。

だとしたら「メソッドに同期用を生やせばいいんじゃないかッピ?」とかなるとじゃあどちらかしか使えない型が爆誕するッピね、となってしまうし、または「ついでに同期処理用のUseCaseも作ればいいじゃないッピ?」とか考え出すとそもそもなんでこんな型作るんだっピか?と考え出して好きにしたら良いじゃないという気持ちになりますね。

参考

https://qiita.com/yimajo/items/43f5793833e449965dba

Discussion