什么是依赖
对象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_requires
和objection_requires_sel
这两个宏用来属性注入,两者的区别是前者使用属性字符(不存在或者拼写错误编译器不会提示),后者采用方法选择器SEL
的形式,如果getter不存在,会发出警告,因此建议采用SEL
形式比较好。
我们来修改一下 A 中的代码:
#import "BObject.h"
@interface AObject : NSObject
@property (strong, nonatomic) BObject* bj;
@end
#import "AObject.h"
#import <Objection/Objection.h>
@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_initializer
和objection_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);
输出:
<AObject: 0x6000024d03a0> - <AObject: 0x6000024c0190>
一般情况下,我们每次从注入器中获取的对象都是不同的,大部分情况下,这个没什么可说的。在你的数据模型中也可以加上宏 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);
输出:
<AObject: 0x6000035602c0> - <AObject: 0x6000035602c0>
我们发现,再次从注入器中获取到的对象是同一个了,该对象作用域类似于系统的单例,但是它并不是真正的单例,仅仅是一个全局变量,并且其作用域可以再次被修改。
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);
控制台输出:
绑定的对象:<AObject: 0x600000e5c720>
<AObject: 0x600000e5c720> - <AObject: 0x600000e5c720>
通过对比发现,确实是同一个实例。当然你也可以将实例换成其他的,就像下面这样:
// 通过 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);
控制台输出:
绑定的对象:<BObject: 0x600002a57aa0>
<BObject: 0x600002a57aa0> xiaoming - <BObject: 0x600002a57aa0> xiaoming
通过对比发现,我们通过类名获取到了之前绑定的实例。到这里,我们大可以将注入器 injector
的使用类比为字典容器的使用,通过 key
将 value
进行绑定,然后通过 key
获取实例的过程。只不过,这个“字典容器”的功能更加的丰富。
2.1.2 绑定协议
在面向接口式编程时,我们通常只关心我的需求是什么,谁来实现并不重要。这样做的好处就是在多人合作开发时,你并不需要落实到某个实例(这通常也不是很重要),你需要做的就是抛出你的需求,然后关心有了这些“实现”去完成接下来要做的事情即可。
例如,跳转某个页面,你需要别人提供一些信息,你大可抛出需求做成接口协议,然后使用协议中的东西,别人也不需要关心你通过何种方式、何种类去实现该协议的(后期更改协议的实现很方便),甚至对方也需要该协议实现某个功能,同样可以放在协议中。
我们创建一个 B 页面和一个对应的协议,该协议需要提供一个背景色以及一段字符串。
@protocol BViewControllerProtocol <NSObject>
@property (strong, nonatomic) UIColor *bgColor;
@property (strong, nonatomic) NSString *others;
@end
在 B 页面遵循该协议,并使用这些数据:
#import "BViewControllerProtocol.h"
@interface BViewController : UIViewController <BViewControllerProtocol>
@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<UITouch *> *)touches withEvent:(UIEvent *)event{
JSObjectionInjector* injector = [JSObjection defaultInjector];
UIViewController <BViewControllerProtocol>* 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<UITouch *> *)touches withEvent:(UIEvent *)event{
JSObjectionInjector* injector = [JSObjection defaultInjector];
UIViewController <BViewControllerProtocol>* nextCtrl1 = [injector getObject:@protocol(BViewControllerProtocol)];
UIViewController <BViewControllerProtocol>* nextCtrl2 = [injector getObject:@protocol(BViewControllerProtocol)];
NSLog(@"first:%@ - second:%@",nextCtrl1, nextCtrl2);
}
控制台输出:
<BViewController: 0x7f9a6bb06d30>
first:<BViewController: 0x7f9a6bb06d30> - second:<BViewController: 0x7f9a6bb06d30>
-bindClass:
-(void)configure {
[self bindClass:BViewController.class toProtocol:@protocol(BViewControllerProtocol)];
}
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
JSObjectionInjector* injector = [JSObjection defaultInjector];
UIViewController <BViewControllerProtocol>* nextCtrl1 = [injector getObject:@protocol(BViewControllerProtocol)];
UIViewController <BViewControllerProtocol>* nextCtrl2 = [injector getObject:@protocol(BViewControllerProtocol)];
NSLog(@"first:%@ - second:%@",nextCtrl1, nextCtrl2);
}
控制台输出:
first:<BViewController: 0x7fbedff0b300> - second:<BViewController: 0x7fbedfd06c20>
- 绑定相同的
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<UITouch *> *)touches withEvent:(UIEvent *)event{
JSObjectionInjector* injector = [JSObjection defaultInjector];
UIViewController <BViewControllerProtocol>* nextCtrl1 = [injector getObject:@protocol(BViewControllerProtocol) named:@"first"];
UIViewController <BViewControllerProtocol>* nextCtrl2 = [injector getObject:@protocol(BViewControllerProtocol) named:@"second"];
NSLog(@"first ctrl:%@ - second ctrl:%@",nextCtrl1, nextCtrl2);
}
控制台输出:
first:<BViewController: 0x7f9ccfe0dcd0> - second:<BViewController: 0x7f9ccfe0e5c0>
first ctrl:<BViewController: 0x7f9ccfe0dcd0> - second ctrl:<BViewController: 0x7f9ccfe0e5c0>
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 <JSObjectionProvider>
@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<UITouch *> *)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);
}
控制台输出:
<AObject: 0x600000a3e400> - 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<UITouch *> *)touches withEvent:(UIEvent *)event{
JSObjectionInjector* injector = [JSObjection defaultInjector];
AObject* ob1 = [injector getObject:AObject.class];
AObject* ob2 = [injector getObject:AObject.class];
NSLog(@"%@ - %@", ob1, ob2);
}
控制台输出:
<AObject: 0x6000027341e0> - <AObject: 0x600002734240>
虽然 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
像是一种字典容器,通过多种形式将 value
和 key
关联起来,在完成配置之后,你只需要关注你通过何种 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<Action>)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<Action>)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 初始化/类方法注入
在上一节中,我们使用的就是此种注入方法。几个注意点:
- 构造器方法可以是实例方法也可以是类方法。
- 可以是无参的创建方法,例如[Person person]。
- 如果没有给出初始化方法,那么就会使用[[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 <Typhoon/Typhoon.h>
#import "Action.h"
@interface Person : NSObject
@property (copy, nonatomic) NSString* name;
@property (assign, nonatomic) NSInteger age;
@property (strong, nonatomic) id <Action> dog;
// 构造器
-(instancetype)initWithName:(NSString*)name age:(NSInteger)age;
@end
Action
协议内容如下:
#import <UIKit/UIKit.h>
// 行为接口协议
@protocol Action <NSObject>
@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<Action>)animal{
return [TyphoonDefinition withClass:Animal.class configuration:^(TyphoonDefinition *definition) {
[definition injectProperty:@selector(height) with:@(23.5)];
}];
}
@end
当你使用了自动注入宏后,你可以省去手动注入的代码:
#import <Typhoon/Typhoon.h> // 导入头文件
@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<Action>)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 自动注入类
类似 InjectedProtocol
,InjectedClass
对应注入类,它的使用和注意事项和协议的基本一致。
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_sel
和objection_initializer_sel
分别对应属性注入和构造器注入),你还可以通过模块的概念将功能划分统一配置管理,绑定的类型丰富多样,但是 Objection 的侵入性较强。Typhoon 侵入性较低,使用的形式比较统一规范,它的注入容器都需要继承自 TyphoonAssembly,没有指定的统一入口。