#この記事は何か
少し前に話題になったクリーンアーキテクチャーの中で出てきた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
というプロトコルが登場しました。そして、User
はUserSpec
に適合し(5)、Presenter
はUser
の代わりにUserSpec
に適合したインスタンスを所有します。これにより、user
は、User
を継承したクラスではなく、UserSpec
に適合したクラスのインスタンスを使用することができます。
DIの勘所
上の例では、「User
クラスのfullname()
メソッドを取り出して、UserSpec
プロトコルにまとめただけ」にしか見えません。しかし、ここでは、発想を逆にしてみます。つまり、
Presenter
は設計図であるUserSpec
を作った。下請けであるUser
は、その設計図に合わせた実装を作った。
という発想です。こうすることで、Presenter
とUser
の間に主従関係が見えてきます。
今回は、既存のクラスを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
が変更されたことだけを通知し、Presenter
がfullname
を表示するようになります。
さらに、中から外、外から中の両方のアクセスに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フレームワークの大規模な改変が始まる、という可能性もあるかもしれません。クリーンアーキテクチャーなどを使って、できるだけフレームワークへの依存度を減らしておくと良いかもしれませんね。