31
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DI(Dependency Injection)の概要

Last updated at Posted at 2017-03-30

#この記事は何か
 少し前に話題になったクリーンアーキテクチャーの中で出てきたDI(Dependency Injection: 依存性の注入)について、自分なりに解釈したものです。
#DIとは
 あるオブジェクトと別のオブジェクトの間の依存関係を薄くするために、依存関係を外から注入するという考え方です。
 まず、依存性の高いプログラムの例を紹介します。

class User {
    let firstName = "Taro"
    let familyName = "Yamada"

    func fullname() -> String {
        return firstName + " " + familyName
    }
}

class Presenter {
    let user = User()   // (1)

    func present() {
        print(user.fullname())
    }
}

let presenter = Presenter()
presenter.present()

 このような例の場合、Presenterクラスは、Userクラスを内部で作成、所持しています(1)。Presenterのインスタンスは、Userのインスタンスがなければ存在もできません。Userクラスへの依存性が非常に高い、ということです。
 ここで、依存性を低くしてみます。

class User {
    (上の例と同じ)
}

class Presenter {
    var user: User!   // (2)

    func present() {
        print(user.fullname())
    }
}

let presenter = Presenter()
let user = User()
presenter.user = user   // (3)
presenter.present()    

 Presenterクラス内部でUserクラスを作ることはなくなり(2)、外部で作ったuserを注入しています(3)。字面どおりの「依存性の注入」です。userには、Userクラスを継承したクラスのインスタンスを指定できるので、自由度も増しました。しかし、(2)で var user: User!を指定している以上、内容的にUserに依存していることには変わりありません。
 そこで、次の例です。

protocol UserSpec {   // (4)
    func fullname() -> String
}

class User: UserSpec {   // (5)
    let firstName = "Taro"
    let familyName = "Yamada"

    func fullname() -> String {
        return firstName + " " + familyName
    }
}

class Presenter {
    var user: UserSpec!   // (6)

    func present() {
        print(user.fullname())
    }
}

let Presenter = Presenter()
let user = User()
presenter.user = user
presenter.present()

 UserSpecというプロトコルが登場しました。そして、UserUserSpecに適合し(5)、PresenterUserの代わりにUserSpecに適合したインスタンスを所有します。これにより、userは、Userを継承したクラスではなく、UserSpecに適合したクラスのインスタンスを使用することができます。

DIの勘所

 上の例では、「Userクラスのfullname()メソッドを取り出して、UserSpecプロトコルにまとめただけ」にしか見えません。しかし、ここでは、発想を逆にしてみます。つまり、
Presenterは設計図であるUserSpecを作った。下請けであるUserは、その設計図に合わせた実装を作った。
という発想です。こうすることで、PresenterUserの間に主従関係が見えてきます。
 今回は、既存のクラスをDI対応にするため、下請けクラスの関数をプロトコルとして取り出しましたが、今後新しくクラスを作るときは、下請けが適合すべきプロトコルを先に作り、下請けクラスがそれを実装する、という流れを作ればいいのです。

クリーンアーキテクチャーへの適用

 最初に出てきたクリーンアーキテクチャーは、外側の円は内側の円のことを知っているが、内側の円は外側を知らない、というのが原則です。ですから、内側の円が外側にアクセスしたい時に、DIを使って「下請けに出す」を実装すれば良いわけです。
 上の例では、画面に文字を表示するPresenterがデータを保持するUserを下請けにしていましたが、クリーンアーキテクチャー的には主従関係が逆になります。そこで、このように変更しました。

protocol PresenterSpec {
    func nameChanged()
}

class User {
    var presenter: PresenterSpec!

    var firstName = "Taro" {
        didSet {
            presenter.nameChanged()
        }
    }
    var familyName = "Yamada" {
        didSet {
            presenter.nameChanged()
        }
    }
    func fullname() -> String {
        return firstName + " " + familyName
    }
}

class Presenter: PresenterSpec {

    func nameChanged() {
        print(user.fullname())
    }
}

let presenter = Presenter()
let user = User()
user.presenter = presenter
user.familyName = "Kawada"

 若干恣意的ではありますが、PresenterSpecに定義している関数は、名前が変わったことを通知するnameChanged()関数であり、PresenterSpecに適合したクラスがどんな処理をするのかUserクラスは知りません(firstNameだけ表示する処理かもしれませんし、ひょっとしたら、表示をせずにデータベースに保存するという処理かもしれません)。
 これで、Userは外側の円のことを一切知ることなく、firstNameまたはfamilyNameが変更されたことだけを通知し、Presenterfullnameを表示するようになります。
 さらに、中から外、外から中の両方のアクセスにDIを使っても良いでしょう。

protocol PresenterSpec: class {
    func nameChanged()
}

class User: UserSpec {
    weak var presenter: PresenterSpec!

    var firstName = "Taro" {
        didSet {
            presenter.nameChanged()
        }
    }
    var familyName = "Yamada" {
        didSet {
            presenter.nameChanged()
        }
    }
    func fullname() -> String {
        return firstName + " " + familyName
    }
}

protocol UserSpec {
    func fullname() -> String
}

class Presenter: PresenterSpec {
    var user: UserSpec!

    func nameChanged() {
        print(user.fullname())
    }
}

let presenter = Presenter()
let user = User()
user.presenter = presenter
presenter.user = user
user.familyName = "Kawada"

 こうすると、主従関係がわかりにくい気がしますが、「データの管理はUserが主、表示に関してはPresenterが主」と考えれば良いと思います。

ファイルの構成

 実際のプログラミングでは、すべてのクラスやプロトコルを一つのファイルに収めることはしないので、先ほどの例をファイル単位に分割します。User.swiftはこうなりました。

protocol PresenterSpec: class {
    func nameChanged()
}

class User {
    weak var presenter: PresenterSpec!

    var firstName = "Taro" {
        didSet {
            presenter.nameChanged()
        }
    }
    var familyName = "Yamada" {
        didSet {
            presenter.nameChanged()
        }
    }
}

extension User: UserSpec {

    func fullname() -> String {
        return firstName + " " + familyName
    }

}

 User.swiftファイルには、Userクラス本体と、Userが欲しているPresenterSpecプロトコル、そして、Presenterクラスから要求されているUserSpecプロトコルの実装を記載します。UserSpecプロトコルはPresenterが必要としているものであるので、プロトコルの定義そのものをUserクラスと同じファイルには記載しません。
 ファイルサイズが大きくなるにつれ、この3つのブロックも別々のファイルに書くことになるかと思いますが、UserクラスとUserSpecプロトコルの定義は別々であるべき、ということは間違いないでしょう。

余禄

 ここで挙げた例は、DIの説明といいつつも、実はプロトコルの説明にすぎません。DIが役に立つのは、ユニットテストの時だけだ、という人もいるそうです。どういった時にDIが役に立つのか、もう少し勉強したいと思います。
 そう言えば、現在iOS, macOSで使用しているCocoaフレームワークはObjective-C時代に作られたものであり、随所にSwiftの考え方とそぐわない部分があります。Swiftのバージョンアップに伴い少しずつ改変が入っていますが、SwiftのABIの安定化が図られたら、次は、Cocoaフレームワークの大規模な改変が始まる、という可能性もあるかもしれません。クリーンアーキテクチャーなどを使って、できるだけフレームワークへの依存度を減らしておくと良いかもしれませんね。

31
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?