Dependency Injectionを使用したプロジェクトはクリスマスガーランドに似ています-それは美しく、子供と大人を喜ばせます。 ただし、依存関係をどこかに挿入しないと、アプリケーションセグメント全体が切断されます。 問題の原因を見つけるには、このセグメントのすべての依存関係を確認する必要があります。
この記事では、空の依存関係を見つけるためのいくつかのオプションについて説明します。 そして、私たちのリポジトリには、これを支援する小さなライブラリがあります: TinkoffCreditSystems / InjectionsCheck
Initでの依存性注入
すべての依存関係を非オプションとして宣言し、それらをInitに挿入することが最も信頼できる方法です。
class TheObject: IObjectProtocol { var service: IService init( _ service: IService) { self.service = service } }
長所:
- プロジェクトを初期化するのを忘れた場合、プロジェクトは組み立てられません。
- コンパイラは、何をどこで忘れたかを示します。
短所:
- 循環依存関係を使用できません。
- Initの多くのパラメーター。
- ViewControllerおよび同様のオブジェクトに依存関係を挿入することはできません。
テストおよびDIコンテナの依存関係チェック
テストを使用して、すべての依存関係が整っているかどうかを確認できます。 しかし、それらを追跡することはオブジェクトを追跡するよりも難しいため、そのようなテストを最新の状態に保つ簡単な方法が必要です。 これは、関数がオブジェクトのすべてのオプションプロパティをチェックし、nilが見つかった場合にエラーを報告するのに役立ちます。
テストが空の依存関係を見逃さないようにするには、除外リストの依存関係を除くすべてをチェックするようテストに強制する必要があります。 チェックするプロパティのリストよりも例外リストを維持する方が簡単です。 例外が登録されていない場合、テストは失敗し、これが顕著になります。
長所:
- 依存性注入の自由度が増えました。
- プロトコルに関する依存関係を実装できます。
短所:
- テストを実行して、何も忘れていないことを確認する必要があります。
- スキャンされたオブジェクトはすべてテスト中でなければなりません。
- 依存関係の場合、
guard
またはservice?.doSomething
を使用する必要があります。
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を使用すると、キーではなく文字列ではなくオブジェクトで値を取得できます。 現在、プロパティ名を文字列として取得することはできません。 また、すべてのKeyPathの完全なリストを取得することはできません。
SelectorNameプロトコルを使用して、型キャストなしのすべてのオプションと列挙型をサポートします。
protocol SelectorName { var value: String } class TheObject: IObjectProtocol { var notService: INotService? enum SelectorsToIgnore: String, SelectorName { case notService } }
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 } }
依存性注入をチェックするための最終的な関数コードは次のようになります。
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! }
長所:
- nilの場合、アプリケーションはすぐにクラッシュします。
- エラーログは、何がどこにあったかを示します。
- 依存関係を使用すると便利です。
短所:
- アプリケーションがクラッシュします。
- 本番環境でクラッシュするコードをスキップできます。
- アプリケーションでは、空の依存関係を持つ場所を見つけて、それが落ちるようにする必要があります。
アプリケーションがクラッシュすると、ユーザーエクスペリエンスが低下します。 花輪の例のように、燃え尽きると明るく爆発する電球が手に入ります。 ほとんどの場合、そのような空の依存関係は、あまり重要ではない、またはめったに使用されない関数を担当し、機能が制限されているにもかかわらずアプリケーションが機能する場合、開発者とテスターによって渡されます。
たとえば、注文のリストに電話番号フォーマッターがない場合、アプリケーションは機能し、残りの情報を表示し、クラッシュしません。 そして、もちろん、それは問題について開発者に通知します。
DIコンテナの終了時にチェック+デバッグモードでクラッシュ
Swiftには条件付きコンパイルがあり、デバッグモードでのみラップされていない依存関係の哲学を使用できます。 コンパイラ条件を使用すると、空の依存関係が見つかった場合に、デバッグモードでfatalErrorを引き起こす関数を作成できます。 また、サービスロケーター、アセンブリ、または工場を終了するときに使用すると便利です。
長所:
- テストを実行せずに動作します。
- アプリケーションはデバッグモードでのみクラッシュします。
- エラーログには、何がどこでnilであったかが示されます。
- プロジェクトへの埋め込みは簡単です。
短所:
- すべてのスキャンされたオブジェクトは、機能を通過する必要があります。
- 依存関係の場合、
guard
またはservice?.doSomething
を使用する必要があります。 - リリースビルドのテストでは、ログによってのみエラーを検出できます。
このラッパー関数は、オブジェクトの依存関係をチェックし、コンパイラに-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