読者です 読者をやめる 読者になる 読者になる

サナギわさわさ.json

サナギさんとキルミーベイベーとプログラミングが好きです

Swift2のProtocol Extensionsとクラス継承を比較する

iOS プログラミング Swift

プログラムを書く時はできるだけ頭を使いたくないので、当然コードもできるだけ共通化して使いまわしたいわけです。

今まで自分はiOS開発ではクラス継承を使って処理の共通化を行っていたのですが、Swift2ではProtocol Extensionsを使って処理の共通化を行うのが良いらしいので、今回はそれについて調べてみました。

TL;DR

  • クラス継承で処理を共通化していた時は仕様変更のたびに親クラスが肥大化したり、子クラスの挙動が親クラスに依存して分かりにくくなったりするのが辛かったが、Protocol Extensionsでは複数のProtocolを使って実装を共通化できるので見通しが良くなりそう

  • 逆にSwift2でProtocol Extensionsよりクラス継承を使うべきケースを知りたい(誰か教えてください)

Protocol Extensionsの概要

SwiftのProtocolとはインターフェースみたいなもので、Protocol Extensionsとは、本来実装を持たないProtocolに対してメソッドの実装を追加できる機能です。Protocolは複数継承することができるので、これによってコードの再利用・共通化を柔軟に行うことができるようになります。

クラス継承との比較

せっかくなので従来のクラス継承を使った場合と比べてみます。 要件として、ViewControllerA,ViewControllerBで共通の処理が存在する場合を考えてみます。

まずは、ロード時に現在メンテナンス中かどうかをサーバーに確認し、必要に応じてポップアップを表示する共通処理を実装してみます。

クラス継承を用いる場合

共通処理をBaseViewControllerに記述し、 ViewControllerA,ViewControllerBからBaseViewControllerを継承します。

class BaseViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        checkMaintenance()
    }
    
    func checkMaintenance() {
        API.checkMaintenance() { result in
            //show popup if needed
        }
    }
}

class ViewControllerA: BaseViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

class ViewControllerB: BaseViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Protocol Extensionsを用いる場合

MaintenanceCheckableというProtocolを作り、Protocol Extensionsで共通処理を実装します。その後ViewControllerA,ViewControllerBからMaintenanceCheckableを継承します。

protocol MaintenanceCheckable {
    func checkMaintenance()
}

extension MaintenanceCheckable where Self: UIViewController {
    func checkMaintenance() {
        API.checkMaintenance() { result in
            //show popup if needed
        }
    }
}

class ViewControllerA: MaintenanceCheckable {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.checkMaintenance()
    }
}

class ViewControllerB: MaintenanceCheckable {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.checkMaintenance()
    }
}

この時点だと別にどちらの実装でも良さそうに見えます。では仕様変更が発生し、

  • ロード時にアクセス解析ログをサーバに送る処理を行う
  • メンテナンス中かどうかの確認はViewControllerBでは行わない

という実装をする必要が出てきた場合を考えてみます。

クラス継承を用いる場合

共通処理及び条件分岐をBaseViewControllerに追加します。まだ大丈夫ですが、この調子で仕様が増えていくとBaseViewControllerの処理が肥大化して見通しが悪くなる恐れがあります。また、子クラスの実装者が親クラスが何をやっているか把握し辛く新規メンバーの学習コストが増す気がします。

class BaseViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        sendAccessLog()
        if Mirror(reflecting: self).subjectType !=  ViewControllerB.self {
            checkMaintenance()    
        }        
    }
    
    func checkMaintenance() {
        API.checkMaintenance() { result in
            //show popup if needed
        }
    }
    
    func sendAccessLog() {
        API.sendAccessLog() { result in
         }
    }
}

class ViewControllerA: BaseViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

class ViewControllerB: BaseViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Protocol Extensionsを用いる場合

LogSendableというProtocolを新しく作り、Protocol Extensionsで共通処理を実装します。
その後ViewControllerA,ViewControllerBの継承にLogSendableを追加し、ViewControllerBの継承からMaintenanceCheckableを外します。
処理をProtocolごとに細かく分ける事で見通しが良くなり、継承しているProtocolを見ればどのような特徴を持つクラスなのかも分かります。

protocol LogSendable {
    func sendAccessLog()
}

extension LogSendable where Self: UIViewController {
    func sendAccessLog() {
        API.sendAccessLog() { result in
         }
    }
}

protocol MaintenanceCheckable {
    func checkMaintenance()
}

extension MaintenanceCheckable where Self: UIViewController {
    func checkMaintenance() {
        API.checkMaintenance() { result in
            //show popup if needed
        }
    }
}

class ViewControllerA: MaintenanceCheckable, LogSendable {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.sendAccessLog()
        self.checkMaintenance()
    }
}

class ViewControllerB: LogSendable {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.sendAccessLog()        
    }
}

まとめ

  • クラス継承で処理を共通化すると親クラスが肥大化して辛い事に加え、子クラスの挙動が分かりにくくなり新規メンバーの学習コストが増す
  • Protocol Extensionsを用いて細かいProtocolを複数継承して処理を記述する事で肥大化が避けられ、挙動も継承Protocolを見れば大体分かるようになる
  • 逆にどんな場合にProtocolよりクラス継承を使うべきなのでしょうか?(だれか教えてください)

参考