Swift Language
依存性注入
サーチ…
ビューコントローラによる依存性注入
Dependenct注射イントロ
アプリケーションは、お互いに協力する多くのオブジェクトで構成されています。オブジェクトは通常、他のオブジェクトに依存して何らかのタスクを実行します。オブジェクトがそれ自身の依存関係を参照する責任を負う場合、オブジェクトは結びつきが強く、テストが難しく、変更が難しいコードにつながります。
依存性注入は、依存性を解決するための制御の逆転を実装するソフトウェア設計パターンです。インジェクションはそれを使用する従属オブジェクトに依存関係を渡すことです。これにより、クライアントの依存関係をクライアントの動作から分離することができ、アプリケーションを疎結合させることができます。
上記の定義と混同しないでください。依存性注入とは、オブジェクトにインスタンス変数を渡すことだけです。
それは単純ですが、それは多くの利点を提供します:
- コードのテストが簡単(ユニットテストやUIテストなどの自動テストを使用)
- プロトコル指向プログラミングと組み合わせて使用すると、特定のクラスの実装を簡単に変更できるようになります。リファクタリングが容易です
- コードをモジュール化して再利用可能にする
依存性注入(DI)をアプリケーションに実装するには、最も一般的に使用される3つの方法があります。
- イニシャル注入
- 特性注入
- サードパーティのDIフレームワーク(Swinject、Cleanse、Dip、Typhoonなど)を使用すると、
興味深い記事があります。依存性注入に関する記事へのリンクがありますので、DIとInversion of Controlの原則を詳しく調べてください。
View ControllerでDIを使用する方法を紹介しましょう。平均的なiOSデベロッパーのための毎日のタスクです。
DIなしの例
2つのView Controller、 LoginViewControllerとTimelineViewControllerがあります 。 LoginViewControllerはログインに使用され、成功するとTimelineViewControllerに切り替わります。両方のView ControllerはFirebaseNetworkServiceに依存しています。
LoginViewController
class LoginViewController: UIViewController {
var networkService = FirebaseNetworkService()
override func viewDidLoad() {
super.viewDidLoad()
}
}
TimelineViewController
class TimelineViewController: UIViewController {
var networkService = FirebaseNetworkService()
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func logoutButtonPressed(_ sender: UIButton) {
networkService.logutCurrentUser()
}
}
FirebaseNetworkService
class FirebaseNetworkService {
func loginUser(username: String, passwordHash: String) {
// Implementation not important for this example
}
func logutCurrentUser() {
// Implementation not important for this example
}
}
この例は非常に簡単ですが、10または15の異なるView Controllerを持ち、そのうちのいくつかがFirebaseNetworkServiceに依存しているとします。しばらくすると、社内のバックエンドサービスでFirebaseをバックエンドサービスとして変更したいと考えています。これを行うには、すべてのView Controllerを通過し、CompanyNetworkServiceでFirebaseNetworkServiceを変更する必要があります。 CompanyNetworkServiceのメソッドの一部が変更されている場合は、多くの作業が必要になります。
ユニットとUIのテストはこの例の範囲ではありませんが、密接に結合された依存関係を持つユニットテストビューコントローラを使用する場合は、そうするのが非常に難しいでしょう。
この例を書き換えて、View ControllerにNetwork Serviceを注入しましょう。
依存性注入の例
Dependency Injectionを最大限に活用するために、プロトコルでNetwork Serviceの機能を定義しましょう。このように、ネットワーク・サービスに依存するView Controllerは、実際の実装について知る必要はありません。
protocol NetworkService {
func loginUser(username: String, passwordHash: String)
func logutCurrentUser()
}
NetworkServiceプロトコルの実装を追加します。
class FirebaseNetworkServiceImpl: NetworkService {
func loginUser(username: String, passwordHash: String) {
// Firebase implementation
}
func logutCurrentUser() {
// Firebase implementation
}
}
LoginViewControllerとTimelineViewControllerをFirebaseNetworkServiceの代わりに新しいNetworkServiceプロトコルを使用するように変更しましょう。
LoginViewController
class LoginViewController: UIViewController {
// No need to initialize it here since an implementation
// of the NetworkService protocol will be injected
var networkService: NetworkService?
override func viewDidLoad() {
super.viewDidLoad()
}
}
TimelineViewController
class TimelineViewController: UIViewController {
var networkService: NetworkService?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func logoutButtonPressed(_ sender: UIButton) {
networkService?.logutCurrentUser()
}
}
さて、問題は、LoginViewControllerとTimelineViewControllerで正しいNetworkService実装をどのように注入するのですか?
LoginViewControllerは開始ビューコントローラであり、アプリケーションが起動するたびに表示されるため、すべての依存関係をAppDelegateに注入できます。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// This logic will be different based on your project's structure or whether
// you have a navigation controller or tab bar controller for your starting view controller
if let loginVC = window?.rootViewController as? LoginViewController {
loginVC.networkService = FirebaseNetworkServiceImpl()
}
return true
}
AppDelegateでは、最初のビューコントローラ(LoginViewController)への参照を取得し、プロパティ注入メソッドを使用してNetworkService実装を注入します。
次に、NetworkService実装をTimelineViewControllerに挿入します。最も簡単な方法は、LoginViewControllerがTimlineViewControllerに移行しているときに行うことです。
LoginViewControllerのprepareForSegueメソッドに注入コードを追加します(異なるアプローチを使用してView Controllerをナビゲートし、そこに注入コードを配置する場合)。
私たちのLoginViewControllerクラスは次のようになります:
class LoginViewController: UIViewController {
// No need to initialize it here since an implementation
// of the NetworkService protocol will be injected
var networkService: NetworkService?
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "TimelineViewController" {
if let timelineVC = segue.destination as? TimelineViewController {
// Injecting the NetworkService implementation
timelineVC.networkService = networkService
}
}
}
}
私たちは完了しており、それは簡単です。
今度は、FireboxのNetworkService実装をカスタム会社のバックエンド実装に切り替えることを考えてみましょう。私たちがしなければならないのは、
新しいNetworkService実装クラスを追加:
class CompanyNetworkServiceImpl: NetworkService {
func loginUser(username: String, passwordHash: String) {
// Company API implementation
}
func logutCurrentUser() {
// Company API implementation
}
}
AppDelegateの新しい実装でFirebaseNetworkServiceImplを切り替えます。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// This logic will be different based on your project's structure or whether
// you have a navigation controller or tab bar controller for your starting view controller
if let loginVC = window?.rootViewController as? LoginViewController {
loginVC.networkService = CompanyNetworkServiceImpl()
}
return true
}
これで、LoginViewControllerまたはTimelineViewControllerに触れることなく、NetworkServiceプロトコルの実装全体を切り替えました。
これは簡単な例ですから、今のところすべてのメリットを見ることはできませんが、プロジェクトでDIを使用しようとすると、メリットが表示され、常に依存性注入が使用されます。
依存性注入タイプ
この例では、SwiftでDI (Dependency Injection)デザインパターンを使用してこれらのメソッドを使用する方法を示します。
- イニシャライザインジェクション (適切な用語はコンストラクタインジェクションですが、Swiftにはイニシャライザがあるのでイニシャライザインジェクションと呼ばれます)
- プロパティインジェクション
- メソッド注入
DIなしの設定例
protocol Engine {
func startEngine()
func stopEngine()
}
class TrainEngine: Engine {
func startEngine() {
print("Engine started")
}
func stopEngine() {
print("Engine stopped")
}
}
protocol TrainCar {
var numberOfSeats: Int { get }
func attachCar(attach: Bool)
}
class RestaurantCar: TrainCar {
var numberOfSeats: Int {
get {
return 30
}
}
func attachCar(attach: Bool) {
print("Attach car")
}
}
class PassengerCar: TrainCar {
var numberOfSeats: Int {
get {
return 50
}
}
func attachCar(attach: Bool) {
print("Attach car")
}
}
class Train {
let engine: Engine?
var mainCar: TrainCar?
}
初期化子依存性注入
名前が示すように、すべての依存関係はクラス初期化子を通して注入されます。依存関係を初期化子を通して注入するために、初期化子をTrain
クラスに追加します。
列車のクラスは次のようになります:
class Train {
let engine: Engine?
var mainCar: TrainCar?
init(engine: Engine) {
self.engine = engine
}
}
Trainクラスのインスタンスを作成する場合は、イニシャライザを使用して特定のEngine実装をインジェクトします。
let train = Train(engine: TrainEngine())
注:初期化関数の注入とプロパティ注入の主な利点は、変数をプライベート変数として設定したり、 let
キーワードを使って変数を定数にすることができることです(この例のように)。このようにして、誰もそれにアクセスしたり変更することができないようにすることができます。
プロパティー依存性注入
プロパティを使用するDIは、イニシャライザを使用するより簡単です。プロパティDIを使って既に作成した列車オブジェクトにPassengerCar依存関係を挿入しましょう:
train.mainCar = PassengerCar()
それでおしまい。私たちの列車のmainCar
は現在、 PassengerCar
インスタンスです。
メソッド依存性注入
このタイプの依存性注入は、オブジェクト全体に影響を与えないため、以前の2つとは少し異なりますが、特定のメソッドのスコープ内で使用される依存性のみを注入します。依存関係が1つのメソッドでのみ使用されている場合、オブジェクト全体をそれに依存させることは通常は好ましくありません。 Trainクラスに新しいメソッドを追加しましょう:
func reparkCar(trainCar: TrainCar) {
trainCar.attachCar(attach: true)
engine?.startEngine()
engine?.stopEngine()
trainCar.attachCar(attach: false)
}
ここで、新しいTrainのクラスメソッドを呼び出すと、メソッドの依存関係注入を使用してTrainCar
が注入されます。
train.reparkCar(trainCar: RestaurantCar())