概要
Swiftはenumを使って型による分岐の網羅性検査ができますが、classやprotocolに対してはできません。そこで、classやprotocolでも網羅性を実現するためのSwitcherパターンを紹介します。
Switcherパターン
Switcherパターンとは、網羅検査をしたい型に対して、サブタイプごとのcaseをもったenumを作成して、そのenumを返すプロパティを実装するパターンの事です。
パターン名については僕の勝手な命名なので、広く周知されている呼び方があったら教えてください。
例を用いて説明します。下記のように、3つのサブクラスが定義されたAnimalクラスがあったとします。話の前提として、サブクラスはこれらに限定されているとします。
class Animal {
}
final class Cat : Animal {
}
final class Dog : Animal {
}
final class Pigeon : Animal {
}
これを引数に取るtakeAnimal関数があり、型による分岐をするとします。
func takeAnimal(_ animal: Animal) {
switch animal {
case let cat as Cat:
print("猫")
case let dog as Dog:
print("犬")
case let pigeon as Pigeon:
print("鳩")
default:
fatalError("never come here")
}
}
このようなswitch-caseでの実装は、網羅性が保証されないという問題があります。
Switcherパターンでは、親クラスにSwitcherというenumを定義し、そのサブクラスごとにcaseを定義します。caseにはassociated valueとして、そのサブクラス型の値を与えます。
class Animal {
enum Switcher {
case cat(Cat)
case dog(Dog)
case pigeon(Pigeon)
}
}
そして、親クラスにこのSwitcherの値を返すプロパティを実装します。
class Animal {
enum Switcher {
case cat(Cat)
case dog(Dog)
case pigeon(Pigeon)
}
var switcher: Switcher {
fatalError("not overrided")
}
}
個別のサブクラスには、それぞれに対応したcaseを返す実装を与えます。
final class Cat : Animal {
override var switcher: Switcher {
return .cat(self)
}
}
final class Dog : Animal {
override var switcher: Switcher {
return .dog(self)
}
}
final class Pigeon : Animal {
override var switcher: Switcher {
return .pigeon(self)
}
}
これで実装は完了です。これは下記のようにして使います。
func takeAnimal(_ animal: Animal) {
switch animal.switcher {
case .cat(let cat):
print("猫")
case .dog(let dog):
print("犬")
case .pigeon(let pigeon):
print("鳩")
}
}
これで網羅検査ができるようになります。
プロトコルでの利用
プロトコルでも同じようにできます。
enum AnimalSwitcher {
case cat(Cat)
case dog(Dog)
case pigeon(Pigeon)
}
protocol Animal {
var switcher: AnimalSwitcher { get }
}
struct Cat : Animal {
var switcher: AnimalSwitcher {
return .cat(self)
}
}
struct Dog : Animal {
var switcher: AnimalSwitcher {
return .dog(self)
}
}
struct Pigeon : Animal {
var switcher: AnimalSwitcher {
return .pigeon(self)
}
}
func takeAnimal(_ animal: Animal) {
switch animal.switcher {
case .cat(let cat):
print("猫")
case .dog(let dog):
print("犬")
case .pigeon(let pigeon):
print("鳩")
}
}
特定の範囲での分類
Switcherは実際のところ型とは関係なく、ただ外から定義しているだけなので、ある範囲での分類を実装する事もできます。
下記はこれまでの動物の例に対して、哺乳類について網羅するSwitcherです。そうでない場合はnilにします。
class Animal {
enum MammalSwitcher {
case cat(Cat)
case dog(Dog)
}
var mammalSwitcher: MammalSwitcher? {
return nil
}
}
final class Cat : Animal {
override var mammalSwitcher: MammalSwitcher? {
return .cat(self)
}
}
final class Dog : Animal {
override var mammalSwitcher: MammalSwitcher? {
return .dog(self)
}
}
final class Pigeon : Animal {}
func takeAnimal(_ animal: Animal) {
guard let switcher = animal.mammalSwitcher else {
return
}
switch switcher {
case .cat(let cat):
print("猫")
case .dog(let dog):
print("犬")
}
}
評価
Switcherパターンを使うと型による分岐の際には確実な網羅検査を使うことが出来ます。
バグを産む可能性として、switcherプロパティそれ自体の実装をミスする可能性はあります。クラスの例において、オーバライドを忘れる可能性があるからです。また、サブクラスを追加する際に、Switcherの定義に追加する事を忘れる可能性もあります。この点の解決には、言語自体にabstract classが実装されるのを待つ必要があります。それでもこのパターンには価値があります。なぜなら、switcherの実装は1度きりであるのに対して、分岐は複数回になりうるからです。よって、バグの生じる可能性を大きく減らせます。
プロトコルに対して適用する場合は、switcherの実装を強制できるため、実装ミスの可能性はありません。プロトコルにはinner typeが使えないため、Switcher型の名前が長くなってしまう問題はあります。