iOS
ポエム
設計
Swift
懺悔

SwiftでProtocolの利用方法を盛大に勘違いしていた話

iOS界隈では少し前に設計に関する議論がとても活発だった時期がありました。
Clean ArchitectureやVIPER、MVVM、MVPなどなどですね。
所属している会社ではそういう設計を取り入れる事は無く、あくまでもMVCでやり続けるという表明をしたりしていたのですが、個人的になんかもやもやしていたので、少しずつ興味がある所から調べたりしていました。
そんな中でこれまでprotocolの利用法を大きく勘違いしていた事が判明したので共有したいと思います。

大きく勘違いしていた事

どのアーキテクチャでも言われてる事として、役割の細分化があります。
大抵細かく役割を切って、それぞれの役割をprotocolでやり取りするといった説明があるかと思います。
ここで僕の勘違いしていた事が「ふむふむ、とりあえずprotocol作って分割させればええんやな」と思っていた事です。
とりあえず ViewControllerが出来る事をprotocolで定義させてしまっていました。
例えばこんな感じ

// Viewが出来る事を定義していた
protocol HogeViewProtocol: class {
    func onTap(saveButton: UIBarButtonItem)
}

struct HogeModel {
    func save(_ completion: (Bool) -> ()) {
        // TODO: Implements save
        completion(true)
    }
}

class HogeViewController: UIViewController, HogeViewProtocol {
    var model: HogeModel = HogeModel()

    @objc func onTap(saveButton: UIBarButtonItem) {
        model.save { result in
            if result {
                self.showSaveSuccessAlert()
            } else {
                self.showSaveFailedAlert()
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .done, target: self, action: #selector(onTap(saveButton:)))
    }

    /// 保存が成功したアラートを表示する
    func showSaveSuccessAlert() {
        // TODO: Implements
    }

    /// 保存が失敗したアラートを表示する
    func showSaveFailedAlert() {
        // TODO: Implements
    }
}

当初は「protocol分割した俺偉いだろー」ぐらいに思っていたんですが、今見ると何の為にprotocolを分割したのか意味不明ですね。
そう、ただのFatViewControllerの出来上がりです。

ではどうすれば良いのか

protocolは相手がいて成り立つ物だった。
例えばViewPresenterという物がいて、PresenterViewの動作を受け取り、ViewPresenterの処理の結果を受け取る役割だとします。
すると上記のコードは次の様になるかと思います。

// Presenterが呼べるメソッドを定義する
protocol HogeViewProtocol: class {
    func showSaveSuccessAlert()
    func showSaveFailedAlert()
}

// ModelもProtocol化
protocol HogeModelRepresentable {
    func save(_ completion: @escaping (Bool) -> ())
}

struct HogeModel: HogeModelRepresentable {
    func save(_ completion: @escaping (Bool) -> ()) {
        // TODO: Implements save
        completion(true)
    }
}

// 画面に関する事のみに注力する
class HogeViewController: UIViewController, HogeViewProtocol {
    lazy var presenter: HogePresenterProtocol = HogePresenter(view: self, model: HogeModel())

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .done, target: self, action: #selector(onTap(saveButton:)))
    }

    @objc private func onTap(saveButton: UIBarButtonItem) {
        presenter.saveHoge()
    }

    func showSaveSuccessAlert() {
        // TODO: implements show alert
    }
    func showSaveFailedAlert() {
        // TODO: implements show alert
    }
}

// Viewから見える事だけ定義する
protocol HogePresenterProtocol: class {
    init(view: HogeViewProtocol, model: HogeModelRepresentable)
    func saveHoge()
}

// Viewから呼ばれた事を実装して結果をViewに返す
class HogePresenter: HogePresenterProtocol {
    weak var view: HogeViewProtocol!
    var model: HogeModelRepresentable
    required init(view: HogeViewProtocol, model: HogeModelRepresentable) {
        self.view = view
        self.model = model
    }

    func saveHoge() {
        model.save { result in
            if result {
                self.view.showSaveSuccessAlert()
            } else {
                self.view.showSaveFailedAlert()
            }
        }
    }
}

これでViewPresenterprotocolによって依存性を排除して実装する事が出来ました。
特に何かのアーキテクチャを意識して書いたわけでは無いですが、依存性を分ける実装の基本は大体この様になっているかと思います。
勘違いしていた時に書いたコードはそのままだとテストがとても難しいですが、勘違いが判明してから書いたコードはテストもとてもし易い事が分かります。

まとめ

色んなアーキテクチャを調べてみる事で、今まで自分の犯していた大きな間違いに気付く事が出来ました。
まだどのアーキテクチャが良いとは一概には言えないですが、少なくとも以前よりテストがし易いコードが書ける様になった気がします。
但し、MVCのままだとこの様な分け方をする事は難しく、最低限ModelViewの間に仲介をしてくれる役割の物が居てくれるとテストを考える際に有難いなと感じる様になりました。
誤りや更に良い方法があればご指摘頂ければと思います。