A shot in the arm for your code — dependency injection

A shot in the arm for your code — dependency injection

[[151314]]

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:

  1. - (instancetype)initWithDependency1:(Dependency1 *)d1
  2. dependency2:(Dependency2 *)d2;

Setter Injection:

  1. @property (nonatomic, retain) Dependency1 *dependency1;
  2. @property (nonatomic, retain) Dependency2 *dependency2;

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:

  • 1. Clear dependency declarations. The operations that an object needs to perform become clear at a glance, and it is also easy to eliminate dangerous hidden dependencies, such as global variables.
  • 2. Componentization. Dependency injection advocates composition over inheritance to improve code reusability.
  • 3. Easier to customize. When creating an object, it is easier to customize part of the object in special circumstances.
  • 4. Make dependencies clear. Especially when using constructor dependency injection, object ownership rules are strictly enforced - a direct acyclic object graph can be established.
  • 5. Testability. Dependency injection makes objects more testable than other methods. Because it is simple to create these objects through constructors, there is no need to manage hidden dependencies. In addition, it is easy to simulate dependencies, so that the test can be focused on the object being tested.

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:

  1. @interface RCRaceCar ()
  2.  
  3. @property (nonatomic, readonly) RCEngine *engine;
  4.  
  5. @end  
  6.  
  7. @implementation RCRaceCar
  8.  
  9. - (instancetype)init
  10. {
  11. ...
  12. // Create the engine. Note that it cannot be customized or  
  13. // mocked out without modifying the internals of RCRaceCar.  
  14. _engine = [[RCEngine alloc] init];
  15.  
  16. return self;
  17. }
  18.  
  19. @end  

The dependency injection has been slightly modified:

  1. @interface RCRaceCar ()
  2.  
  3. @property (nonatomic, readonly) RCEngine *engine;
  4.  
  5. @end  
  6.  
  7. @implementation RCRaceCar
  8.  
  9. // The engine is created before the race car and passed in  
  10. // as a parameter, and the caller can customize it if desired.  
  11. - (instancetype)initWithEngine:(RCEngine *)engine
  12. {
  13. ...
  14.  
  15. _engine = engine;
  16.  
  17. return self;
  18. }
  19.  
  20. @end  

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:

  1. @interface RCRaceCar ()
  2.  
  3. @property (nonatomic) RCEngine *engine;
  4.  
  5. @end  
  6.  
  7. @implementation RCRaceCar
  8.  
  9. - (instancetype)initWithEngine:(RCEngine *)engine
  10. {
  11. ...
  12.  
  13. _engine = engine;
  14. return self;
  15. }
  16.  
  17. - ( void )recoverFromCrash
  18. {
  19. if (self.fire != nil) {
  20. RCFireExtinguisher *fireExtinguisher = [[RCFireExtinguisher alloc] init];
  21. [fireExtinguisher extinguishFire:self.fire];
  22. }
  23. }
  24.  
  25. @end  

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:

  1. typedef RCFireExtinguisher *(^RCFireExtinguisherFactory)();
  2.  
  3. @interface RCRaceCar ()
  4.  
  5. @property (nonatomic, readonly) RCEngine *engine;
  6. @property (nonatomic, copy, readonly) RCFireExtinguisherFactory fireExtinguisherFactory;
  7.  
  8. @end  
  9.  
  10. @implementation RCRaceCar
  11. - (instancetype)initWithEngine:(RCEngine *)engine
  12. fireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory
  13. {
  14. ...
  15.  
  16. _engine = engine;
  17. _fireExtinguisherFactory = [extFactory copy];
  18. return self;
  19. }
  20.  
  21. - ( void )recoverFromCrash
  22. {
  23. if (self.fire != nil) {
  24. RCFireExtinguisher *fireExtinguisher = self.fireExtinguisherFactory();
  25. [fireExtinguisher extinguishFire:self.fire];
  26. }
  27. }
  28.  
  29. @end  

The factory pattern is also useful when we need to create an unknown number of dependencies, even in initializers, like:

  1. @implementation RCRaceCar
  2.  
  3. - (instancetype)initWithEngine:(RCEngine *)engine
  4. transmission:(RCTransmission *)transmission
  5. wheelFactory:(RCWheel *(^)())wheelFactory;
  6. {
  7. self = [ super init];
  8. if (self == nil) {
  9. return nil;
  10. }
  11.  
  12. _engine = engine;
  13. _transmission = transmission;
  14.  
  15. _leftFrontWheel = wheelFactory();
  16. _leftRearWheel = wheelFactory();
  17. _rightFrontWheel = wheelFactory();
  18. _rightRearWheel = wheelFactory();
  19.  
  20. // Keep the wheel factory for later in case we need a spare.  
  21. _wheelFactory = [wheelFactory copy];
  22.  
  23. return self;
  24. }
  25.  
  26. @end  

#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:

  1. + (instancetype)raceCarWithPitRadioFrequency:(RCRadioFrequency *)frequency;
  2. {
  3. RCEngine *engine = [[RCEngine alloc] init];
  4. RCTransmission *transmission = [[RCTransmission alloc] init];
  5.  
  6. RCWheel *(^wheelFactory)() = ^{
  7. return [[RCWheel alloc] init];
  8. };
  9.  
  10. return [[self alloc] initWithEngine:engine
  11. transmission: transmission
  12. pitRadioFrequency:frequency
  13. wheelFactory:wheelFactory];
  14. }

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.
System Singleton

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:

  1. / An example where we can't directly call the the initializer.
  2. RCRaceTrack *raceTrack = [objectYouCantModify createRaceTrack];
  3.  
  4. // We can still use properties to configure our race track.  
  5. raceTrack.width = 10;
  6. raceTrack.numberOfHairpinTurns = 2;

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.

  1. NSArray *raceCarClasses = @[
  2. [RCFastRaceCar class ],
  3. [RCSlowRaceCar class ],
  4. ];
  5.  
  6. NSMutableArray *raceCars = [[NSMutableArray alloc] init];
  7. for (Class raceCarClass in raceCarClasses) {
  8. // All race cars must have the same initializer ("init" in this case).  
  9. // This means we can't customize different subclasses in different ways.  
  10. [raceCars addObject:[[raceCarClass alloc] init]];
  11. }

For such problems, factory blocks can be used to simply replace the list of type declarations.

  1. typedef RCRaceCar *(^RCRaceCarFactory)();
  2.  
  3. NSArray *raceCarFactories = @[
  4. ^{ return [[RCFastRaceCar alloc] initWithTopSpeed: 200 ]; },
  5. ^{ return [[RCSlowRaceCar alloc] initWithLeatherPlushiness: 11 ]; }
  6. ];
  7.  
  8. NSMutableArray *raceCars = [[NSMutableArray alloc] init];
  9. for (RCRaceCarFactory raceCarFactory in raceCarFactories) {
  10. // We now no longer care which initializer is being called.  
  11. [raceCars addObject:raceCarFactory()];
  12. }

Storyboards
Storyboards provide a convenient way to lay out our user interface, but they bring problems to dependency injection. In particular, initializing the View Controller in the storyboard does not allow you to choose which initialization method to call. Similarly, when defining page jumps in the sytoyboard, the target View Controller will not give you a custom initialization method to generate an instance.

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.

  1. // In public ObjectA.h.  
  2. @interface ObjectA
  3. // Because the initializer uses a reference to ObjectB we need to  
  4. // make the Object B header public where we wouldn't have before.  
  5. - (instancetype)initWithObjectB:(ObjectB *)objectB;
  6. @end  
  7.  
  8. @interface ObjectB
  9. // Same here: we need to expose ObjectC.h.  
  10. - (instancetype)initWithObjectC:(ObjectC *)objectC;
  11. @end  
  12.  
  13. @interface ObjectC
  14. - (instancetype)init;
  15. @end  

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.

  1. @interface ObjectA
  2. - (instancetype)initWithObjectB:(id )objectB;
  3. @end  
  4.  
  5. // This protocol exposes only the parts of the original ObjectB that  
  6. // are needed by ObjectA. We're not creating a hard dependency on  
  7. // our concrete ObjectB (or ObjectC) implementation.  
  8. @protocol ObjectB
  9. - ( void )methodNeededByObjectA;
  10. @end  

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?

Recommend

10,000 words to dismantle the L’Oreal brand!

As the wave of new brands cools down, the spotlig...

A guide to private domain traffic for restaurants!

Between 2020 and 2021, various catering companies...

Geek Academy "Unity3D Engineer" Junior, Intermediate, Senior and Senior Engineer

Course Catalog ├──01-Junior Unity3D Development E...

How to plan a fission event promotion?

Hello everyone, I believe that every operator who...

The operational procedures of event planning scheme are universal!

This template is a relatively general activity te...

Sina Fuyi advertising display and billing model!

We know that there is a huge difference between S...

This battery is awesome and it only takes one minute to fully charge!

Scientists say they have invented a new type of b...

Notes on Xiaohongshu’s “commercialization” of promoting products?

Lei Jun, who became a soul singer popular on Bili...