IOS反転

画像







Evgeny Yolchev rsi 、iOSチームのリードKODE







最近、私はDIについてますます耳にしました。 彼はギーク大学の私の学生に興味があり、チャットルームで言及されています。 このパターンは若くはありませんが、多くの人はそれを正しく理解していません。

多くの場合、DIは台風やswinjectなどのフレームワークを指します。 この記事では、DI実装の原則とIoCの原則を詳細に分析します。 興味があれば、猫の下でお願いします。







DI(依存性注入)は、ソフトウェアコンポーネントに外部依存性を提供するプロセスです。 依存関係管理に適用される場合、「IoC」の特定の形式です。 唯一の責任の原則に完全に従って、オブジェクトは、この一般的なメカニズムのために特別に設計された外部に必要な依存関係を構築することに注意を払います。


IoC(Inversion of Control)は、コンピュータープログラムのリンク(凝集)を減らすために使用されるオブジェクト指向プログラミングの重要な原則です。

この記事はDIに関するものですが、DIについては説明しませんが、DIはIoCの種類の1つにすぎず、全体像を確認する必要があるため、IoCを使用します。







IoC



まず、管理とは何かを把握しましょう。 最も単純な例であるコンソール「Hello world」をご覧ください。







let firstWord = «hello» let secondWord = "world!" let phrase = firstWord + " " + secondWord print(phrase)
      
      





この例では、コマンドは文字列リテラルと変数で表されるデータを操作します。 この抽象化レベルではこれ以上制御できませんが、三項演算子を使用して追加できます。







 let number = arc4random_uniform(1) let firstWord = number == 0 ? "hello" : "bye" let secondWord = "world!" let phrase = firstWord + " " + secondWord print(phrase)
      
      





コードはあいまいになり、乱数に応じてコンソールの行が変わります。 言い換えれば、データがプログラムを駆動します。 これは、制御反転の最も一般的で最も単純な例です。







典型的なiOSアプリケーションでは、制御はどこにでもあります。 システム、ユーザー、サーバーがアプリケーションを制御します。 アプリケーションは、サーバー、ユーザー、およびシステムを管理します。 コードには、相互に制御する膨大な数のオブジェクトが含まれています。 たとえば、 AuthViewControllerクラスのオブジェクトは、 AuthServiceクラスのオブジェクトを制御できます。







このような施設管理は、いくつかの側面から構築されます。 最初に、 AuthViewControllerAuthServiceメソッドを呼び出し、次に、それを作成します。 これはすべて、オブジェクトの高い接続性につながります; AuthViewControllerの使用はAuthServiceなしでは不可能になります。 これは依存関係と呼ばれ、 AuthViewControllerはAuthServiceに完全に依存しています







このような依存関係に問題はないと考えられています。 原則として、コントローラーは再利用されず、常にアプリケーションをサポートするサービスと連携します。 しかし、長寿命のアプリケーションをサポートしてきた人々は、これがそうではないことを知っています。 要件は常に変化しています。バグを見つけ、フローを変更し、再設計を行います。 同時に、URLSessionの単なるラッパーであるいくつかのボタンとサービスを備えた複数のコントローラーよりもアプリケーションが複雑な場合、依存関係にinれています。 クラス間の依存関係はWebを形成し、循環的な依存関係を見つけることができます。 クラスを変更することはできません。クラスの使用方法と使用場所が明確ではないため、古いメソッドを変更するよりも新しいメソッドを作成する方が簡単です。 クラスを置き換えると痛みに変わります。 コンストラクターの呼び出しはさまざまなメソッドによって分散されており、これらのメソッドも変更する必要があります。 最終的に、何が起こっているのか理解できなくなり、コードはプレーンテキストに変わり、検索機能を使用して、このテキスト内の単語または文を置き換え、コンパイラエラーのみをチェックし始めます。







イベントのこの結果を防ぐために、多くの原則と技術が発明されました。 たとえば、SOLIDの原則の1つであるDIPの原則では、メソッドを呼び出すときに接続を減らす方法を説明しています。これはIoCです。







DIP(Dependency Inversion Principle)は、5つのSOLID原則の1つです。



言葉遣い:



上位モジュールは下位モジュールに依存しないでください。 どちらのタイプのモジュールも抽象化に依存する必要があります。



抽象化は詳細に依存すべきではありません。 詳細は抽象化に依存する必要があります。

しかし、それでも、誰かが「 IoC 」と言うとき、依存関係を作成するときの制御の反転を意味します。 さらに、この意味でのみ使用します。 ちなみに、DIPはIoCなしで実装することはほとんど不可能ですが、その逆は不可能です。 IoCを使用しても、DIPへの準拠は保証されません。 別の重要なニュアンス。 DIPとDIは2つの異なる原則です。







IoCへの途中



実際、IoCは非常に単純な概念であり、多くの文献を読む必要はありません。数年間チベットに行ってZenを理解し、使い始めることができます。







例として、「騎士」( Knight )とその「鎧」( Armor )のクラスを検討します。すべてのクラスを以下に示します。

画像

それでは、 Armorクラスの実装を見てみましょう。







 class Armor { private var boots: Boots? private var pants: Pants? private var belt: Belt? private var chest: hest? private var bracers: Bracers? private var gloves: Gloves? private var helmet: Helmet? func configure() { self.boots = Boots() self.pants = Pants() self.belt = Belt() self.chest = hest() self.bracers = Bracers() self.gloves = Gloves() self.helmet = Helmet() } }
      
      





騎士







 class Knight { private var armor: Armor? func prepareForBattle() { self.armor = Armor() self.armor.configure() } }
      
      





一見、すべてが正常です。 ナイトが必要な場合は、作成します。







 let knight = Knight()
      
      





しかし、それほど単純ではありません。 残念ながら、代理の例では、このアプローチがもたらすすべての苦痛を伝えることはできません。







クラスは互いにはんだ付けされています。 Armor



make



メソッドで7つのクラスを作成します。 これにより、クラスが骨化されます。 このアプローチでは、クラスを作成する場所と方法を決定することはできません。 アーマーから継承して、たとえば儀式用のアーマーを作成してヘルメットを置き換える必要がある場合は、メソッド全体を再定義する必要があります。







このアプローチの唯一のプラスは、クラスを作成するときに将来について考える必要がないため、コードの記述速度です。







これが人生でどのように見えるかの小さな例です:







 class FightViewController: BaseViewController { var titleLabel: UIView! var knightList: UIView! override func viewDidLoad() { super.viewDidLoad() self.title = "" //       ,       //   let backgroundView = UIView() //    self.view.addSubview(backgroundView) //    backgroundView.backgroundColor = UIColor.red //   backgroundView.translatesAutoresizingMaskIntoConstraints = false backgroundView.translatesAutoresizingMaskIntoConstraints = false backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true backgroundView.topAnchor.constraint(equalTo: topAnchor).isActive = true backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true let title = Views.BigHeader.View() self.titleLabel = title title.labelView.text = "labelView" self.view.addSubview(title) title.translatesAutoresizingMaskIntoConstraints = false title.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true title.topAnchor.constraint(equalTo: topAnchor).isActive = true title.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true title.heightAnchor.constraint(equalToConstant: 56).isActive = true let knightList = Views.DataView.View() self.knightList = knightList knightList.titleView.text = "knightList" knightList.dataView.text = "" self.view.addSubview(knightList) knightList.translatesAutoresizingMaskIntoConstraints = false knightList.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true knightList.topAnchor.constraint(equalTo: title.topAnchor).isActive = true knightList.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true knightList.heightAnchor.constraint(equalToConstant: 45).isActive = true } }
      
      





このようなコードは、他の人のプロジェクトで簡単に見つけることができます。 彼は、任意の場所に依存関係クラスを作成するのは良い考えではないことを完全に示しています。 さらに、鎧とは異なり、ここの要素は作成されるだけでなく、構成され、配置されます。 コードは混乱に変わりました。







これはどのように改善できますか? 「工場メソッド」パターンを使用します。 すべての問題を解決するわけではありませんが、クラスをより柔軟にします。







仮想メソッドとも呼ばれるファクトリメソッドは、クラスをインスタンス化するためのインターフェイスをサブクラスに提供する一般的なデザインパターンです。



 class Armor { private var boots: Boots? private var pants: Pants? func configure() { self.boots = makeBoots() self.pants = makePants() } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





依存関係の作成は別の方法で行われます。 クラスロジックに損傷を与えることなく、簡単に見つけて変更できます。 継承では、それらを再定義して、依存関係を再定義できます。







それでも、クラスは依存関係の作成の詳細を知る必要はなく、それらを使用するだけです。 これに対処する方法は? 生成ロジックをクラスからより高いレベルに引き上げる必要があります。







生成ロジックは、クラスまたは構造のインスタンスを作成するコードです。 つまり、オブジェクトを生成するコード。



 class Armor { private var boots: Boots? private var pants: Pants? func configure(boots: Boots?, pants: Pants?) { self.boots = boots self.pants = pants } }
      
      





現在、 Armor



クラスには、依存関係がどのように作成されるかがわかりません。それらは単に引数として渡されます。 これにより、最大限の柔軟性が得られます。 クラスをプロトコルに置き換え、実装の詳細を完全に無視することもできます。







しかし、私たちのKnight



クラスではうまくいきません。







 class Knight { private var armor: Armor? func preapreForBattle() { self.armor = Armor() let boots = makeBoots() let pants = makePants() self.armor?.make(boots: boots, pants: pants) } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





彼は彼の鎧のすべての部分を作成します。 私たちの騎士は彼自身の鍛冶屋であると言えます。

これは間違っています。ナイトはタスクレベルではなく、装甲を偽造するべきではありません。 再び生成ロジックをより高いレベルに引き上げることができますが、グラフの上部にあるクラスは依存関係を作成するための巨大なダンプになります。







別の生成パターンが救助に来るでしょう-「工場」。







ファクトリー(英語のファクトリー)-他のオブジェクトを作成するオブジェクト。

装甲の一部を作成して1つのセットにまとめるフォージを構築します。







 class Forge { func makeArmor() -> Armor { let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





Armor



クラスとKnight



クラスは生成ロジックを取り除き、簡潔に見えます。







 class Armor { var boots: Boots? var pants: Pants? } class Knight { var armor: Armor? }
      
      





ここで、「工場」から依存関係をどのように、どこで、いつ取得してクラスに転送するかという問題に直面しています。 そして、それはついにDIとSLの概念に到達したことを意味します。







サービスロケーター(SL)



このパターンから始めましょう。 まず、よりシンプルです。 第二に、多くの人々はこれがDIであると考えていますが、そうではありません。







SL(サービスロケーター)は、強力な抽象化レベルでサービスを取得することに関連するプロセスをカプセル化するためのソフトウェアの開発で使用される設計パターンです。 このテンプレートは、「サービスロケーター」と呼ばれる中央レジストリを使用します。このレジストリは、要求に応じて、特定のタスクを完了するために必要な情報(通常はオブジェクト)を返します。

その本質は何ですか? 依存関係を取得するために、クラスにはコンストラクタで「ファクトリ」が渡され、そこから取得するものが選択されます。







この場合、クラスは次のようになります。







 class Forge { func makeArmor() -> Armor { let armor = Armor(forge: self) return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





 class Knight { private let forge: Forge private var armor: Armor? init(forge: Forge) { self.forge = forge configure() } private func configure() { armor = forge.makeArmor() } }
      
      





 class Armor { private let forge: Forge private var boots: Boots? private var pants: Pants? init(forge: Forge) { self.forge = forge configure() } private func configure() { boots = forge.makeBoots() pants = forge.makePants() } }
      
      





 let forge = Forge() let knight = Knight(forge: forge)
      
      





個人的に、このアプローチは二重の感覚を引き起こします。 一方では、生成ロジックは「工場」にあり、他方では、依存関係を取得するプロセスは多少混乱しています。 しかし、主な欠点は、クラスを見て、その依存関係を明確に決定することが不可能であることです。 彼は「工場」から何でも得ることができます;典型的な開発の間違いは、アプリケーション全体に対してそのような「工場」を作成することです。 同時に、「工場」はジャンクの巨大なダンプに変わり、彼らが本当に必要としないものの中に入ろうとする誘惑を引き起こします。 クラスは連絡、制限を失います。







私たちの騎士には、必要な鎧を手に入れることができる宝箱が提供されたと想像できますが、付属物では誰も不必要な宝石の収集を止めることはできません。

このため、このパターンは善悪の境界を越えて反パターンになりました。 DIとSLを選択できる場合は、常にDIを選択してください。







DI



クラスに依存関係を提供する2番目の方法はDIです。 これは現在最も一般的なパターンです。 それは非常に人気があり、バックエンドの世界では、すべての通常のフレームワークがすぐにサポートしています。 残念ながら、私たちはそれほど幸運ではありませんでした。







このパターンの本質は、依存関係が外部からクラスに埋め込まれ、依存関係グラフがDIコンテナー内に構築されることです。DIコンテナーは「ファクトリー」または「ファクトリー」のセットです。







同時にクラスは次のようになります。







 class Armor { var boots: Boots? var pants: Pants? } class Knight { var armor: Armor? } class Forge { func makeArmor() -> Armor { let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





 class Garrison { lazy var forge: Forge = { return Forge() }() func makeKnight() -> Knight { let knight = Knight() knight.armor = forge.makeArmor() return knight } }
      
      





 let garrison = Garrison() let knight = garrison.makeKnight()
      
      





この場合、クラスはきれいに見えますが、生成ロジックは完全に欠けています。 アセンブリ全体の責任は、 Garrison



Forge



という2つの「工場」が引き受けました。 必要に応じて、クラスの成長を防ぐために、これらの「工場」の数を増やすことができます。 あらゆる種類の関連オブジェクトを作成する「工場」を作成することをお勧めします。 たとえば、この「工場」はサービス、特定のユーザーストーリーのコントローラーを作成できます。







同時に、私たちの騎士はついに自分の地位にふさわしくないことをやり終え、従者は弾薬を担当し、騎士は戦いと王女に集中することができます。

これは完了する可能性がありますが、DIのいくつかの側面と現在利用可能なフレームワークについて話す価値があります。







DIタイプ



初期化インジェクター-コンストラクターによる依存性インジェクション。 このアプローチは、クラスが依存関係なしでは存在できない場合に使用されますが、存在しない場合でも、クラスコントラクトをより明確に定義するために使用できます。 すべての依存関係がコンストラクター引数として宣言されている場合、それらの定義は簡単です。 しかし、夢中にならないでください。クラスに10個の依存関係がある場合は、コンストラクターでそれらを渡さないことをお勧めします(または、クラスに非常に多くの依存関係がある理由を見つけてください)。







 class Armor { let boots: Boots let pants: Pants init(boots: Boots, pants: Pants) { self.boots = boots self.pants = pants } } class Forge { func makeArmor() -> Armor { let boots = makeBoots() let pants = makePants() let armor = Armor(boots: boots, pants: pants) return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





プロパティ注入-プロパティを介した依存性注入。 このメソッドは、クラスにオプションの依存関係があり、それがなくても実行できる場合、または依存関係がオブジェクトの初期化段階だけでなく変更できる場合に使用されます。







 class Armor { var boots: Boots? var pants: Pants? } class Forge { func makeArmor() -> Armor { let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





メソッド注入-メソッドを介した依存性注入。 このメソッドは、プロパティインジェクションに非常に似ていますが、アクションの実行時にのみ時間依存関係を実装したり、依存関係の実装をクラスロジックとより密接に関連付けるために使用できます。







 class Knight { private var armor: Armor? func winTournament(armor: Armor) { self.armor = armor defeatEnemy() seducePrincess() self.armor = nil } func defeatEnemy() {} func seducePrincess() {} } class Garrison { lazy var forge: Forge = { return Forge() }() func makeKnight() -> Knight { let knight = Knight() return knight } } let garrison = Garrison() let knight = garrison.makeKnight() let armor = garrison.forge.makeArmor() knight.winTournament(armor: armor)
      
      





私の観察によると、最も一般的なタイプは初期化インジェクションとプロパティインジェクションであり、あまり一般的ではないメソッドインジェクションです。 また、1つのタイプを選択する典型的なケースについて説明しましたが、Swiftは非常に柔軟な言語であり、タイプを選択するためのオプションが豊富であることを忘れてはなりません。 そのため、たとえば、オプションの依存関係がある場合でも、オプションの引数とデフォルトでnilを使用してコンストラクターを実装できます。 この場合、プロパティインジェクションの代わりに初期化インジェクションを使用できます。 いずれにせよ、それはあなたのコードを改善または低下させる妥協であり、選択はあなた次第です。







ディップ



上記の例のように、IoCを簡単に使用すること自体が良い成果をもたらしますが、さらに進んで、SOLIDのDIP原則への準拠を達成することができます。 これを行うには、プロトコルとの依存関係を閉じ、「プロトコル」の背後にある実装が正確に何であるかを「工場」だけが知ります。







 class Knight { var armor: AbstractArmor? } class Forge { func makeArmor() -> AbstractArmor { let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





この場合、予約の実装を代替品に簡単に置き換えることができます。







SOLIDはこの記事の範囲を超えていますが、それが何であるかわからない場合は、この一連の原則を理解することをお勧めします。 良い入門記事から始めて、 この本の関連する章を読み続けてください







スコープ



オブジェクトのスコープを管理すること自体はIoCコンセプトの一部ではなく、むしろ実装の詳細ですが、それでもシングルトーンを放棄し、一般的な依存関係を持つ他の問題を解決できる非常に強力なメカニズムです。 スコープは、「ファクトリ」内で作成された依存関係が存続する期間を決定します。依存関係は、毎回作成されるか、最初の作成後に保存され、参照によって単に渡されます。







スコープはパターンに記述されていないため、それぞれが適切と思われるように実装し、名前を付けます。 最も一般的に使用される2つのタイプを見ていきます。







標準スコープは、上記のすべての例で実装した動作です。 「ファクトリ」はオブジェクトを作成し、それを渡し、その存在を忘れます。 ファクトリメソッドが再度呼び出されると、新しいオブジェクトが作成されます。







コンテナのスコープは、シングルトンに似た動作です。 ファクトリメソッドが最初に呼び出されると、新しいオブジェクトが作成され、「ファクトリ」はその参照を保存し、ファクトリメソッドの結果としてそれを返します。他のすべてのメソッド呼び出しでは、新しいオブジェクトは作成されませんが、最初のオブジェクトへの参照が返されます。







 class Forge { private var armor: AbstractArmor? func makeArmor() -> AbstractArmor { //        if let armor = self.armor { return armor } let armor = Armor() armor.boots = makeBoots() armor.pants = makePants() self.armor = armor return armor } func makeBoots() -> Boots { return Boots() } func makePants() -> Pants { return Pants() } }
      
      





ご覧のとおり、上記の例では、鎧は一度だけ作成され、他のすべての場合では、以前に作成されたインスタンスが返されます。 シングルトンと同様に、グローバルスコープなしで常に同じクラスインスタンスを使用します。







長所と短所



プログラミングの他の原則と同様に、IoCは特効薬ではなく、次のような利点があります。









そして、短所:









私の意見では、メインの唯一のマイナスは、DIPの原則を厳密に守りたいという思い切った欲望の結果として過剰に設計されているということです。 , , , .







, , . , ? , ? ? , ?







まとめると



, IoC , , . iOS-, , android- DI, dagger, . , , spring . php-, , , Laravel DI . iOS, , , , . Objective-C , swift.







, . , IoC — , , , , DI. IoC , DI SL, «», . «» DI .








All Articles