【プログラミング初心者】Swift基礎~プロトコル~

はじめに

今回はオブジェクト指向の考え方の1つであるポリモーフィズムの実現方法について紹介します。
ポリモーフィズムがどういうものかはこちらで簡単に説明しています。

Swiftでポリモーフィズムを実現するにはプロトコル(protocol)という機能を使います。
他言語でいうところのインターフェース(interface)に該当します。

プロトコル

プロトコルとは

プロトコルとは取り決め、約束事というような意味を持ちます。
Swiftのプロトコルとは、こんなメソッド、プロパティを必ず持つということを強制する技術です。

クラスや構造体では作成されたオブジェクトが持つプロパティやメソッドの具体的な実装が記述されています。

一方でプロトコルには具体的な処理の実装はなく、実装すべきプロパティ、メソッドが記述されているだけです。

クラスや構造体がオブジェクトの設計書とするならばプロトコルは仕様書といったところでしょうか。

まだピンとこないかと思いますが実装方法を見ていきましょう。

プロトコル定義

プロトコルの定義方法は以下となります。

protocol プロトコル名 {
    var プロパティ名: 型名 { get set }

    func メソッド名(引数名: 型名)
}

クラス定義などと似ていますが、まずプロパティに{ get set }とあります。
{ get set }とした場合、そのプロパティが読み書き可能ということを表します。
{ get }の場合は読み込み専用ということを表します。

またメソッドがありますが具体的な処理が書かれていません。

このように実装すべきプロパティとメソッドを定義します。

プロトコルはこのように具体的な実装はない抽象的な定義です。
そのためクラスなどとは違いプロトコルからオブジェクトを作成することはできません。
プロトコルはクラスで継承させて使います。

実装例を見ていきます。
動物プロトコルを作ります。動物は必ず鳴くものとしbark()メソッドを持っているとします。

protocol AnimalProtocol {
    /// 鳴く
    func bark()
}

これでAnimalProtocolを作成できました。

ここから動物プロトコルを継承した犬クラスを作成してみます。
一度以下のように実装してみてください。

class Dog: AnimalProtocol {

}

これで犬クラスは作成されましたが
Type ‘Dog’ does not conform to protocol ‘AnimalProtocol’
というエラーが発生したかと思います。
犬クラスは動物プロトコルに準拠していませんという意味です。
つまりプロトコルとして定義されているものが実装されていませんと怒られています。
このようにして実装を強制します。

ではbark()メソッドを実装します。

class Dog: AnimalProtocol {
    func bark() {
        print("ワン")
    }
}

これでエラーが消えました。
あとはいつも通りクラスからインスタンスを作成してあげるだけです。

let dog = Dog()
dog.bark()

プロパティもプロトコルに追加してあげます。

protocol AnimalProtocol {
    /// 名前
    var name: String { get set }

    /// 鳴く
    func bark()
}

するとDognameプロパティを追加してあげる必要があります。

class Dog: AnimalProtocol {
    var name: String

    init(name: String) {
        self.name = name
    }

    func bark() {
        print("ワン")
    }
}
let dog = Dog(name: "ポチ")
print(dog.name) // ポチ

これでプロパティもプロトコルに追加することができました。

基本的な定義の方法はこれだけです。

プロトコルのメリット

さてこれで定義することはできましたがあまりメリットを感じないどころか、無駄な実装が増えたように感じます。
実際1つのクラスだけではメリットはあまりありません。

では動物プロトコルに準拠した猫クラスを追加してみましょう。

class Cat: AnimalProtocol {
    var name: String

    init(name: String) {
        self.name = name
    }

    func bark() {
        print("にゃー")
    }
}

さらに人間クラスを作成し、ペットというプロパティを持っているとします。
人間クラスはstrokePet()という撫でるメソッドを持ち、呼び出すとペットが鳴くという実装をします。
その場合以下のような実装になります。

class Human {
    var pet: AnimalProtocol

    init(pet: AnimalProtocol) {
        self.pet = pet
    }

    func strokePet() {
        self.pet.bark()
    }
}

すると以下のようになります。

print("---人間のペットが犬の場合---")
let human1 = Human(pet: Dog(name: "ポチ"))
human1.strokePet()

print("---人間のペットが猫の場合---")
let human2 = Human(pet: Cat(name: "タマ"))
human2.strokePet()
---人間のペットが犬の場合---
ワン
---人間のペットが猫の場合---
にゃー

この結果が当たり前のように感じるかもしれませんが、クラスのみを扱ってきた今までとは以下の部分が決定的に異なります。

let human1 = Human(pet: Dog(name: "ポチ"))
let human2 = Human(pet: Cat(name: "タマ"))

human1human2を比較すると初期化で与えられているオブジェクトのクラスが異なります。
今まではクラスに一致するオブジェクトしか引数に指定できませんでした。
これは人間クラスの初期化でinit(pet: AnimalProtocol)と引数にクラスではなくプロトコルを指定しているためです。
このようにAnimalProtocolに準拠したオブジェクトであればどんなオブジェクトを入れることができます。

またstroke()では

func strokePet() {
    self.pet.bark()
}

という呼び出しをしています。

self.petは犬なのか猫なのかは人間クラスにはわかりません。
ですがpetAnimalProtocolに準拠したオブジェクトなのでbark()の実装を強制されていることはわかっているので犬なのか猫なのかを意識することなく呼び出すことができます。

このように人間クラスの実装を変えることなく具体的な処理の内容を変更することができるのがプロトコルのメリットであり、オブジェクト指向のポリモーフィズムという考え方です。

また、鳥クラスを追加したいとします。
この鳥クラスもAnimalProtocolに準拠させることでHumanクラスの実装を変えることなく扱うことができます。

class Bird: AnimalProtocol {
    var name: String

    init(name: String) {
        self.name = name
    }

    func bark() {
        print("チュン")
    }
}
let human3 = Human(pet: Bird(name: "鳥"))
human3.strokePet()
チュン

このようにプロトコルとして扱うと呼び出し元は具体的なクラスを意識する必要がなくなるため変更に強いコードとなります。

プロトコルを使わない場合は?

補足としてプロトコルを使わない場合の実装例を挙げます。

class Dog {
    var name: String

    init(name: String) {
        self.name = name
    }

    func bark() {
        print("ワン")
    }
}
class Cat {
    var name: String

    init(name: String) {
        self.name = name
    }

    func bark() {
        print("にゃー")
    }
}
class Human {
    var dog: Dog?
    var cat: Cat?

    init(pet: Any) {
        if let dog = pet as? Dog {
            self.dog = dog
        }
        if let cat = pet as? Cat {
            self.cat = cat
        }
    }

    func strokePet() {
        self.dog?.bark()
        self.cat?.bark()
    }
}
print("---人間のペットが犬の場合---")
let human1 = Human(pet: Dog(name: "ポチ"))
human1.strokePet()

print("---人間のペットが猫の場合---")
let human2 = Human(pet: Cat(name: "タマ"))
human2.strokePet()

このように人間クラスは具体的なクラスを指定したdogcatを持つ必要があります。
またイニシャライザではpetをキャストして犬なら犬、猫なら猫それぞれ判定してプロパティに入れる必要があります。
(Anyは全ての型を表します。)

strokePetではそれぞれがオプショナルとして定義しているのでnilではないオブジェクトのみ実行されるというような構成になっています。

さて、これはパッと見ただけであまり綺麗なコードとは思えません。
綺麗さだけならまだしも、さらに鳥クラスを追加するとなると人間クラスの実装も変更する必要があります。

このことからもプロトコルを使って実装した方がいいことがわかります。

最後に

今回はプロトコルの実装方法について紹介しました。
少し難しかったり、どういったタイミングで実際に使えばいいのかイメージしづらいかったかもしれません。
このプロトコルをどう使っていくのかというところは正直かなり難しく、かなり経験が必要になるのかなと思います。
iOSアプリではプロトコルを使ったデリゲートという考えが多く使われています。
こちらは別途紹介しますが基本的な使い方とデリゲートを押さえておけばいいかと思います。

今回の内容は以上です。

コメント

タイトルとURLをコピーしました