こんにちわ。初心者です
前回の記事で、クラスについてのあれこれを長々と書いていた訳ですが、クラスの全てを網羅したわけではありません
なので、今回は前回書ききれなかった事柄について書いていきます
この記事が読了してある前提で書いていくので、ぜひ読んでいってください(ダイマ)
↓
Delegate
初心者であっても、Swiftを勉強しているとDelegateという単語を見ることがままあると思います
プロジェクトを立ち上げると、AppDelegateとSenceDelegateというファイルがデフォルトで入っていているので、見たことない人の方が少ないかもしれませんね
それで、Delegateが何なのかを調べた人も多いと思います
では、Delegateとは一体何なのでしょう
Delegateの定義はこんな感じらしいです↓
Delegate(デリゲート)は、プログラミングにおいて、あるオブジェクト(デリゲーター)が特定のタスクや決定を別のオブジェクト(デリゲート)に委ねる設計パターンです。これは、ソフトウェア開発における「委譲」の概念に基づきます。デリゲートは一種のコールバックメカニズムであり、クラスやコンポーネント間のコミュニケーションを可能にするために使用されます。
は?????
これが理解できなくても、落ち込まないでください
普通に分かりづらいので、分からなくて当然です。私も正直あんまり分かっていません
ですが、Delegateが何なのかが理解できないと困るので、もっと人間に分かりやすい言い回しで噛み砕いてみたいと思います
Delegateとは、継承を用いずに他のクラスの機能(メソッド)を利用する為のコードの書き方である
前回、クラスとは独自に型を定義する機能であると書きました
型とはプロパティやメソッドを一つのメモリの区画にまとめることです。Swiftにおいて、この型というものは非常にルールに厳しい存在で、異なる型同士のものを代入したりすることはできません
例えば、Int型の変数にString型の値を代入できないなどが典型的な例です
ちなみに、この型によって意図しないバグなどを未然に防ぐ設計であることが、Swiftが型安全な言語だと言われる所以でもあります
そして、Swiftは型安全な言語だからこそ、異なるクラスのメソッドを利用することができません
利用する為にはクラスを継承する必要があります
継承すると他のクラスのメソッドが使えるようになるのは、継承元である親クラスと継承先である子クラスは同じ型と扱われているからです
ですが、先ほど言った通り、Delegateは継承を用いることなく、異なる型のクラスのメソッドを利用する方法です
どうすれば、この一見矛盾した方法を成立することが出来るのでしょうか?
今回はこの二つのクラスを例に用いて、話をしていきたいと思います
//ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
func generater() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
}
}
//数字を2倍にするクラス
class Calculator {
func double(number: Int) {
let caluculation = number * 2 {
print("2倍にした数字: \(calculation)")
}
}
Delegate
そもそも、この二つのクラスを使って何を実現させたいのでしょう?
今回、私が行いたいことというのは、継承を用いずにランダムな数字を一つだけ出力するクラスで出力された数字を、数字を2倍にするクラスで数字を2倍にするということです
そのために、Delegateが必要なのです
//ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
func generater() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
}
}
//数字を2倍にするクラス
class Calculator {
func double(number: Int) {
let caluculation = number * 2 {
print("2倍にした数字: \(calculation)")
}
}
今回、ランダムな数字を一つだけ出力するクラス(以下:RandomNumberGeneratorクラス)を委譲する側(デリゲート元)、数字を2倍にするクラス(以下:Calculatorクラス)を委譲される側(デリゲート先)とします
RandomNumberGeneratorクラスで、Calculatorクラスのメソッドが使えないのは、この二つのクラスの型が異なるからであることは、既に理解していると思います
では、逆にRandomNumberGeneratorクラスで、Calculatorクラスのメソッドが使えるようにする為には、この二つのクラスを隔ている型という壁を超えなければなりません
継承はこの壁を乗り越えることができますが、今回は継承を使うことはできません
なので、クラス同士を同じ型にするというアプローチではなく、他の方法を模索する必要があります
ですが、まずは原点に立ち返ります
そもそも、何を持ってして、RandomNumberGeneratorクラスで、Calculatorクラスのメソッドが使えないと判断しているのでしょう?
少なくとも、このようにRandomNumberGeneratorクラスの参照値を代入されている、変数randomNumberからCalculatorクラスのメソッドであるdoubleにアクセスすることはできません
このエラーが出ている理由が、randomNumberとdoubleが別の型であるからだということは理解されていると思います
これによって、クラスの外からアクセスする方法を用いることは出来ないということが一体何なのかが分かりました
なので、アプローチを変える方法があります
クラスの外からメソッドにアクセスできないなら、クラスの中からメソッドアクセスする必要があります
クラスとは型を独自に定義する機能です
型を独自に定義するということは、複数のプロパティやメソッドをまとめるということです
そして、このまとめられる複数のプロパティ、メソッドは同じ型である必要はありません
なので、RandomNumberGeneratorクラスの中に、Calculator型の変数を入れることが出来るということです
// ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
//ここを追加
var delegate: Calculator?
func generate() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
}
}
// 数字を2倍にするクラス
class Calculator {
func double(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}
}
Calculator?型の変数delegateをRandomNumberGeneratorクラスに追加したことで、RandomNumberGeneratorクラスの中でこのようなことが可能になりました
そして、コードは次のようになります
// ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
var delegate: Calculator?
func generate() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
//ここを追加
delegate?.double(number: number)
}
}
// 数字を2倍にするクラス
class Calculator {
func double(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}
}
generateメソッドの中で、doubleメソッドを実行します
doubleメソッドはRandomNumberGeneratorクラスの中にある定数numberを引数としているので、ランダムに生成された数字がそのまま2倍されます
こうすると、視覚的に分かりやすくなるでしょうか
// ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
var delegate: Calculator?
func generate() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
delegate?.double(number: number)
//これを実行する
/*func double(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}*/
}
}
// 数字を2倍にするクラス
class Calculator {
func double(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}
}
では、実際にこのコードを実行してみます
ですが、ただRandomNumberGeneratorクラスのインスタンスを作成して、generateメソッドを実行するだけでは、ランダムに生成された数字を2倍することはできません
RandomNumberGeneratorクラスとCalculatorクラス、二つのインスタンスを作成し、RandomNumberGeneratorクラスのdelegateプロパティにCalculatorクラスのインスタンスの参照値を代入します
そうすることで、generateメソッドが実行された瞬間、そのことがdoubleメソッドに通知され、doubleメソッドが実行されるのです
これで、継承を使うことなく、継承を用いずにランダムな数字を一つだけ出力するクラスでで出力された数字を、数字を2倍にするクラスで数字を2倍にすることができました
protocol
確かに、無事にDelegateを実現することができましたが、まだDelegateの真価を発揮できている状態とは言えません
何故、現状のコードでは不十分なのでしょうか?
そもそも、Delegateを用いなければならない意味から考えるべきです
本来、継承という方法が存在しているのにも関わらず、Delegateという、継承を用いずに他のクラスを利用したい場面とは一体どんな場合なのか
それはDelegateに関わるクラスが三つ以上である場合です
また、継承は便利ですが、継承をしてしまうと他のクラスと関連性を持たせることが出来なくなります
今回のような、Aというクラスの処理をBに委譲するというパターンは、コンソールに出力される結果だけを見れば継承でも再現することは可能です
ですが、Aクラスの処理をBクラスとCクラスとDクラスに委譲するというパターンは継承では再現することはできません
また、AクラスとA’クラスの処理をBに委譲するというパターンも再現できません
このコードでは、RandomNumberGeneratorクラスの処理はCalculatorクラスに委譲されています
// ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
var delegate: Calculator?
func generate() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
//ここを追加
delegate?.double(number: number)
}
}
// 数字を2倍にするクラス
class Calculator {
func double(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}
}
ところで、たびたび出てくる委譲という概念なのですが、これは一体何なのでしょうか
今回の話の基点は、ランダムな数字を一つだけ出力するクラスであるRandomNumberGeneratorクラスです
このRandomNumberGeneratorクラスが出力したものをどう扱うかという話をしています
今回はRandomNumberGeneratorクラスを、出力された数字を2倍にするという使い方をしました
数字の使い道はそれだけではないはずです
RandomNumberGeneratorクラスで出力された数字を2分の1にすることも出来るし、5の倍数の時だけtrueになるなど、様々な使い方ができます
// ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
var delegate: Calculator?
func generate() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
delegate?.double(number: number)
}
}
// 数字を2倍にするクラス
class Calculator {
func double(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}
}
//数字を2分の1にするクラス
class HalveCalculator {
func halvle(number: Int) {
let division = number / 2
print("2分の1にした数字: \(division)")
}
}
//特定の条件を判定するクラス
class Judgement {
func judge(number:Int) {
let conditions = number
if conditions % 5 == 0 {
print("\(conditions)は5の倍数です")
} else {
print("\(conditions)は5の倍数ではありません")
}
}
}
ですが、現状のままではRandomNumberGeneratorクラスの処理をHalveCalculatorクラスとJudgementクラスに委譲することはできません
何故かというと、RandomNumberGeneratorクラスにある変数delegateがCalculator?型であるからです
一応、RandomNumberGeneratorクラスにHalveCalculator型の変数と、Judgement型の変数を用意すれば、このエラーを解消することが出来ます
一応、全てのクラスでDelegateを実現することがありました
ですが、ちょっとこう思ったのではないのでしょうか
これ、書くのめんどくさくね……???
三つの変数delegateを一つにまとめたいですよね
そして、ここで登場するのがprotocol(プロトコル) という機能です
プロトコルという言葉は聞いたことがある人も多いと思います
約束事、協定、手順、命令と言った意味合いの英単語です
プログラミングにおいては、主に通信の手順を定めたルール(HTTP)として扱われます
Swiftにおけるプロトコルの定義とは、特定のメソッドやプロパティ、その他の要件の集まりを定義するためのものです
もっと噛み砕いていくと、プロトコルはクラスや構造体と同じく、独自に型を定義する機能の一つです
そして、プロトコルは型という垣根を超えて、クラスに採用することができます
プロトコルを採用したクラスはプロトコルの中に定義されたメソッドやプロパティを、自分のクラスの中で使うことができます
ですが、プロトコルはクラスや構造体とは異なる特徴があります
それはプロトコルの中に定義されているメソッドには具体的な処理を書かないという点です
protocol SampleProtocol {
func sampleMethod()
//ここに具体的な処理を書かない
}
プロトコルとは約束事という意味合いの英単語だと書きましたが、Swiftにおけるプロトコルとはプロトコルを採用したクラスに、プロトコルの内で定義したメソッドを持たせるという約束事ということです
ちなみに、Swiftではプロトコルと呼ばれているこの機能は、他のオブジェクト指向言語であるJavaやC#においてinterface(インターフェイス) と呼ばれる機能のことです
では、実際にプロトコルを使ってみましょう
protocol SampleProtocol {
func sampleMethod()
//ここに具体的な処理を書かない
}
//SampleProtocolを採用
class SampleClass: SampleProtocol {
func sampleMethod() {
print("これはサンプルです")
}
}
プロトコルによって、SampleProtocol型が作られました。同じように、クラスによって、SampleClass型も作られました
そして、SampleClassにはSampleProtocolが採用されているので、sampleMethodを実装することができます
ここで重要なのは、SampleClassはSampleClass型であると同時に、SampleProtocolを採用した為、SampleProtocol型でもあるという点です
なので、このようなことができます
SampleClassの参照値を代入している定数sampleは当然SampleClass型です
そして、次にあるのはSampleProtocol型であると定義した変数sampleTypeです
変数SampleTypeに定数sampleを代入します
Swiftの言語使用上、異なる型同士の変数や定数を代入することはできません
ですが、定数sampleがSampleClass型で、変数sampleTypeがSampleProtocolであるのにも関わらず、代入することができました
どうして、異なる型同士である変数に定数を代入することができたのでしょうか?
それは、SampleClassがSampleProtocolを採用したことによって、SampleClass型であると同時にSampleProtocol型としても扱われるようになっているからです
SampleClass型でも、SampleProtocol型でもない型の変数に代入しようとするとエラーが起こります
プロトコルとは約束事という意味合いの英単語だと書きましたが、Swiftにおけるプロトコルとはプロトコルを採用したクラスに、プロトコルの内で定義したメソッドを持たせる という約束事ということです
ちなみに、Swiftではプロトコルと呼ばれているこの機能は、他のオブジェクト指向言語であるJavaやC#においてinterface(インターフェイス)と呼ばれる機能のことです
このプロトコルを採用したクラスは複数の型を持っているとみなされるという性質を利用して、3個以上のクラスでのDelegateを実現していきます
protocol RandomNumberDelegate {
func didGenerateRandomNumber(number: Int)
}
// ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
var delegate:RandomNumberDelegate?
func generate() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
delegate?.didGenerateRandomNumber(number: number)
}
}
// 数字を2倍にするクラス
class Calculator:RandomNumberDelegate {
//プロトコルに定義されているメソッドを使う
func didGenerateRandomNumber(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}
}
//数字を2分の1にするクラス
class HalveCalculator:RandomNumberDelegate {
//プロトコルに定義されているメソッドを使う
func didGenerateRandomNumber(number: Int) {
let division = number / 2
print("2分の1にした数字: \(division)")
}
}
//特定の条件を判定するクラス
class Judgement:RandomNumberDelegate {
//プロトコルに定義されているメソッドを使う
func didGenerateRandomNumber(number:Int) {
let conditions = number
if conditions % 5 == 0 {
print("\(conditions)は5の倍数です")
} else {
print("\(conditions)は5の倍数ではありません")
}
}
}
処理を委譲されているCalculatorクラス、HalveCalculatorクラス、JudgementクラスにRandomNumberDelegateプロトコルを採用します
これにより、Calculatorクラス、HalveCalculatorクラス、JudgementクラスはRandomNumberDelegate型でもあると扱われることになります
そして、RandomNumberDelegateプロトコルに定義されているdidGenerateRandomNumberメソッドをクラスに実装しています
なので、RandomNumberGeneratorクラスにある、RandomNumberDelegate?型の変数delegateのみで、三つのクラスの機能を利用することが出来るようになりました
実行するとこうなります
今回は三つのDelegateをバラバラに実行しています
一応、同時にDlegateを実行する方法もありますが、Delegateの本質から外れていそうなので、ここでは説明を省略します
気になる方はマルチキャストデリゲートで調べてみるといいと思います
ところで、クラスはプロトコルを複数採用することが出来ると言いました
なので、今度は委譲する側を増やしてみます
ランダムな数字を一つ出力するクラスに対して、特定の数字を一つ出力するクラスを作成しましょう
protocol RandomNumberDelegate {
func didGenerateRandomNumber(number: Int)
}
// ランダムな数字を一つだけ出力するクラス
class RandomNumberGenerator {
var delegate:RandomNumberDelegate?
func generate() {
let number = Int.random(in: 1...10)
print("生成された数字: \(number)")
delegate?.didGenerateRandomNumber(number: number)
}
}
protocol SpecificNumberDelegate {
func didGenerateSpecificNumberDelegate(number: Int)
}
//特定の数字だけを出力するクラス
class SpecificNumberGenerator {
var delegate: SpecificNumberDelegate?
func output() {
let number = 5
print("特定の数字: \(number)")
delegate?.didGenerateSpecificNumberDelegate(number: number)
}
}
// 数字を2倍にするクラス
class Calculator:RandomNumberDelegate, SpecificNumberDelegate {
func didGenerateRandomNumber(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}
func didGenerateSpecificNumberDelegate(number: Int) {
let calculation = number * 2
print("2倍にした数字: \(calculation)")
}
}
//数字を2分の1にするクラス
class HalveCalculator:RandomNumberDelegate,SpecificNumberDelegate {
func didGenerateRandomNumber(number: Int) {
let division = number / 2
print("2分の1にした数字: \(division)")
}
func didGenerateSpecificNumberDelegate(number: Int) {
let division = number / 2
print("2分の1にした数字: \(division)")
}
}
//特定の条件を判定するクラス
class Judgement:RandomNumberDelegate,SpecificNumberDelegate {
func didGenerateRandomNumber(number:Int) {
let conditions = number
if conditions % 5 == 0 {
print("\(conditions)は5の倍数です")
} else {
print("\(conditions)は5の倍数ではありません")
}
}
func didGenerateSpecificNumberDelegate(number: Int) {
let conditions = number
if conditions % 5 == 0 {
print("\(conditions)は5の倍数です")
} else {
print("\(conditions)は5の倍数ではありません")
}
}
}
まとめ
Delegateはすごくとっつきづらい話題だと思います
委譲という概念が分かりづらいし、書いたコードを実行するのにも一手間必要です
委譲と言われるとややこしくなりますが、Delegateと継承の関係は十徳ナイフと普通のナイフ(工具)のようなものだとイメージしてください
十徳ナイフは様々なものがくっついています
ドライバー・はさみ・やすり・のこぎり、ワインオープナーや栓抜き・缶切りなど、色々な用途で使えます
ですが、機能をまとめ、持ち運びに特化した結果、扱いづらくなってしまいました
ネジを締めたいなら普通のドライバーを使った方が締めやすく、紙を切りたいなら普通のハサミを使った方が切りやすいです
現状のプログラミングの流行り(?)としては、あまり一つのクラスに複数の機能を持たせたくないという傾向が主流です
一つでなんでも出来る十徳ナイフ的なクラスを用意するよりも、一つのことしかできなくても、ドライバー・ハサミ・やすり…etcのような一つの物事に特化したクラスを用意して、必要に応じて使い分ける方がバグが発生しづらく、使いやすいからです
何でもできる十徳ナイフのようなクラスを作るのが継承で、一つのことしかできない様々なクラスを必要に応じて連携させるのがDelegateです
ちなみに、この一つのクラスに複数の機能を持たせないという考え方のことを「単一責任の原則(Single Responsibility Principle, SRP)」と言います
ソフトウェア工学の原則であり、SOLID原則の一つです
とはいえ、継承という方法が悪というわけではなく、場合によっては十徳ナイフの方が役にたつ場面も存在します。逆に、Delegateが悪手になる場合も存在します
ちゃんとメリットデメリットを見極めて、状況に応じてどちらが最適かを選べるようになりたいですね