今天大佬小哥哥给我推荐了两个库,关于依赖注入的,所以做个读书笔记吧~
可参考:https://github.com/Swinject/Swinject 和 https://github.com/appsquickly/typhoon
Swinject Part
1. DI Container
DI是通过Ioc依赖反转来解决依赖问题的一种方式,DI container会管理这些依赖,我们只需要开始的时候将需要的实例类型注册给DI container,需要用这个实例的时候找DI container要就可以啦~
在Swinject里面定义了几个概念:
-
Service: A protocol defining an interface for a dependent type.
大概就是.h文件定义了protocol -
Component: An actual type implementing a service.
Service的实现类 -
Factory: A function or closure instantiating a component.
生产Component的function,会在注册的时候和实例类型一起传给container,告诉container如何实例化出一个对象 -
Container: A collection of component instances.
Component的集合
※ 注册
如果component的实例化,依赖了另一个service,container会先实例化component以后进行另一个service的实例化,然后将另一个service的实例注入到当前创建的component。
注册就像下面这样提供service(实例类型)以及factory(实例化方法)即可:
let container = Container()
container.register(Animal.self) { _ in Cat(name: "Mimi") }
container.register(Person.self) { r in
PetOwner(name: "Stephen", pet: r.resolve(Animal.self)!)
}
这个库还允许你给同一个service注册不同的实现factory,只要给每种实现起个名字,就可以通过不同的名字拿到不同的实现所生成的component~
例如:
let container = Container()
container.register(Animal.self, name: "cat") { _ in Cat(name: "Mimi") }
container.register(Animal.self, name: "dog") { _ in Dog(name: "Hachi") }
还允许factory生成对象的时候是带参的,也就是从DI Container拿component的时候需要传入相应参数:
container.register(Animal.self) { _, name, running in
Horse(name: name, running: running)
}
let animal2 = container.resolve(Animal.self, arguments: "Lucky", true)!
print(animal2.name) // prints "Lucky"
print((animal2 as! Horse).running) // prints "true"
2. Injection Patterns注入模式
2.1 Initializer Injection 初始化注入
初始化适用的场景是,如果class A的初始化必须要用class B的实例,例如:
class PetOwner: Person {
let pet: Animal
init(pet: Animal) {
self.pet = pet
}
}
这里Person的初始化就强依赖于Animal了。
2.2 Property Injection 属性注入
和上面的相反,如果class B的实例对于class A是optional的,就可以通过setter注入。
let container = Container()
container.register(Animal.self) { _ in Cat() }
container.register(Person.self) { r in
let owner = PetOwner2()
// 属性注入
owner.pet = r.resolve(Animal.self)
return owner
}
// PetOwner2可以有pet也可以没有
class PetOwner2: Person {
var pet: Animal?
init() { }
}
2.3 Method Injection 方法注入
它和属性注入非常相似,就是将实例通过方法来传给另外一个实例,例如:
let container = Container()
container.register(Animal.self) { _ in Cat() }
container.register(Person.self) { r in
let owner = PetOwner3()
// 方法注入
owner.setPet(r.resolve(Animal.self)!)
return owner
}
class PetOwner3: Person {
var pet: Animal?
init() { }
func setPet(pet: Animal) {
self.pet = pet
}
}
3. Circular Dependencies 循环依赖
循环依赖其实就是不同type的component之间可能彼此依赖(当然同一个type也是可能的...),例如:
protocol ParentProtocol: AnyObject { }
protocol ChildProtocol: AnyObject { }
class Parent: ParentProtocol {
let child: ChildProtocol?
init(child: ChildProtocol?) {
self.child = child
}
}
class Child: ChildProtocol {
weak var parent: ParentProtocol?
}
child持有一个parent,而parent初始化又需要child。
在使用的时候为了避免无限循环,先初始化一个child给parent,然后initCompleted以后再以依赖注入的方式setParent:
let container = Container()
container.register(ParentProtocol.self) { r in
Parent(child: r.resolve(ChildProtocol.self)!)
}
container.register(ChildProtocol.self) { _ in Child() }
.initCompleted { r, c in
let child = c as! Child
child.parent = r.resolve(ParentProtocol.self)
}
4. Object Scopes
这个东西不知道怎么翻译了,它定义了当我们向一个DI Container要component的时候,container如何给我们一个实例。是不是有点抽象,看看有哪些选择吧还是:
-
Transient
每次找DI Container要都会返回一个新的实例,不会在container内share,于是很容易出现循环依赖。
例如:如果A的init需要B,B的init需要A。那么A初始化的时候会先创建一个A的实例,然后试图创建B的实例,注入给A;但是创建B的时候又会需要新创建A(因为A不能复用,每次都创建新的),这样一直循环就死循环啦。 -
Graph (the default scope)
这个英文说得不是很清楚,按照我们小哥哥的说法就是,每次解循环的时候只有一个实例,也就是当出现了圈圈依赖,某个实例再次被请求的时候,返回给请求者之前已经创建过的实例。
例如:如果A的init需要B,B的init需要A。那么A初始化的时候会先创建一个A的实例,然后试图创建B的实例,注入给A;B创建的时候会先创建一个实例,然后再请求一个A对象依赖注入给B,此时A不会重新再次创建啦,会拿上次建好的直接用。这就是一次解依赖。 -
Container
类似单例,自从第一次请求以后就一直存在在container里面,以后只要请求就都返回同一个实例。 -
Weak
当有强引用指向的时候,这个实例就会存在在container里面被share;当没有引用了以后就被删掉啦,直到下次有请求的时候会再次创建新的实例。
注意哦,如果你的factory方法返回了一个value type(例如struct),也就是其实这个实例不会在container里面被share的,每次你找container要都会给你一个新的,scope也就没用了。
插一句其实service不仅可以使protocol,还可以是抽象类
P.S. 接口和protocol的区别是啥呢?
Objective-C 中的协议(@protocol),相当于 C#, Java 等语言中的接口 (Interface)。协议本身不实现任何方法,只是声明方法,使用协议的类必须实现协议方法。
Objective-C 中的接口(@interface),相当于 C#, Java 等语言中的类(Class),是类的一个声明,不同与 C#, Java 等语言的接口。
5. 其他特征
Container Hierarchy
如果注册给parent container的对象,可以直接从child container中拿到,例如:
let parentContainer = Container()
parentContainer.register(Animal.self) { _ in Cat() }
let childContainer = Container(parent: parentContainer)
let cat = childContainer.resolve(Animal.self)
print(cat != nil) // prints "true"
反之是不可以的哦。
Modularizing Service Registration
Assembly就是将多个service group一下,以及提供shared Container。例如:
class ServiceAssembly: Assembly {
func assemble(container: Container) {
container.register(FooServiceProtocol.self) { r in
return FooService()
}
container.register(BarServiceProtocol.self) { r in
return BarService()
}
}
}
主要还是管理同一组service及component。
Thread Safety
container本身可能不是线程安全的,但可以通过synchronize保证同步,这里其实只是想提醒一下需要考虑多线程的问题。
typhoon Part
1. Why DI?
The reason brittle object graphs are bad is that you cannot easily replace parts of the application. If an object expects to ask its environment for a load of other objects around it, then you cannot simply tell it that it should be using another object. Dependency injection fixes that.
- (NSNumber *)nextReminderId
{
NSNumber *currentReminderId = [[NSUserDefaults standardUserDefaults] objectForKey:@"currentReminderId"];
if (currentReminderId) {
// Increment the last reminderId
currentReminderId = @([currentReminderId intValue] + 1);
} else {
// Set to 0 if it doesn't already exist
currentReminderId = @0;
}
// Update currentReminderId to model
[[NSUserDefaults standardUserDefaults] setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
这段代码其实就是从NSUserDefaults获取了一个id然后加一以后再保存,但是它依赖了NSUserDefaults,这要怎么做单元测试呢?
2. DI Implement
通过构造器注入可以改成:
@interface Example ()
@property (nonatomic, strong, readonly) NSUserDefaults *userDefaults;
@end
@implementation Example
- (instancetype)initWithUserDefaults:(NSUserDefaults *userDefaults)
{
self = [super init];
if (self) {
_userDefaults = userDefaults;
}
return self;
}
@end
但是如果我们传入一个实例,其实强依赖了NSUserDefaults的实现(假设需要import NSUserDefaults.h),如果外部想换一个实现就不是很容易。
It would make more sense for the injected value to be an abstraction (that is, an id satisfying some protocol) instead of a concrete object
用遵从某个抽象协议的对象来注入灵活性更高
当然,还有一种做法是,让_userDefaults成为property以后建立不同子类,每个子类覆写自己的_userDefaults的getter返回不同实例,也可以做到分离。
如果用属性注入注意可以给default值,也就是懒加载一下,如果外界没有赋值,则内部建一个default的对象:(当然如果属性optional可以忽略)
- (NSUserDefaults *)userDefaults
{
if (!_userDefaults) {
_userDefaults = [NSUserDefaults standardUserDefaults];
}
return _userDefaults;
}
注意default value最好不要是第三方库的对象,否则所有用到当前对象的类都需要引入三方库,没有很好地解耦
3. DI的一个常用例子:Interface Builder
IB isn’t just about laying out interfaces; arbitrary properties can be filled with the real objects by declaring those properties as IBOutlets.
也就是当我们拉出一个IBOutlets的时候,其实是view初始化以后将自己注入到了IBOutlets属性,其实就是属性注入。
4. 又一个优化例子
@interface BNRCodeHostFetcher : NSObject
- (void)fetchGithubHome;
- (void)fetchBitbucketHome;
@end
@implementation BNRCodeHostFetcher
- (void)fetchGithubHome
{
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.github.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
NSDictionary *userInfo = @{ @"data": data,
@"response": response,
@"error": error };
[[NSNotificationCenter defaultCenter] postNotificationName:@"BNRGithubFetchCompletedNotification" object:self userInfo:userInfo];
}];
[task resume];
}
- (void)fetchBitbucketHome
{
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.bitbucket.org"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
NSDictionary *userInfo = @{ @"data": data,
@"response": response,
@"error": error };
[[NSNotificationCenter defaultCenter] postNotificationName:@"BNRBitbucketFetchCompletedNotification" object:self userInfo:userInfo];
}];
[task resume];
}
@end
上面这么写是很正常可以实现功能的,但是如果测试的时候我希望改变一下session的cache策略,就需要动BNRCodeHostFetcher的实现代码,这个真的非常不科学,所以可以把session和[NSNotificationCenter defaultCenter]拿出来share并且由外部传入:
@interface BNRCodeHostFetcher : NSObject
@property (nonatomic, readonly) NSURLSession *session;
@property (nonatomic, readonly) NSNotificationCenter *notificationCenter;
- (instancetype)initWithURLSession:(NSURLSession *)session notificationCenter:(NSNotificationCenter *)center;
//...
@end
@interface BNRCodeHostFetcher ()
@property (nonatomic, strong, readwrite) NSURLSession *session;
@property (nonatomic, strong, readwrite) NSNotificationCenter *notificationCenter;
@end
@implementation BNRCodeHostFetcher
- (instancetype)initWithURLSession: (NSURLSession *)session notificationCenter: (NSNotificationCenter *)center
{
self = [super init];
if (self)
{
self.session = session;
self.notificationCenter = center;
}
return self;
}
- (instancetype)init
{
return [self initWithURLSession:[NSURLSession sharedSession]
notificationCenter:[NSNotificationCenter defaultCenter]];
}
//...
@end
5. 讨论点
-
如果test一个方法需要传入一堆对象,都需要用DI注入,那么应该把这个方法分成很多方法分别测试。
-
最后感觉其实DI比较适合要不就是每次创建新的,要不就是同一时间只有一个实例的状况。如果同一时间有两个就不好控制拿到的是哪一个啦。
Finally, Keep pluggable modules in the back of your head.
网友评论