DIとは
引用元:weblio辞書
フルスペル:Dependency Injection
読み方:ディーアイ
別名:依存性注入,依存性の注入
DIとは、プログラミングにおけるデザインパターン(設計思想)の一種で、オブジェクトを成立させるために必要となるコードを実行時に注入(Inject)してゆくという概念のことである。
依存性の注入???
※依存性と言う翻訳は間違いのようです。
DI != 依存性の注入
@loveeさんありがとうございます!
依存という言葉が私には抽象的で理解しずらいので
まず依存という概念から理解するために
私なりの具体例を考えてみました。
「モノマネ芸人」 と 「歌手」
「モノマネ芸人」はマネるため常に「歌手」を監視しています。
そして「歌手」が歌詞を変えると自分が歌う歌詞も変わります。
一方「歌手」は「モノマネ芸人」に合わせる必要はないので
「モノマネ芸人」の歌詞が変わろうと「歌手」の歌詞はかわりません。
この状況を
「モノマネ芸人」は「歌手」に依存しているといえます。
具体例をコードにしてみます。
歌手クラスを作ります。
class 歌手クラス {
let 名前: String = "和田ア〇子"
let 歌詞: String = "あの頃は~♪"
func 歌う() {
print("「\(歌詞)」")
}
}
let 歌手 = 歌手クラス()
歌手.歌う()
//「あの頃は~♪」
続いてモノマネ芸人クラスを作ります。
class モノマネ芸人クラス {
let 名前: String = "シャチ〇コ"
let 本人: 歌手クラス = 歌手クラス() // もはや和田ア〇子
func マネる() {
本人.歌う()
print("by \(名前)")
}
}
let モノマネ芸人 = モノマネ芸人クラス()
モノマネ芸人.マネる()
// 「あの頃は~♪」
// by シャチ〇コ
これでシャチ〇コが和田ア〇子に依存している状態を
作ることができました。
この状態だと
インスタンス化すると自動的に
歌手 = 和田ア〇子
モノマネ芸人 = シャチ〇コ
が確定してしまうし
モノマネ芸人は和田ア〇子のマネしかできません。
「シャチ〇コ」 が **「ミス〇ル」**を歌ったりするにはどうすればいいだろうか。。。
まず歌手クラスはイニシャライザで名前と歌詞を決められるようにしましょう。
class 歌手クラス {
let 名前: String
let 歌詞: String
init(名前: String, 歌詞: String) {
self.名前 = 名前
self.歌詞 = 歌詞
}
func 歌う() {
print("「\(歌詞)」")
}
}
let ミス〇ル = 歌手クラス(名前: "ミス〇ル", 歌詞: "シーソーゲーム♪")
ミス〇ル.歌う()
// 「シーソーゲーム♪」
これで和田ア〇子以外の歌手を作れるようになりました。
モノマネ芸人もイニシャライザで名前と本人(歌手)を決められるようにしましょう。
class モノマネ芸人クラス {
let 名前: String
let 本人: 歌手クラス
init(名前: String, 本人: 歌手クラス) {
self.名前 = 名前
self.本人 = 本人
}
func マネる() {
本人.歌う()
print("by \(名前)")
}
}
let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ", 本人: ミス〇ル)
モノマネ芸人.マネる()
// 「シーソーゲーム♪」
// by シャチ〇コ
これでめでたくミス〇ルを歌うことができました。
実は今のがDIの内の一つなのです。
Initializer Injection
そのままですがイニシャライズ時に依存先のオブジェクトを注入することです。
今回だとこの部分です。
init(名前: String, 本人: 歌手クラス)
イニシャライズで依存先の歌手を代入してますね。
ではもう一つのDIである
Method Injectionという方法があります
こちらはどんなものかと言うとその名の通り
メソッドを呼ぶ時に依存先のオブジェクトを注入することです。
次はメソッドインジェクションを使った
モノマネ芸人のコードを見てみましょう。
class モノマネ芸人クラス {
let 名前: String
init(名前: String) {
self.名前 = 名前
}
func マネる(本人: 歌手クラス) {
本人.歌う()
print("by \(名前)")
}
}
let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ")
モノマネ芸人.マネる(本人: ミス〇ル)
// 「シーソーゲーム♪」
// by シャチ〇コ
「マネる」メソッドの引数で依存先である歌手インスタンスを代入しています。
こちらもめでたくミス〇ルを歌うことができました。
しかし!
実はまだ違和感が残っています。
そもそもの話なのですが
モノマネ芸人って、歌だけじゃないですよね?
モノマネといえば
歌手や俳優やスポーツ選手や芸人など、、、
なんなら動物のまねだってします.
要するに
芸を披露する人をマネるのです!
いわば**「芸能人」**に依存すればいいのです!
※動物は人じゃないですね、、、
「何かしら芸を持ち披露する人を芸能人とする」
このように
「共通な属性を抜き出し、これを一般的な概念として捉えること」
を抽象化といいます。
SOLID原則というオブジェクト指向の設計原則によると
- 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。
- 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。
つまり歌手もモノマネ芸人も芸能人という抽象に依存するように設計するとよさそうです。
プロトコル
Swiftではプロトコルを使うことで抽象を表現できます。
protocol 芸能人 {
var 名前: String { get }
var 芸: String { get }
func 披露()
}
名前と芸を持っていて
披露することができるやつ
すなわち芸能人プロトコルです。
このプロトコルで歌手を表現してみます。
class 歌手クラス: 芸能人 {
let 名前: String
let 芸: String
init(名前: String, 芸: String){
self.名前 = 名前
self.芸 = 芸
}
func 披露() {
print("「\(芸)」")
}
}
let 小田〇正 = 歌手クラス(名前: "小田〇正", 芸: "言葉にできない〜♪")
小田〇正.披露()
// 「言葉にできない〜♪」
試しにポケ〇ンクラスも作ってみます。
class ポケ〇ンクラス: 芸能人 {
let 名前: String
let 芸: String
init(名前: String, 芸: String){
self.名前 = 名前
self.芸 = 芸
}
func 披露() {
print("サ〇シ「いけ!\(名前)!」")
print("「\(芸)」")
}
}
let ピ〇チュウ = ポケ〇ンクラス(名前: "ピ〇チュウ", 芸: "ピッカーーー!")
ピ〇チュウ.披露()
// サ〇シ「いけ!ピ〇チュウ!」
// 「ピッカーーー!」
モノマネ芸人側はこうなります。
class モノマネ芸人クラス {
let 名前: String
let 本人: 芸能人
init(名前: String, 本人: 芸能人) {
self.名前 = 名前
self.本人 = 本人
}
func マネる() {
本人.披露()
print("by \(名前)")
}
}
let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ", 本人: ピ〇チュウ)
モノマネ芸人.マネる()
// サ〇シ「いけ!ピ〇チュウ!」
// 「ピッカーーー!」
// by シャチ〇コ
ずいぶん自由がきく設計となり
モノマネ芸人に様々な
芸能人をDIできるようになりました。
しかしもうちょっとお付き合いいただきたい。
芸能人の芸がString型しか扱えないのも
修正したいです。
protocol 芸能人 {
var 名前: String { get }
var 芸: String { get } // これです。
func 披露()
}
Swiftでは動的に型を指定する仕組みとして
ジェネリクスと言う仕組みを使います。
protocolに使う場合は
assosiatedtype 型変数
で型変数を定義しておいて
class内にて
typealias 型変数 = 指定の型
としてクラス側で型を決めることができます。
今回の場合だと
protocol 芸能人 {
associatedtype 芸種 // この型変数が↓
var 名前: String { get }
var 芸: 芸種 { get } // ここにも反映されます
func 披露()
}
※型変数と言う言葉は正しいかどうかわかりませんがわかりやすいので今回はそう呼ばせてください!
[String]を持った歌手クラス
class 歌手クラス: 芸能人 {
typealias 芸種 = [String] //←ここでprotocolの芸種という型変数に[String]型を代入
var 名前: String
var 芸: 芸種 // ここも
init(名前: String, 芸: 芸種){ //ここも
self.名前 = 名前
self.芸 = 芸
}
func 披露() {
for g in 芸 { // 配列で定義しているのでイテレータ回せちゃいます
print("「\(g)」")
}
}
}
let 小田〇正 = 歌手クラス(名前: "小田〇正", 芸: ["ら〜らーらー","ららーらー","言葉ぁに","できなぁい"])
小田〇正.披露()
// 「ら〜らーらー」
// 「ららーらー」
// 「言葉ぁに」
// 「できなぁい」
Int型の芸を持ったスクワットクラス
class スクワットクラス: 芸能人 {
typealias 芸種 = Int
let 名前: String
let 芸: 芸種
init(名前: String, 芸: 芸種){
self.名前 = 名前
self.芸 = 芸
}
func 披露() {
print("レッツスクワット!")
for i in 1...芸 {
print("\(String(i))!")
}
print("ビクトリー!")
}
}
let ビリー = スクワットクラス(名前: "ビリー", 芸: 5)
ビリー.披露()
// レッツスクワット!
// 1!
// 2!
// 3!
// 4!
// 5!
// ビクトリー!
しかしここで注意です。
associatedtypeを持ったprotocolはプロパティの型としてとして利用できません
class モノマネ芸人クラス {
let 名前: String
let 本人: 芸能人 // ここです。
...
こんなエラーが出ます。
error: protocol '芸能人' can only be used as a generic constraint
because it has Self or associated type requirements
google翻訳によると
「プロトコル '芸能人'は、自己または関連する型の要件があるため、ジェネリック制約としてのみ使用できます。」
つまり型として使うにはtypealiasなどで型を指定しないと制約として機能しないので
ストアドプロパティには直接定義できなくなるのです。
ただしジェネリック制約としてのみ使用できるようです。
エラーメッセージに言われた通り
ひと工夫してジェネリクスを使って動的な型(クラス)を指定すると実現が可能です。
class モノマネ芸人クラス<T: 芸能人>{
let 名前: String
let 本人: T
init(名前: String, 本人: T) {
self.名前 = 名前
self.本人 = 本人
}
func 披露() {
本人.披露()
print("by \(名前)")
}
}
そうすると
小田〇正をDIしたり
let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ", 本人: 小田〇正)
モノマネ芸人.披露()
// 「ら〜らーらー」
// 「ららーらー」
// 「言葉ぁに」
// 「できなぁい」
// by シャチ〇コ
ビリーをDIしたり
も出来ちゃうのです。
let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ", 本人: ビリー)
モノマネ芸人.披露()
// レッツスクワット!
// 1!
// 2!
// 3!
// 4!
// 5!
// ビクトリー!
// by シャチ〇コ
最後に
DIは手段であり目的ではないのですが
設計に関するドキュメントを読むとよく出てきて
さらっと流されることもしばしばあります。DIに限りませんがw
私はそこから先に進めないことが多々あったので
もし同じ状態の方がいらっしゃれば
一旦DIのイメージが沸いた状態で
ドキュメントに戻るとより理解が深まるのではないかという想いで
敢えてDIという手段自体に焦点を当てた記事にしました。
有識者からすると突っ込みどころは満載だろうと予測しております。
可能であれば間違いやご指摘をたくさんいただけると幸いです。