iOS 依赖注入:Objection 和 Typhoon

什么是依赖

对象A持有了对象B,我们就可以说对象A依赖对象B,或者对象B是对象A的一个依赖。一个对象需要的依赖越多,该对象耦合度越高,也就越难解藕。

/************** A对象 **************/
#import "BObject.h"
@interface AObject : NSObject
@property (strong, nonatomic) BObject* bj;
-(void)dosomething;
@end

#import "AObject.h"
@implementation AObject
-(instancetype)init {
    self = [super init];
    if (self) {
        self.bj = [[BObject alloc] initWithName:@"LOLITA"];
    }
    return self;
}
-(void)dosomething{
    NSLog(@"I am %@.", self.bj.name);
    [self.bj running];
}
@end

/************** B对象 **************/
@interface BObject : NSObject
@property (copy, nonatomic, readonly) NSString* name;
@property (assign, nonatomic, readonly) NSInteger age;
-(instancetype)initWithName:(NSString*)name;
-(instancetype)initWithName:(NSString*)name age:(NSInteger)age;
-(void)running;
@end

#import "BObject.h"
@interface BObject ()
@property (copy, nonatomic, readwrite) NSString* name;
@property (assign, nonatomic, readwrite) NSInteger age;
@end
@implementation BObject

-(instancetype)initWithName:(NSString*)name{
    return [self initWithName:name age:26];
}

-(instancetype)initWithName:(NSString*)name age:(NSInteger)age{
    self = [super init];
    if (self) {
        self.name = name;
        self.age = age;
    }
    return self;
}

-(void)running{
    NSLog(@"running...");
}
@end

存在的问题

仔细观察上述代码,我们会发现一些问题:

  • 如果现在想要更换 B 的构造方式,如使用构造方法-initWithName:age:,那么我们需要修改 A 类中的代码;
  • 如果想要测试多种的 B 对 A 的影响变得很难,因为 A 中写死了 B 的构造方法。

什么是依赖注入?

上面那种将依赖直接自己初始化是一种硬编码的方式,弊端在于两个类不够独立,不方便测试,我们对 A 进行一点修改:

#import "BObject.h"
@interface AObject : NSObject
@property (strong, nonatomic) BObject* bj;
-(instancetype)initWithBObject:(BObject*)bj;
-(void)dosomething;
@end

#import "AObject.h"
@implementation AObject
-(instancetype)initWithBObject:(BObject *)bj {
    self = [super init];
    if (self) {
        self.bj = bj;
    }
    return self;
}
-(void)dosomething{
    NSLog(@"I am %@.", self.bj.name);
    [self.bj running];
}
@end

看起来并没有多少变化,上述修改最大的特点是我们将 B 作为 A 的构造函数的一部分传入,在调用 A 的构造方法之前就已经初始化好的 B。像这种非自身主动创建依赖,而是通过外部传入的方式,我们就称为依赖注入。

【依赖注入】(Dependency Injection) 是面向对象编程的一种设计模式,用来减少代码之间的耦合度。通常基于接口来实现,也就是说:不需要亲自new一个对象,而是通过相关的控制器来获取对象。

依赖注入的好处

  • 方便地使用新对象取代旧的对象。如果从类的一种实现更改为另外一种实现,只需更改一个声明;
  • 消除紧密耦合;
  • 类更容易测试;
  • 关注点分离和面向接口。很容易看出每个类需要什么才能完成工作,让团队成员之间的合作变得更容易。

注入的方式

依赖注入的方式大体分为两种,相信你们都能想到:构造方法注入和属性注入 。

  • 构造方法注入

上面通过构造器注入依赖对象的方式就是一种典型的方法注入。外部使用的形式如下:

BObject* bj = [[BObject alloc] initWithName:@"xiaoming"];
AObject* ob = [[AObject alloc] initWithBObject:bj];
[ob dosomething];
  • 属性注入

属性注入也是一种常见的注入方式,通过表现就是调用对象的相关属性进行赋值:

AObject* ob = [[AObject alloc] init];
ob.bj = [[BObject alloc] initWithName:@"xiaoming"];
[ob dosomething];

除此之外,你可能还听说过其他的注入方式,比如工厂注入、延迟注入(块)等,这些大体上都是上述两种的变体。不管是何种注入,需要遵循的原则就是外部创建对象的依赖,而不是自身主动创建,这样的好处就是帮助我们开发出松散耦合(loose coupled)、可维护、可测试的代码和程序,这种原则就是大家熟知的面向接口,或者说是面向抽象编程。

下面我们主要介绍 iOS 依赖注入的两大框架 Objection 和 Typhoon 的使用,帮助大家更好的理解依赖注入以及面向接口编程的思想。


Objection

简介

Objection 是适用于 MacOS X 和 iOS 的 Objective-C 的轻量级依赖注入框架。Objection 旨在减轻维护大型 XML 容器或手动构造对象的需求。

基本用法

** 注入器 Injector **

上面关于依赖注入一节讲过 不需要亲自new一个对象,而是通过相关的控制器来获取对象Injector就是所谓的控制器,我们用它来配置和管理需要进行依赖注入的对象。

// 启用默认的注入器
JSObjectionInjector* injector = [JSObjection defaultInjector];
// 如果不存在,则创建一个
injector = injector ? : [JSObjection createInjector];
[JSObjection setDefaultInjector:injector];

// 通过注入器获取一个对象
AObject* aj = [injector getObject:AObject.class];
//    injector[AObject.class];  // 支持下标获取
//    [injector objectForKeyedSubscript:AObject.class];
NSLog(@"%@",aj);

1.0 一些宏的使用

上面的示例演示了默认注入器Injector的创建以及使用Class获取一个实例。注意,这里的defaultInjector并不是系统常见的单例,它仅仅是一个常量,需要手动初始化,可以被重置。

1.1 属性注入

上面示例你是否发现,虽然通过注入器获取到了实例,但是该实例的依赖就为nil,不要着急,我们一步一步来介绍。

objection_requiresobjection_requires_sel这两个宏用来属性注入,两者的区别是前者使用属性字符(不存在或者拼写错误编译器不会提示),后者采用方法选择器SEL的形式,如果getter不存在,会发出警告,因此建议采用SEL形式比较好。

我们来修改一下 A 中的代码:

#import "BObject.h"
@interface AObject : NSObject
@property (strong, nonatomic) BObject* bj;
@end

#import "AObject.h"
#import 
@implementation AObject
//objection_requires(@"bj")
objection_requires_sel(@selector(bj))
-(void)dosomething{
    NSLog(@"I am %@.", self.bj.name);
}
@end

然后我们在合适的位置来获取 A 对象。这里关于注入器的创建设置部分你可以放在应用启动的地方,这里不再演示出来。

JSObjectionInjector* injector = [JSObjection defaultInjector];
AObject* aj = [injector getObject:AObject.class];
NSLog(@"%@ - %@",aj, aj.bj);

这样,你通过属性注入宏以及注入器轻松的获取到了 A 对象并为 A 对象绑定了依赖 B 对象。

注意,发生拼写错误都会挂掉哦。

1.2 构造方法注入

和属性注入一样,方法注入同样有两个宏可以使用:objection_initializerobjection_initializer_sel。这次我们通过构造器来进行依赖注入:

@interface AObject : NSObject
@property (strong, nonatomic) BObject* bj;
-(instancetype)initWithBObject:(BObject*)bj; // 实例方法
+(AObject*)objectWithBObject:(BObject*)bj; // 类方法
@end

@implementation AObject
objection_initializer_sel(@selector(initWithBObject:)) // 该宏只需且只能出现一次
//objection_initializer_sel(@selector(objectWithBObject:))
-(instancetype)initWithBObject:(BObject*)bj{
    self = [super init];
    if (self) {
        self.bj = bj;
    }
    return self;
}
+(AObject *)objectWithBObject:(BObject *)bj{
    AObject* aj = AObject.new;
    aj.bj = bj;
    return aj;
}
-(void)dosomething{
    NSLog(@"I am %@.", self.bj.name);
}
@end

在合适的地方通过注入器获取 A :

JSObjectionInjector* injector = [JSObjection defaultInjector];
AObject* aj =
// 参数使用数组的形式传入
[injector getObject:AObject.class argumentList:@[BObject.new]];
// 参数通过不定参数传入
//    [injector getObjectWithArgs:AObject.class, BObject.new, nil];
NSLog(@"%@ - %@",aj, aj.bj);

1.3 非侵入式方法注入

1.2节中,我们使用了宏绑定了构造方法,这次我们采用非侵入式的方式获取 A 对象。

我们先删除之前的方法注入的宏,然后通过注入器直接指定某个构造器,传入对应的参数来获取 A 对象:

JSObjectionInjector* injector = [JSObjection defaultInjector];
AObject* aj = [injector getObject:AObject.class initializer:@selector(initWithBObject:) argumentList:@[BObject.new]];
NSLog(@"%@ - %@",aj, aj.bj);

这种方式似乎并不比使用原生的构造方法创建 A 对象来的简洁。

1.4 作用域

JSObjectionInjector* injector = [JSObjection defaultInjector];
AObject* ob1 = [injector getObject:AObject.class];
AObject* ob2 = [injector getObject:AObject.class];
NSLog(@"%@ - %@", ob1, ob2);

输出:
 - 

一般情况下,我们每次从注入器中获取的对象都是不同的,大部分情况下,这个没什么可说的。在你的数据模型中也可以加上宏 objection_register ,该宏是将当前类注册到注入器中,当然,你不写,你依旧能从注入器中获取到实例对象,我们找到该宏:

#define objection_register(value)           \
    + (void)initialize { \
        if (self == [value class]) { \
            [JSObjection registerClass:[value class] scope: JSObjectionScopeNormal]; \
        } \
    }

可以看到,该宏实际上给数据模型类中添加类一个初始化方法,并将自身的作用域 scope 设置为普通类型,该类型是个枚举:

typedef enum {
      JSObjectionScopeNone = -1,
      JSObjectionScopeNormal,
      JSObjectionScopeSingleton  
} JSObjectionScope;

这个枚举中有一个单例枚举,对应的宏为 objection_register_singleton ,我们使用该宏,看看有什么效果:

#import "AObject.h"
@implementation AObject
objection_register_singleton(self)
-(instancetype)initWithBObject:(BObject *)bj {
    self = [super init];
    if (self) {
        self.bj = bj;
    }
    return self;
}
-(void)dosomething{
    NSLog(@"I am %@.", self.bj.name);
    [self.bj running];
}
@end

同样的,我们运行下面的代码:

JSObjectionInjector* injector = [JSObjection defaultInjector];
AObject* ob1 = [injector getObject:AObject.class];
AObject* ob2 = [injector getObject:AObject.class];
NSLog(@"%@ - %@", ob1, ob2);

输出:
 - 

我们发现,再次从注入器中获取到的对象是同一个了,该对象作用域类似于系统的单例,但是它并不是真正的单例,仅仅是一个全局变量,并且其作用域可以再次被修改。

2.0 模块

模块是一组绑定,这些绑定为注入器提供了额外的配置信息。 这对于将外部依赖关系以及将协议绑定到类或实例特别有用。

我们的 Module 需要继承自 JSObjectionModule

2.1 实例和协议的绑定

2.1.1 绑定实例

我们创建一个名为 MyModule 的对象,继承自 JSObjectionModule 。在方法 -configure 中完成配置。

#import "MyModule.h"
#import "AObject.h"
#import "BObject.h"
@implementation MyModule
-(void)configure {
    // 通过 class 绑定某个实例对象,该对象通常是该类的实例
    AObject* ob = AObject.new;
    NSLog(@"绑定的对象:%@",ob);
    [self bind:ob toClass:AObject.class];
}
@end

注入器不再使用默认的,而是通过我们自己的模块来创建:

// 启用自己的模块创建注入器
JSObjectionInjector* injector = [JSObjection createInjector:MyModule.new];
[JSObjection setDefaultInjector:injector];

在某个合适的时机通过类名来获取该实例对象:

JSObjectionInjector* injector = [JSObjection defaultInjector];
AObject* ob1 = [injector getObject:AObject.class];
AObject* ob2 = [injector getObject:AObject.class];
NSLog(@"%@ - %@", ob1, ob2);

控制台输出:
绑定的对象:
 - 

通过对比发现,确实是同一个实例。当然你也可以将实例换成其他的,就像下面这样:

// 通过 class 绑定某个实例对象
BObject* bj = [[BObject alloc] initWithName:@"xiaoming"];
NSLog(@"绑定的对象:%@",bj);
[self bind:bj toClass:AObject.class];

JSObjectionInjector* injector = [JSObjection defaultInjector];
BObject* ob1 = [injector getObject:AObject.class];
BObject* ob2 = [injector getObject:AObject.class];
NSLog(@"%@ %@ - %@ %@", ob1, ob1.name, ob2, ob2.name);

控制台输出:
绑定的对象:
 xiaoming -  xiaoming

通过对比发现,我们通过类名获取到了之前绑定的实例。到这里,我们大可以将注入器 injector 的使用类比为字典容器的使用,通过 keyvalue 进行绑定,然后通过 key 获取实例的过程。只不过,这个“字典容器”的功能更加的丰富。

2.1.2 绑定协议

在面向接口式编程时,我们通常只关心我的需求是什么,谁来实现并不重要。这样做的好处就是在多人合作开发时,你并不需要落实到某个实例(这通常也不是很重要),你需要做的就是抛出你的需求,然后关心有了这些“实现”去完成接下来要做的事情即可。

例如,跳转某个页面,你需要别人提供一些信息,你大可抛出需求做成接口协议,然后使用协议中的东西,别人也不需要关心你通过何种方式、何种类去实现该协议的(后期更改协议的实现很方便),甚至对方也需要该协议实现某个功能,同样可以放在协议中。

我们创建一个 B 页面和一个对应的协议,该协议需要提供一个背景色以及一段字符串。

@protocol BViewControllerProtocol 
@property (strong, nonatomic) UIColor *bgColor;
@property (strong, nonatomic) NSString *others;
@end

在 B 页面遵循该协议,并使用这些数据:

#import "BViewControllerProtocol.h"
@interface BViewController : UIViewController 
@end

@implementation BViewController
@synthesize bgColor,others;
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = self.bgColor;
    NSLog(@"%@",self.others);
}
@end

接着,我们来配置模块,我们将该协议绑定到具体的实现类上:

-(void)configure {
    // 通过接口协议,将符合BViewControllerProtocol的类进行绑定
    [self bindClass:BViewController.class toProtocol:@protocol(BViewControllerProtocol)];
}

在某个合适的时机,我们通过注入器获取符合该协议的实例对象。

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    JSObjectionInjector* injector = [JSObjection defaultInjector];
    UIViewController * nextCtrl = [injector getObject:@protocol(BViewControllerProtocol)];
    nextCtrl.bgColor = UIColor.redColor;
    nextCtrl.others = @"something...";
    [self.navigationController pushViewController:nextCtrl animated:YES];
}

这样,外部无须关注是谁实现了该协议,只要符合该协议接口的任何实例就行,而且在后期更换协议的实现时,外部也无须进行任何改动。

2.1.3 注意事项

  • -bind:-bindClass:

方法 -bind:-bindClass: 的区别就是,前者绑定实例是全局实例,即通过注入器会获取同一个实例,就是你所绑定的实例,后者绑定局部实例,即每次通过注入器获取的对象都是不同的,因为你绑定的是类。

-bind:

-(void)configure {
    BViewController* ob1 = BViewController.new;
    NSLog(@"%@",ob1);
    [self bind:ob1 toProtocol:@protocol(BViewControllerProtocol)];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    JSObjectionInjector* injector = [JSObjection defaultInjector];
    UIViewController * nextCtrl1 = [injector getObject:@protocol(BViewControllerProtocol)];
    UIViewController * nextCtrl2 = [injector getObject:@protocol(BViewControllerProtocol)];
    NSLog(@"first:%@ - second:%@",nextCtrl1, nextCtrl2);
}

控制台输出:

first: - second:

-bindClass:

-(void)configure {
    [self bindClass:BViewController.class toProtocol:@protocol(BViewControllerProtocol)];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    JSObjectionInjector* injector = [JSObjection defaultInjector];
    UIViewController * nextCtrl1 = [injector getObject:@protocol(BViewControllerProtocol)];
    UIViewController * nextCtrl2 = [injector getObject:@protocol(BViewControllerProtocol)];
    NSLog(@"first:%@ - second:%@",nextCtrl1, nextCtrl2);
}

控制台输出:
first: - second:
  • 绑定相同的 class/protocol

当你绑定相同的 class/protocol 时,之前的绑定会被覆盖,就像下面这样的情况。覆盖的好处就是,我们可以在某些时刻修改相应实现,后面会提到。

-(void)configure {
    [self bindClass:BViewController.class toProtocol:@protocol(BViewControllerProtocol)];
    BViewController* ob1 = BViewController.new;
    [self bind:ob1 toProtocol:@protocol(BViewControllerProtocol)];
}

如果你不希望被覆盖,你可以使用 name 进行区分:

-(void)configure {
    BViewController* ob1 = BViewController.new;
    [self bind:ob1 toProtocol:@protocol(BViewControllerProtocol) named:@"first"];
    BViewController* ob2 = BViewController.new;
    [self bind:ob2 toProtocol:@protocol(BViewControllerProtocol) named:@"second"];
    NSLog(@"first:%@ - second:%@",ob1, ob2);
}

获取:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    JSObjectionInjector* injector = [JSObjection defaultInjector];
    UIViewController * nextCtrl1 = [injector getObject:@protocol(BViewControllerProtocol) named:@"first"];
    UIViewController * nextCtrl2 = [injector getObject:@protocol(BViewControllerProtocol) named:@"second"];
    NSLog(@"first ctrl:%@ - second ctrl:%@",nextCtrl1, nextCtrl2);
}

控制台输出:
first: - second:
first ctrl: - second ctrl:

2.2 手动构建实例

如果你需要介入实例的构建过程,你可以使用 block 回调来配置你的实例对象:

-(void)configure {
    [self bindBlock:^id(JSObjectionInjector *context) {
        AObject* ob = AObject.new;
        ob.bj = [[BObject alloc] initWithName:@"xiaoming"];
        return ob;
    } toClass:AObject.class];
}

虽然 block 允许你手动构建实例,但是我们通常是需要外部的一些参数来创建该实例的,该回调并没有将外部的参数传递进来,不过不用担心,你可以创建符合 ObjectionProvider 协议的类,在该类中,你可以接收到外部传入的参数,通过这些参数来创建依赖。

@interface AObjectProvider : NSObject 
@end

@implementation AObjectProvider
-(id)provide:(JSObjectionInjector *)context arguments:(NSArray *)arguments{
    AObject* ob = AObject.new;
    NSString* name = arguments.firstObject;
    ob.bj = [[BObject alloc] initWithName:name];
    return ob;
}
@end

-(void)configure {
    [self bindProvider:AObjectProvider.new toClass:AObject.class];
}

在合适的时机获取对象:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    JSObjectionInjector* injector = [JSObjection defaultInjector];
    AObject* ob1 = [injector getObject:AObject.class argumentList:@[@"xiaoming"]];
//    [injector getObjectWithArgs:AObject.class, @"xiaoming", nil];
    NSLog(@"%@ - %@", ob1, ob1.bj.name);
}

控制台输出:
 - xiaoming

2.3 作用域

在模块中,类的范围可以是单例。 相应的,在注入器的上下文中,已注册的单例可以降级为正常的生命周期。

@implementation MyModule
-(void)configure {
    [self bindClass:[Singleton class] inScope:JSObjectionScopeNormal];
    [self bindClass:[AObject class] inScope:JSObjectionScopeSingleton];
}
@end

这里的 Singleton 单例并非 OC 中真正的单例,而是 Objection 中的 JSObjectionScopeSingleton 类型,即你无法将自定义的单例类降级。

我们给 A 对象注册为 JSObjectionScopeSingleton 类型,然后使用降级。

@implementation AObject
objection_register_singleton(self)
……
@end

@implementation MyModule
-(void)configure {
    [self bindClass:AObject.class inScope:JSObjectionScopeNormal];
}
@end

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    JSObjectionInjector* injector = [JSObjection defaultInjector];
    AObject* ob1 = [injector getObject:AObject.class];
    AObject* ob2 = [injector getObject:AObject.class];
    NSLog(@"%@ - %@", ob1, ob2);
}

控制台输出:
 - 

虽然 A 类注册了单例类型,但是我们通过模块的降级处理,从注入器中获取的实例对象依旧是不同的。

2.4 多个模块

可以通过已创建的注入器加入更多的模块:

// 启用默认的注入器
JSObjectionInjector* injector = [JSObjection defaultInjector];
injector = injector ? : [JSObjection createInjector];

// 添加自定义的模块
injector = [injector withModule:MyModule.new];

// 添加更多的模块
injector = [injector withModules:MyModule1.new, MyModule2.new, nil];
[JSObjection setDefaultInjector:injector];

-withModule:方法加入的模块,会将之前相同的绑定覆盖,如果你想更换甭定、作用域等等,就可以使用该方法加入新的绑定。

既然能够加入,那么对应的,withoutModuleOfType: 将会移除之前的模块。

2.5 +load

关于注入器的创建,除了在应用启动时进行配置,我们可以将其放在 +load 中,该方法会在编译的过程中被调用,这样做的好处就是我们不需要在 AppDelegate 中导入各种模块类,后期修改时,也不需要手动移除。

@implementation MyModule
+(void)load{
    JSObjectionInjector* injector = [JSObjection defaultInjector];
    injector = injector ? : [JSObjection createInjector];
    injector = [injector withModule:[self.class new]];
    [JSObjection setDefaultInjector:injector];
}
-(void)configure {
    // 绑定……
}
@end

3.0 总结

Objection 帮助你实现【依赖注入】,你只需要完成两件事情,配置依赖关系和获取依赖对象。配置依赖关系时,你可以使用几个常用的宏来快速的完成依赖关系的配置,另外你还可以使用模块的概念来完成更多的绑定操作,它允许你将某些类或某些协议接口绑定到某个或某一类的对象上,在配置完成后,你就可以使用关键的 injector 注入器获取到你所需要的对象。

Objection 像是一种字典容器,通过多种形式将 valuekey 关联起来,在完成配置之后,你只需要关注你通过何种 key 获取到需要的 value 即可。Objection 最主要的功能之一就是面向接口编程的实现,在上面的示例中也进行了演示,面向接口编程是一种非常重要的编程思想。


Typhoon

Typhoon 使用 Objective-C 运行时来收集元数据并实例化对象。相比于 Objection ,它有 Swift 版本,并且有着自己的团队进行维护。用官网的介绍,Typhoon 有着以下的优势:

  • 不需要宏或XML,使用功能强大的 ObjC 运行时检测;
  • 支持 IDE 重构,代码完成和编译时检查;
  • 提供配置详细信息的完全模块化和封装;
  • 可以使用任何顺序声明依赖项;
  • 使具有相同基类或协议的多个配置变得非常容易;
  • 支持注入视图控制器;
  • 支持初始化注入和属性注入,以及生命周期管理;
  • 强大的内存管理功能。提供预配置的对象,没有单例的内存开销;
  • 支持循环依赖;
  • 占用空间极低,非常适合 CPU 和 内存受限的设备;
  • 功能丰富,轻量级;
  • 历经实战测试 。

简单示例

1.1 创建 Assembly

类似于 Objection 的注入器,Typhoon 需要 TyphoonAssembly 的子类实例,该实例用于管理、配置依赖的各种实例。

@interface MyAssembly : TyphoonAssembly
-(Person*)person;
-(id)animal;
@end

1.2 配置依赖

-(Person *)person{
    return [TyphoonDefinition withClass:[Person class] configuration:^(TyphoonDefinition *definition) {
        // 进行配置
        // 构造器/类方法注入
        [definition useInitializer:@selector(initWithName:age:) parameters:^(TyphoonMethod *initializer) {
            // 注入相应的参数,顺序一致
            [initializer injectParameterWith:@"xiaoming"];
            [initializer injectParameterWith:@(26)];
        }];
    }];
}
-(id)animal{
    return [TyphoonDefinition withClass:Animal.class];
}

1.3 获取依赖

MyAssembly* assembly = [[MyAssembly new] activated];
Person* p = assembly.person;
Animal* animal = assembly.animal;

至此,我们完成了 Typhoon 的依赖注入的简单使用,Assembly 的创建 -> 依赖的配置 -> 依赖的获取。

从使用来看,Typhoon 的思路很简单,创建一个依赖注入的管理容器,在该容器中配置各种依赖关系,接着在需要的地方获取对应的依赖实例。似乎并没有亮点指出,不同之处是用于配置依赖的创建方法进行了统一。

更多的注入方式

2.1 初始化/类方法注入

在上一节中,我们使用的就是此种注入方法。几个注意点:

  1. 构造器方法可以是实例方法也可以是类方法。
  2. 可以是无参的创建方法,例如[Person person]。
  3. 如果没有给出初始化方法,那么就会使用[[alloc] init]。

由于 OC 运行时没有提供参数类型的信息,因此 Typhoon 无法提供参数类型,如果你想要通过类型进行注入,那么你需要使用下面的属性注入的方式。

2.2 属性注入

-(Person *)person{
    return [TyphoonDefinition withClass:[Person class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(name) with:@"xiaoming"];
        [definition injectProperty:@selector(age) with:@(26)];
    }];
}

属性注入可以通过类型完成。另外,你还可以使用自动注入宏来完成属性注入。

2.2.1 自动注入协议

为了介绍宏的作用,我们来演示不用自动宏的情况下的示例:

我们给类 Person 添加一个遵循协议接口 Action 的 dog 实例。

#import 
#import "Action.h"
@interface Person : NSObject
@property (copy, nonatomic) NSString* name;
@property (assign, nonatomic) NSInteger age;
@property (strong, nonatomic) id  dog;
// 构造器
-(instancetype)initWithName:(NSString*)name age:(NSInteger)age;
@end

Action 协议内容如下:

#import 
// 行为接口协议
@protocol Action 
@optional
@property (assign, nonatomic) CGFloat height;
-(void)run;
@end

我们的 Assembly 需要手动将 animal 注入到属性 dog 上:

@implementation MyAssembly
-(Person *)person{
    return [TyphoonDefinition withClass:[Person class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(name) with:@"xiaoming"];
        [definition injectProperty:@selector(age) with:@(26)];
        [definition injectProperty:@selector(dog) with:self.animal];
    }];
}
-(id)animal{
    return [TyphoonDefinition withClass:Animal.class configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(height) with:@(23.5)];
    }];
}
@end

当你使用了自动注入宏后,你可以省去手动注入的代码:

#import  // 导入头文件
@interface Person : NSObject
@property (copy, nonatomic) NSString* name;
@property (assign, nonatomic) NSInteger age;
@property (strong, nonatomic) InjectedProtocol(Action) dog; // 使用自动注入宏
-(instancetype)initWithName:(NSString*)name age:(NSInteger)age;
@end

@implementation MyAssembly
-(Person *)person{
    return [TyphoonDefinition withClass:[Person class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(name) with:@"xiaoming"];
        [definition injectProperty:@selector(age) with:@(26)];
    }];
}
-(id)animal{
    return [TyphoonDefinition withClass:Animal.class configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(height) with:@(23.5)];
    }];
}
@end

这样,MyAssembly 会自动关联遵循 Action 的依赖配置 animal

但是,使用该宏需要注意的:

  • Assembly 中必须有且只能有一个符合条件的实例;
  • 如果Person中需要多个遵循协议的都会被指定到同一个实例;
  • 使用了宏之后,不需要写 id <协议> 这样的声明。

2.2.2 自动注入类

类似 InjectedProtocolInjectedClass 对应注入类,它的使用和注意事项和协议的基本一致。

2.2.3 When and Why?

在知道自动注入宏的特点之后,什么情况下使用,什么情况下不应该使用呢?

使用

  • 当你想省事时
  • 当你想要在类中记录哪些属性需要依赖时

不应使用

  • 当你想要避免 Typhoon 侵入你的任何类,只想注入过程出现在 TyphoonAssembly 时
  • 当你只想集中使用 Typhoon 时

建议

  • 对顶层组件(例如视图控制器)使用自动注入
  • 对测试用例使用自动注入

2.3 方法注入

-(Person*)personWithMethodInjection{
    return [TyphoonDefinition withClass:Person.class configuration:^(TyphoonDefinition *definition) {
        [definition injectMethod:@selector(setName:age:) parameters:^(TyphoonMethod *method) {
            [method injectParameterWith:@"xiaoming"];
            [method injectParameterWith:@(26)];
        }];
    }];
}

方法注入和初始化注入非常类似。

2.4 注入的回调时机

Typhoon 允许你参与注入的时机,给 Typhoon 传入指定的方法,Typhoon 会在注入的前后进行回调。

// 注入前执行某方法
[definition performBeforeInjections:@selector(beforeInjectionsAction)];
// 注入后执行某方法
[definition performAfterInjections:@selector(afterInjectionsAction:) parameters:^(TyphoonMethod *method) {
    // 添加参数
    [method injectParameterWith:@"xiaoming"];
}];

如果你不介意被 Typhoon 侵入,你可以导入头文件 Typhoon.h,在相应的方法中得到注入的时机。

#import Typhoon.h
- (void)typhoonWillInject{
}
- (void)typhoonDidInject{
}

2.5 运行时参数注入

允许配置依赖实例是带入参数,像如同一般的方法定义:

-(BViewController*)detailControllerForPerson:(Person*)p{
    return [TyphoonDefinition withClass:BViewController.class configuration:^(TyphoonDefinition *definition) {
        [definition useInitializer:@selector(initWithPerson:) parameters:^(TyphoonMethod *initializer) {
            [initializer injectParameterWith:p];
        }];
    }];
}

参数必须是一个对象类型,基本数据类型需要进行包装(NSValue、NSNumber)。

2.6 工厂注入

-(PersonFactory*)personFactory{
    return [TyphoonDefinition withClass:PersonFactory.class];
}
-(Person*)student{
    return [TyphoonDefinition withFactory:[self personFactory] selector:@selector(personWithTag:) parameters:^(TyphoonMethod *factoryMethod) {
        [factoryMethod injectParameterWith:@"student"];
    }];
}

2.7 循环依赖

Typhoon 支持循环依赖。例如控制器需要视图,视图需要控制器作为它的代理。

- (SettingsController *)appSettingsController{
    return [TyphoonDefinition withClass:[AppSettingsController class]
        configuration:^(TyphoonDefinition* definition){
        [definition useInitializer:@selector(initWithSoundManager:settingsView:)
            parameters:^(TyphoonMethod* initializer){
            [initializer injectParameterWith:[_kernel soundManager]];
            [initializer injectParameterWith:[self appSettingsView]];
        }];
        [definition injectProperty:@selector(title) with:@"Settings"];
    }];
}
- (SettingsView *)appSettingsView{
    return [TyphoonDefinition withClass:[AppSettingsView class]
        configuration:^(TyphoonDefinition* definition){
        [definition injectProperty:@selector(delegate) with:
            [self appSettingsController]];
    }];

2.8 抽象和基类

如果你想要在派生类中共享配置,你可以像下面这样使用:

-(Person *)person{
    return [TyphoonDefinition withClass:[Person class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(name) with:@"xiaoming"];
        [definition injectProperty:@selector(age) with:@(26)];
    }];
}
-(Student *)student{
    return [TyphoonDefinition withParent:self.person class:Student.class configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(level) with:@(13)];
    }];
}

Student 将从其父级 Person 继承初始化程序,属性注入,方法注入和作用域。 这些都可以被覆盖。父类可以无限地串连在一起。

2.9 作用域

Objection 一样,Typhoon 也有作用域的概念。

-(Person *)person{
    return [TyphoonDefinition withClass:[Person class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(name) with:@"xiaoming"];
        [definition injectProperty:@selector(age) with:@(26)];
        definition.scope = TyphoonScopeSingleton;
    }];
}

TyphoonScopeSingleton 类型作用下,将获取的相同的对象。

3.0 小结

Typhoon 采用了简洁的管理依赖的形式,紧紧围绕着依赖注入的两种注入方式:方法注入和属性注入展开,将依赖对象的构建、配置进行了相对地统一。我们只需要继承自TyphoonAssembly,就可以是生成一个注入容器。


总结

Objection 和 Typhoon 都是优秀的依赖注入的三方库。Objection 提供了一个全局的注入器,是所有依赖对象获取的入口。针对单独的类中的依赖对象,Objection 提供了便捷的宏来添加依赖对象的创建(objection_requires_selobjection_initializer_sel分别对应属性注入和构造器注入),你还可以通过模块的概念将功能划分统一配置管理,绑定的类型丰富多样,但是 Objection 的侵入性较强。Typhoon 侵入性较低,使用的形式比较统一规范,但它的注入容器都需要继承自 TyphoonAssembly,没有统一入口,模块过多时,并不是很理想。


相关

  • Dependency Injection

  • dependency-injection-ios-and-you

  • dependency-injection

你可能感兴趣的:(iOS 依赖注入:Objection 和 Typhoon)