Swiftでの依存性注入の確認

画像







Dependency Injectionを使用したプロジェクトはクリスマスガーランドに似ています-それは美しく、子供と大人を喜ばせます。 ただし、依存関係をどこかに挿入しないと、アプリケーションセグメント全体が切断されます。 問題の原因を見つけるには、このセグメントのすべての依存関係を確認する必要があります。







この記事では、空の依存関係を見つけるためのいくつかのオプションについて説明します。 そして、私たちのリポジトリには、これを支援する小さなライブラリがあります: TinkoffCreditSystems / InjectionsCheck







Initでの依存性注入



すべての依存関係を非オプションとして宣言し、それらをInitに挿入することが最も信頼できる方法です。







class TheObject: IObjectProtocol { var service: IService init( _ service: IService) { self.service = service } }
      
      





長所:









短所:









テストおよびDIコンテナの依存関係チェック



テストを使用して、すべての依存関係が整っているかどうかを確認できます。 しかし、それらを追跡することはオブジェクトを追跡するよりも難しいため、そのようなテストを最新の状態に保つ簡単な方法が必要です。 これは、関数がオブジェクトのすべてのオプションプロパティをチェックし、nilが見つかった場合にエラーを報告するのに役立ちます。







テストが空の依存関係を見逃さないようにするには、除外リストの依存関係を除くすべてをチェックするようテストに強制する必要があります。 チェックするプロパティのリストよりも例外リストを維持する方が簡単です。 例外が登録されていない場合、テストは失敗し、これが顕著になります。







長所:









短所:









Swiftでは、この関数はリフレクションとMirrorクラスを使用して作成できます。 すべてのプロパティをバイパスして、それらの値を確認できます。







 func checkInjections(of object: Any) { print(“Properties of \(String(reflecting: object))”) for child in Mirror(reflecting: object).children { print(“\t\(child.label) = \(child.value)”) } }
      
      





label: String



-プロパティ名、

value: Any



は、nilの可能性がある値です。







しかし、厄介な点が1つあります。







 if child.value == nil { //   }
      
      





SwiftはAnyとnilを比較しません。 「Any」と「Any?」はタイプが異なるため、正式に彼は正しい。 したがって、ミラークラスを再度使用して、Anyの最後のOptionalを見つけ、nilかどうかを調べる必要があります。







 fileprivate func unwrap<ObjectType: Any>(_ object: ObjectType) -> ObjectType? { let mirror = Mirror(reflecting: object) guard mirror.displayStyle == .optional else { return object } guard let child = mirror.children.first else { return nil } return unwrap(any: child.value) as? ObjectType }
      
      





AnyはネストされたOptional(たとえば、IService ??。)を持つことができるため、再帰が使用されます。この関数の結果は、nilと比較できる通常のOptionalを生成します。







プロパティ除外リスト



一部のオブジェクトでは、すべてのプロパティをnilでチェックする必要があるわけではないため、例外プロパティのリストを追加します。 child.labelは文字列であるため、例外は次のようにのみ設定できます。









Swift 4 KeyPathを使用できない理由

Swift 4 KeyPathを使用すると、キーではなく文字列ではなくオブジェクトで値を取得できます。 現在、プロパティ名を文字列として取得することはできません。 また、すべてのKeyPathの完全なリストを取得することはできません。







SelectorNameプロトコルを使用して、型キャストなしのすべてのオプションと列挙型をサポートします。







 protocol SelectorName { var value: String } class TheObject: IObjectProtocol { var notService: INotService? enum SelectorsToIgnore: String, SelectorName { case notService } }
      
      





String、Selector、およびRawRepresentableのSelectorNameプロトコルのサポート
 extension String: SelectorName { var value: String { return self } } extension Selectoe: SelectorName { var value: String { return String(describing: self) } } extension SelectorName where Self: RawRepresentable, RawType == String { var valur: String { return self.rawValue } }
      
      





依存性注入をチェックするための最終的な関数コードは次のようになります。







func checkInjections
 enum InjectionCheckError: Error { case notInjected(properties: [String], in: Any) } public func checkInjections<ObjectType>( _ object: ObjectType, ignoring selectorsToIgnore: [SelectorName] = [] ) throws -> ObjectType { let selectorsSet = Set<String>(selectorsToIgnore.flatMap { $0.stringValue } ) let mirror = Mirror(reflecting: object) var uninjectedProperties: [String] = [] for child in mirror.children { guard let label = child.label, !selectorsSet.contains(label), unwrap(child.value) == nil else { continue } uninjectedProperties.append(label) } guard uninjectedProperties.count == 0 else { let error = InjectionCheckError.notInjected(properties: uninjectedProperties, in: object) throw error } return object }
      
      





強制的なアンラップ依存関係



依存関係は、強制的にラップ解除されると宣言できます。







 class TheObject: IObjectProtocol { var service: IService! }
      
      





長所:









短所:









アプリケーションがクラッシュすると、ユーザーエクスペリエンスが低下します。 花輪の例のように、燃え尽きると明るく爆発する電球が手に入ります。 ほとんどの場合、そのような空の依存関係は、あまり重要ではない、またはめったに使用されない関数を担当し、機能が制限されているにもかかわらずアプリケーションが機能する場合、開発者とテスターに​​よって渡されます。







たとえば、注文のリストに電話番号フォーマッターがない場合、アプリケーションは機能し、残りの情報を表示し、クラッシュしません。 そして、もちろん、それは問題について開発者に通知します。







DIコンテナの終了時にチェック+デバッグモードでクラッシュ



Swiftには条件付きコンパイルがあり、デバッグモードでのみラップされていない依存関係の哲学を使用できます。 コンパイラ条件を使用すると、空の依存関係が見つかった場合に、デバッグモードでfatalErrorを引き起こす関数を作成できます。 また、サービスロケーター、アセンブリ、または工場を終了するときに使用すると便利です。







長所:









短所:









このラッパー関数は、オブジェクトの依存関係をチェックし、コンパイラに-DDEBUGまたは-DINJECTION_CHECK_ENABLEDフラグが設定されている場合、それを削除します。 残りのケースでは、彼は静かにログに書き込みます:







 @discardableResult public func debugCheckInjections<ObjectType>( _ object: ObjectType, ignoring selectorsToIgnore: [IgnorableSelector] = [], errorClosure: (_ error: Error) -> Void = { fatalError("Injection check error: \($0)") }) -> ObjectType? { do { let object = try checkInjections(object, ignoring: selectorsToIgnore) return object } catch { #if DEBUG || INJECTION_CHECK_ENABLED errorClosure(error) #else print("Injection check error: \(error)") #endif return nil } }
      
      





結論の代わりに



すべてのメソッドには長所と短所があります。 プロジェクトに応じて、チーム、確立された仕事の原則、何らかの方法でうまくいきます。







ストーリーボードから作成された依存関係をViewControllerに挿入することはできないため、Initを介して依存関係を挿入しないことを好みます。 そして、これはリファクタリングと変更を複雑にします。







Forced Unwrapは、依存関係の管理に使用されるだけでなく、一般的な運用環境でも使用されます。 SwiftLintは 'force_unwrapping'ルールも有効にして、使用されないようにします。







テストでの依存関係のテストは、古いコードをサポートするのにうまく機能します。 古い依存関係が失われないようにし、すべてが引き続き機能するようにします。 しかし、現在の開発には不便です。







したがって、デバッグモードでクラッシュしたDIコンテナからの終了を確認することをお勧めします。 最速の方法は、空の依存関係を検出してすぐに修正することです。







ここに示されているすべての機能はライブラリにあります。

TinkoffCreditSystems / InjectionsCheck








All Articles