多くのプログラミング言語がオブジェクト指向という方式を採用しており、class
で定義されたオブジェクト同士の相互作用による変化でアプリケーションの動作を記述します。
対してSwiftはProtocol指向言語と言われており、オブジェクト指向言語と一線を画しています。ここではProtocol指向の概要、メリット、そしてなぜSwiftがそのように呼ばれるのかについてみていきたいと思います。
Protocolでできること
1. 処理の共通化
まず、以下のようにAPI Clientを実装する場合を考えます。
protocol ProtocolApiClient {
var baseURLString: String { get }
var path: String { get }
func call()
}
そして、準拠させる時にデフォルトの値、関数の処理内容が決まっている場合は、以下のようにextension
を用いることでデフォルト値、処理が定義できます。
// 追加
extension ProtocolApiClient {
var baseURLString: String {
return "https://qiita.com/api/v2"
}
func call() {
let request = URLRequest(url: URL(string: "\(baseURLString)\(path)")!)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data else { return }
do {
let object = try JSONSerialization.jsonObject(with: data, options: [])
print(object)
} catch let error {
print(error)
}
}
task.resume()
}
}
こうすることで、簡単に異なる処理を行う複数のAPI Clientを実装できます。
struct ProtocolApiClientA: ProtocolApiClient {
var path: String = "/groups"
}
struct ProtocolApiClientB: ProtocolApiClient {
var path: String = "/users"
}
let apiClientOfProtocolA = ProtocolApiClientA()
apiClientOfProtocolA.call() // Group取得結果のJSON
let apiClientOfProtocolB = ProtocolApiClientB()
apiClientOfProtocolB.call() // User取得結果のJSON
2. 複数のProtocolへの準拠
またクラスとの大きな違いとして、クラスは継承元を一つしか選べませんが、protocolは複数のprotocolに同時に準拠できると言うメリットがあります。
例えば、API Clientにmethodも定義したいといった場合です。
protocol Methodable {
var method: String { get }
}
struct ProtocolApiClientA: ProtocolApiClient, Methodable {
var path: String = "/templates"
var method: String = "POST"
}
struct ProtocolApiClientB: ProtocolApiClient, Methodable {
var path: String = "/users"
var method: String = "GET"
}
ただこのままですと、ProtocolApiClient
のデフォルト処理であるcall()
が呼ばれてしまうため、method
に対応してくれません。このように、あるprotocolで特定のprotocolに準拠している場合のみデフォルト処理を変えたい時、以下のように特定の条件下で設定されるデフォルト値を定義できます。
extension ProtocolApiClient where Self: Methodable {
func call() {
var request = URLRequest(url: URL(string: "\(baseURLString)\(path)")!)
// methodがあることが保証されているので、追加できる。
request.httpMethod = method
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data else { return }
do {
let object = try JSONSerialization.jsonObject(with: data, options: [])
print(object)
} catch let error {
print(error)
}
}
task.resume()
}
}
let apiClientOfProtocolA = ProtocolApiClientA()
apiClientOfProtocolA.call() // Templates POST結果のJSON(注:Unauthorizedエラーになります)
let apiClientOfProtocolB = ProtocolApiClientB()
apiClientOfProtocolB.call() // User取得結果のJSON
Protocol指向であることのメリット
同じことをクラスで実装しようとした場合にメリットがわかりやすいので、前章の内容をクラスで実装していきます。
処理の共通化
まず1.で作成した、共通処理を持っているAPI Clientは以下のように実装できます。
class ClassApiClient {
var baseURLString = "https://qiita.com/api/v2"
var path = ""
func call() {
let request = URLRequest(url: URL(string: "\(baseURLString)\(path)")!)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data else { return }
do {
let object = try JSONSerialization.jsonObject(with: data, options: [])
print(object)
} catch let error {
print(error)
}
}
task.resume()
}
}
class ClassApiClientA: ClassApiClient {
override var path: String {
get {
"/groups"
}
set {
}
}
}
class ClassApiClientB: ClassApiClient {
override var path: String {
get {
"/users"
}
set {
}
}
}
let apiClientA = ClassApiClientA()
apiClientA.call() // Group取得結果のJSON
let apiClientB = ClassApiClientB()
apiClientB.call() // User取得結果のJSON
classでは具体的な値が入ってしまっているため、継承先ごとにデフォルト値を変える場合、継承先でoverrideしていかなくてはなりません。
複数のクラスを元に継承する
さらに、ここにmethodを追加する必要がある場合、以下のようになります。
// 追加
class ClassApiClientAllMethod: ClassApiClient {
var method = "GET"
override func call() {
var request = URLRequest(url: URL(string: "\(baseURLString)\(path)")!)
request.httpMethod = method
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data else { return }
do {
let object = try JSONSerialization.jsonObject(with: data, options: [])
print(object)
} catch let error {
print(error)
}
}
task.resume()
}
}
class ClassApiClientAllMethodA: ClassApiClientAllMethod {
override var path: String {
get {
"/templates"
}
set {
}
}
// 追加
override var method: String {
get {
"POST"
}
set {}
}
}
class ClassApiClientAllMethodB: ClassApiClientAllMethod {
override var path: String {
get {
"/users"
}
set {
}
}
}
classの場合、継承元を一つしか選べません。そのため、複数の継承先を実装しようとすると、一方を継承元とし、もう一方を継承したclassからさらに継承する、といった段階を踏まなくてはなりません。そのため、例えばここからMethodがPOSTの場合はBodyを付与して、GETの場合はQueryParameterを付与して、、のように値を追加すると、その組み合わせの数だけ、継承のために作られたclassが増えることになります。
Protocolの場合、Bodyを付与するprotocol, QueryParameterを付与するprotocolを実装し、準拠先に追加すればOKです。
protocolそのものの利点
classではクラスの継承によって処理や値を共通化するため、複雑なクラスになってくると不要な変数や関数も継承先で使用できてしまうため、何かバグが生じた時、それがどのクラスの処理で起こっているエラーなのかが判断しにくいです。
対してprotocolは、必要な変数や関数のみが記述されていると言う特性上、不要な変数や関数が準拠先に行ってしまうということはまずありません。エラーが起こった際にも、どのprotocolの関数で生じているエラーなのかがすぐに判断できます。
このように、classのように複雑な関係になりにくい点も、protocolの利点であるといえます。
なぜSwiftはProtocol指向と呼ばれるのか
まず大きな理由として、SwiftではArray
などの基本型からView
などのUIに関するものに至るまで、ほとんどの型がProtocolによって実装されていることが挙げられます。
そのため、Protocolに準拠させて何か処理を書くことが他の言語に比べてしやすいため、上記で挙げたProtocolの持つメリットを享受しやすいです。
またSwiftに限らずですが、Protocolのメリットとしてよく挙げられる具体に依存しないコードが書けることもSwiftでProtocolがよく用いられるメリットの一つかと思います。そのため、コードの変更に強いアプリが作成できます。
最後に、Swiftは複数のProtocolへの準拠を組み合わせてアプリを完成させている言語です。もしこれを全てclassで実装しようとすると、例えばm個のクラスとn個のクラスの2つを組み合わせた別クラスを作る際、m個のクラスごとにそれぞれn個のクラスを継承させていかなくてはならず、合計m * n通りの別クラスが必要になりますが、protocolだとm個のprotocolとn個のprotocolのそれぞれ一つずつを準拠させるだけで済むので、必要となるprotocolはこれらm + n通りだけで済みます。
このようにprotocolは、複数のprotocolへの準拠も容易に行え、かつ具体と抽象を切り離せるため、Swiftにおいて主要な使い方になっています。
まとめ
- Swiftは複数のprotocolを同時に準拠してアプリを作成する言語であるため、protocol指向プログラミング言語と呼ばれる。
- protocolはextensionを定義することでデフォルト値の設定が行える。
- ある特定のprotocolに準拠している場合にのみ処理の内容を書き換えたい場合、
where Self
句を用いることで実現できる。 - classと比較した際のProtocolのメリットは主に3つ。
- デフォルト値や処理の設定、あるインスタンス特有の値の設定が簡単に行える。
- 異なる型の組み合わせを簡単に行える。
- 内部処理が複雑になりにくい。
最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。