The project with Dependency Injection is similar to a Christmas tree garland - it is beautiful, it pleases children and adults. But if you do not inject a dependency somewhere, an entire segment of the application will disconnect. And, to find the source of the problems, you will have to check all the dependencies in this segment.
This article describes several options for finding empty dependencies. And in our repository there is a small library that will help with this: TinkoffCreditSystems / InjectionsCheck
To declare all dependencies as non-optional and embed them in Init is the most reliable way.
class TheObject: IObjectProtocol { var service: IService init( _ service: IService) { self.service = service } }
Pros:
Minuses:
You can use tests to check if all dependencies are in place. But it is more difficult to follow them than for objects, so you need a simple way to keep such tests up to date. A function that helps to check all the optional-properties of the object and reports an error if it finds nil will help with this.
To be sure that the tests will not miss empty dependencies, you need to force them to check everything except dependencies from the list of exceptions. It is easier to maintain an exclusion list than a list of properties that need to be checked. If the exception is not registered, the test will fail, and it will be noticeable.
Pros:
Minuses:
guard
or service?.doSomething
.In Swift, this function can be created using reflection and the Mirror class. It allows you to bypass all the properties and see their values.
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
- the name of the property,value: Any
is a value that can be nil.
But there is one trouble:
if child.value == nil { // }
Swift does not compare Any with nil. Formally, he is right, because βAnyβ and βAny?β Are different types. Therefore, you need to use the Mirror class again to find the final Optional in Any and see if it is equal to 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 }
Recursion is used because Any can have an optional nested, for example, an IService ?? ... The result of this function will produce the usual Optional, which can be compared to nil.
For some objects, not all properties need to be checked for nil, so we will add a list of exception properties. Due to the fact that child.label is a string, you can set exceptions only this way:
[String]
#Selector(keypath:)
(only for properties with objc )#keyPath()
(only for properties with objc )Swift 4 KeyPath allows you to get a value by key - an object, not a string. Now it is impossible to get the property name as a string. And you can not get a complete list of all KeyPath to walk on them.
We use the SelectorName protocol to support all variants without type casting, as well as enum:
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 } }
The final function code for checking dependency injection will look like this:
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 }
Dependencies can be declared as Force unwraped:
class TheObject: IObjectProtocol { var service: IService! }
Pros:
Minuses:
The fall of the app creates a negative user experience. As in the example with garlands: you get a light bulb that explodes brightly if it burns out. Most likely, such an empty dependency will pass by the developer and tester, if it is responsible for a not very important or rarely used function, and the application is able to work, albeit with limited functionality.
For example, without a phone number formatter in the list of orders, the application will work, will display the rest of the information, and not fall. And, of course, inform the developers about the problem.
In Swift, there is a conditional compilation, which allows using the Force unwrapped dependencies philosophy only in Debug mode. Compiler conditions allow you to make a function that will call fatalError in debug mode if it finds empty dependencies. And it is convenient to use it at the exit from the service locator, Assembly or factory.
Pros:
Minuses:
guard
or service?.doSomething
.This wrapper function checks the object's dependencies and drops it with an error if the -DDEBUG or -DINJECTION_CHECK_ENABLED flag is set for the compiler. In the rest of the case, he quietly writes to the log:
@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 } }
All methods have their pros and cons. Depending on the project, team, well-established principles of work will work better one way or another.
I prefer not to introduce dependencies through Init, because it is impossible to embed dependencies that are created from Storyboard into ViewController. And it complicates refactoring and making changes.
Forced Unwrap is not used not only for managing dependencies, but also in production in general. SwiftLint even has a 'force_unwrapping' rule activated that prevents it from being used.
Dependency checking in tests works well to support old code. Ensures that the old dependencies are not lost, and everything continues to work. But inconvenient for current development.
Therefore, I prefer checking on leaving the DI container with crashing in Debug mode. So the fastest you can find empty dependencies and immediately fix them.
All functions presented here are in the library:
TinkoffCreditSystems / InjectionsCheck
Source: https://habr.com/ru/post/336504/
All Articles