What is Dependency Injection? Dependency injection (DI) is a popular design pattern in many programming languages, such as Java and C#, but it is not widely used in Objective-C. This article aims to briefly introduce dependency injection with examples in Objective-C, and introduce practical methods to use dependency injection in Objective-C code. Although the article focuses on Objective-C, all the concepts mentioned are also applicable to Swift. The concept of dependency injection is quite simple: an object should be passed dependencies to it, rather than creating them itself. I recommend Martin Fowler's excellent discussion on the subject as background reading. Dependencies can be passed to an object through an initializer (or constructor) or a property (set method). They are often called "constructor injection" and "setter injection". Constructor Injection:
Setter Injection:
According to Fowler's description, in general, constructor injection is preferred, and setter injection is used only when constructor injection is not suitable. Although you may still need to define properties for these dependencies when using constructor injection, you can set these properties to read only to simplify your object API. Why use dependency injection? There are many advantages to using dependency injection:
Using Dependency Injection in Your Code Your codebase may not yet be using the Dependency Injection design pattern, but it's easy to make the switch. The great thing about dependency injection is that you don't have to adopt the pattern across your entire project. Instead, you can apply it in a specific area of your codebase and expand from there. Secondary injection of various types First, divide your classes into two types: primitive types and complex types. Primitive types have no dependencies, or only depend on other primitive types. Primitive types rarely need to be inherited, because their functionality is clear and unchanging, and they don't need to link to external resources. Many primitive types are available from Cocoa itself, such as NSString, NSArray, NSDictionary, and NSNumber. Complex types are the opposite. They have complex dependencies, including application-level logic (the part that needs to be modified), or access to additional resources, such as disk, network, or global memory services. The vast majority of classes in an application are complex, including almost all controller objects and model objects. Many Cocoa types are also complex, such as NSURLConnection or UIViewController. According to the above classification, the easiest way to use the dependency injection pattern is to first select a complex class in the application, find the place where other complex objects are initialized in the class (find the "alloc]init" or "new" keywords). Introduce dependency injection into the class, and change this instantiated object to be passed as an initialization parameter in the class instead of the class initialization object itself. Assigning dependencies during initialization Let's look at an example where the child object (dependency) is initialized in the parent's initialization function. The original code is as follows:
The dependency injection has been slightly modified:
Lazy initialization of dependencies Some objects may not be used until some time later, or after initialization, or may never be used. Example before dependency injection:
The cars will not crash, so we will never use our fire extinguisher. Since the chances of needing this object are low, we don't want to slow down the creation of each car by creating it right away in the initialization method. Also, if our car needs to recover from multiple crashes, this would require creating multiple fire extinguishers. For situations like this, we can use the Factory design pattern. The factory design pattern is a standard Objectice-C blocks syntax that takes no parameters and returns an instance of an object. An object can use their blocks to create dependencies without having to know the details of how to create them. Here is an example of using dependency injection, also known as the factory design pattern, to create our fire extinguisher:
The factory pattern is also useful when we need to create an unknown number of dependencies, even in initializers, like:
#p# Avoid clunky configuration If an object shouldn't be alloc'ed in another object, where should it be alloc'ed? Are all these dependencies hard to configure? Are they alloc'ed the same every time? The solution to these problems is to rely on concise initializers for types (e.g. +[NSDictionary dictionary]). We take our object graph configuration out of ordinary objects, making them pure and testable, with clear business logic. Before adding a type's simplified initialization method, make sure it is necessary. If an object has only a few parameters in the init method, and these parameters do not have reasonable default values, then this type does not need a simplified initialization method, and can directly call the standard init method. We will collect our dependencies from 4 places to configure our objects: The value does not have a sensible default value. For example, each instance may contain a different boolean or numeric value. These values should be passed as parameters to the type's concise initializer. Existing shared objects. These objects should be passed as arguments to the type's concise initializer (e.g. a radio wave). These are objects that may have previously been evaluated as singletons or passed a superclass pointer. Newly created objects. If our object cannot share these dependencies with other objects, then the collaborating objects should create a new instance in the type profile initializer. These are objects that were previously allocated directly in the object's implementation. System singletons. These are singletons provided by Cocoa and can be used directly. Applications of these singletons, such as [NSFileManager defaultManager], can use singletons in your app for types that are expected to only generate one instance. There are many such singletons in the system. A concise initialization method for a racing car class is as follows:
Your type convenience initializers should go where appropriate. Common or reusable configuration files should go as objects in the .m file, while configurators used by a particular Foo object should go in the @interface of RaceCar. Many objects in the Cocoa library only have one instance, such as [UIApplication sharedApplication], [NSFileManager defaultManager], [NSUserDefaults standardUserDefaults], [UIDevice currentDevice]. If an object depends on one of these objects, you should put it in the initializer parameters. Even though your code may only have one instance, your test may want to mock that instance or create a test of one instance to avoid test dependencies. It is recommended that you avoid creating singletons of global references in your own code, and do not create a single instance of an object when it is first needed or inject it into all objects that depend on it. Immutable Constructors Occasionally there is a problem where the initializer/constructor of a class cannot be changed or called directly. In this case, setter injection should be used, for example:
Setter invocation allows you to configure objects, but this introduces additional variability in the object design that needs to be tested and resolved. Fortunately, the two main scenarios that lead to initializers being inaccessible or unchangeable can be avoided. Class Registration Using the class registration factory pattern means that objects cannot modify their initializers.
For such problems, factory blocks can be used to simply replace the list of type declarations.
Storyboards The solution is to avoid using storyboard. This sounds like an extreme solution, but we will find that using storyboard will cause a lot of other problems. In addition, if you don't want to lose the convenience brought by storyboard, you can use XIB, and XIB allows you to customize the initializer. Public vs. Private Dependency injection encourages you to expose more objects in your public interface. As mentioned before, this has many advantages. When building a framework, it can greatly enrich your public API. And with dependency injection, public object A can use private object B (which in turn can use private object C), but objects B and C are never exposed outside the framework. Object A has dependency injection on object B in its initializer, and object B's constructor creates public object C.
You also don't want users of the framework to worry about the implementation details of Object B and Object C. We can solve this problem through protocols.
Conclusion Dependency injection is a good fit for Objective-C and later Swift. Proper use can make your code base more readable, testable, and maintainable. |
<<: The Sino-Indian "Silicon Valley War" in the technology world: where does China lose?
>>: How exactly is an iPhone locked remotely?
Recently, market research firm Bernstein Research...
As the wave of new brands cools down, the spotlig...
Between 2020 and 2021, various catering companies...
Recently, a girl in Guangdong had persistent coug...
Course Catalog ├──01-Junior Unity3D Development E...
As a practitioner in Internet advertising , some ...
Hello everyone, I believe that every operator who...
This template is a relatively general activity te...
We know that there is a huge difference between S...
Recently, with the launch of new seasons of progr...
In October 2020, the national second-hand car mar...
Wandoujia is a mobile phone assistant designed sp...
Scientists say they have invented a new type of b...
Lei Jun, who became a soul singer popular on Bili...
If you have been paying attention to the Chinese ...