抽象概念を表現する方法として最も一般的なのはクラスの継承になると思います。
なので抽象概念を表現する場合は構造体ではなく
クラスを使って型の定義をした方がいいのかなと思いました。
しかし、Swiftではクラスではなく構造体の利用を推奨しています。
どっちを使えばいいのー!!という時に
プロトコルという抽象概念を表すもう一つの方法を思い出しました。
構造体や列挙型はプロトコルに準拠するという形で抽象的な概念を具現化できます。
だったらプロトコルを使おう!ということで、
二つの表現方法を比較して優劣を付けてみたいと思います。
##クラスの継承がもたらす期待しない挙動
次のサンプルコードでは、
動物という抽象的な概念を表すAnimalsクラスと、
犬、野生の鷹という具体的なものを表すDog, WildHawkサブクラスを定義します。
どの動物にも共通した行動である「睡眠」と「移動」と飼い主をスーパークラスに定義します。
それぞれの動物の動きは異なるので、
サブクラスでは「移動」のメソッドをオーバーライドします。
// 動物クラス(スーパークラス)
class Animals {
var owner: String?
func sleep() {
print("sleeping")
}
func move() {}
}
// 犬クラス
class Dog: Animals {
override func move() {
print("Running")
}
}
// 野生の鷹クラス
class WildHawk: Animals {
override func move() {
print("Flying")
}
}
クラスの継承を利用することで、下記の挙動を得ることができました。
・move( )メソッドの多様性が実現されている
・それぞれのサブクラスで実装せずともsleep( )メソッドが使える
しかし、次のような予期せぬ挙動も招いてしまいます。
・Animalsクラスは特定の動物を表さない抽象的な概念であるため
インスタンス化は不可能であるべきだが、インスタンス化ができてしまう。
・WildHawkは野生の鷹なのでオーナーはいないはずだが、
継承によってownerプロパティが自動的に追加されている。
不要なイニシャライザやプロパティは、
クラスの誤用を招く恐れがあるため望ましくありません。
##プロトコルによるクラスの継承の問題点の克服
プロトコルとプロトコルエクステンションを利用すれば、
クラスの継承で実現可能な挙動を満たした上で、クラスの継承の問題点も克服できます。
サブクラスは一つのスーパークラスの継承しかできないのに比べ、
型は複数のプロトコルに準拠することができます。
なので、Animalsクラスをプロトコルとして表す場合は、
より適切な意味の単位で分割することができます。
次のサンプルコードでは、
Animalsクラスで定義されていた3つの要素を、動物の行動を表す
sleep( )メソッドとmove( )メソッドという2つのメソッドを含むAnimalプロトコルと、
飼うことができることを表すownerプロパティを含むOwnableプロトコルに分割しました。
protocol Ownable {
var owner: String { get set }
}
protocol Animal {
func sleep()
func move()
}
extension Animal {
func sleep() {
print("Sleeping")
}
}
struct Dog: Animal, Ownable {
var owner: String
func move() {
print("Running")
}
}
struct WildHawk: Animal {
func move() {
print("Flying")
}
}
クラスの継承と比較すると、
クラスの継承で実現できていた挙動はプロトコルでも実現できています。
・move( )メソッドの多態性が実現できている
->共通のインターフェースをプロトコルで実装している
・それぞれのサブクラスで実装せずともsleep( )メソッドが使える
->Animalプロトコルを拡張することで、sleep( )メソッドのデフォルト実装を定義している
さらに、クラスの継承で招く予期せぬ挙動は次のように克服しています。
・Animalsクラスは特定の動物を表さない抽象的な概念であるため
インスタンス化は不可能であるべきだが、インスタンス化ができてしまう。
->Animalはプロトコルなのでインスタンス化できない
・WildHawkは野生の鷹なのでオーナーはいないはずだが、
継承によってownerプロパティが自動的に追加されている。
->クラスは多重継承ができないが、複数のプロトコルに準拠する型は定義できます。
なので必要な型だけOwnableプロトコルに準拠させなければいい。
##クラス継承を利用すべき時
とはいえ、クラスの継承を全く利用しなくていいのかというとそうでもありません。
もちろんクラスの継承を利用した方がいいケースもあります。
###複数の型の間でストアドプロパティの実装を共有する
プロトコルエクステンションを利用すれば、
クラスの継承を用いずとも複数の型の間でデフォルト実装を共有できることは説明しました。
しかし、プロトコルエクステンションでは
ストアドプロパティを実装できないという制限があり、実装を共有できないケースもあります。
次のサンプルコードでは、
Animalクラスのownerプロパティにプロパティオブザーバが定義されています。
didSetキーワード
なので、初期化されたタイミングで実行されます。
class Animal {
var owner: String? {
didSet {
guard let owner = owner else {
return
}
print("\(owner) was assigned as the owner")
}
}
}
let dog = Animal()
dog.owner = "Tanaka Tarou"
実行結果
Tanaka Tarou was assigned as the owner
この挙動を先ほどのようにプロトコルで実装しようとすると下記のようになります。
protocol Ownable {
var owner: String { get set }
}
extension Ownable {
// コンパイルエラー
var owner: String {
didSet {
print("\(owner) was assigned as the owner")
}
}
}
エラー内容:Extensions must not contain stored properties
和訳:拡張機能には、保存されたプロパティを含めることはできません
同様の挙動を実装する場合は下記のようなコードになります。
protocol Ownable {
var owner: String { get set }
}
struct Dog: Ownable {
var owner: String {
didSet {
print("\(owner) was assigned as the owner")
}
}
}
var dog = Dog(owner: "Tanaka Jirou")
dog.owner = "Tanaka Jirou"
実行結果
Tanaka Jirou was assigned as the owner
これでは、全く同じ箇所が複数箇所に現れることになるため冗長なコードになります。
また、挙動を変更する際に全ての箇所を修正する必要がありますので
変更の弱いコードにもなってしまいます。
なので、ストアドプロパティを含む実装の共有は、
プロトコルではなくクラスの継承を用いる方がいいです。
以上が継承に対してのプロトコルの優位性になります。
基本的には構造体&プロトコルでの設計をし、
ストアドプロパティの実装を共有する時のみクラス&継承の組み合わせで設計しましょう。
最後までご覧いただきありがとうございました。