今天大佬小哥哥給我推薦了兩個(gè)庫(kù),關(guān)于依賴注入的,所以做個(gè)讀書筆記吧~
可參考:https://github.com/Swinject/Swinject 和 https://github.com/appsquickly/typhoon
Swinject Part
1. DI Container
DI是通過Ioc依賴反轉(zhuǎn)來解決依賴問題的一種方式,DI container會(huì)管理這些依賴,我們只需要開始的時(shí)候?qū)⑿枰膶?shí)例類型注冊(cè)給DI container,需要用這個(gè)實(shí)例的時(shí)候找DI container要就可以啦~
在Swinject里面定義了幾個(gè)概念:
Service: A protocol defining an interface for a dependent type.
大概就是.h文件定義了protocolComponent: An actual type implementing a service.
Service的實(shí)現(xiàn)類Factory: A function or closure instantiating a component.
生產(chǎn)Component的function,會(huì)在注冊(cè)的時(shí)候和實(shí)例類型一起傳給container,告訴container如何實(shí)例化出一個(gè)對(duì)象Container: A collection of component instances.
Component的集合
※ 注冊(cè)
如果component的實(shí)例化,依賴了另一個(gè)service,container會(huì)先實(shí)例化component以后進(jìn)行另一個(gè)service的實(shí)例化,然后將另一個(gè)service的實(shí)例注入到當(dāng)前創(chuàng)建的component。
注冊(cè)就像下面這樣提供service(實(shí)例類型)以及factory(實(shí)例化方法)即可:
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)!)
}
這個(gè)庫(kù)還允許你給同一個(gè)service注冊(cè)不同的實(shí)現(xiàn)factory,只要給每種實(shí)現(xiàn)起個(gè)名字,就可以通過不同的名字拿到不同的實(shí)現(xiàn)所生成的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生成對(duì)象的時(shí)候是帶參的,也就是從DI Container拿component的時(shí)候需要傳入相應(yīng)參數(shù):
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 初始化注入
初始化適用的場(chǎng)景是,如果class A的初始化必須要用class B的實(shí)例,例如:
class PetOwner: Person {
let pet: Animal
init(pet: Animal) {
self.pet = pet
}
}
這里Person的初始化就強(qiáng)依賴于Animal了。
2.2 Property Injection 屬性注入
和上面的相反,如果class B的實(shí)例對(duì)于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 方法注入
它和屬性注入非常相似,就是將實(shí)例通過方法來傳給另外一個(gè)實(shí)例,例如:
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 循環(huán)依賴
循環(huán)依賴其實(shí)就是不同type的component之間可能彼此依賴(當(dāng)然同一個(gè)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持有一個(gè)parent,而parent初始化又需要child。
在使用的時(shí)候?yàn)榱吮苊鉄o限循環(huán),先初始化一個(gè)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
這個(gè)東西不知道怎么翻譯了,它定義了當(dāng)我們向一個(gè)DI Container要component的時(shí)候,container如何給我們一個(gè)實(shí)例。是不是有點(diǎn)抽象,看看有哪些選擇吧還是:
Transient
每次找DI Container要都會(huì)返回一個(gè)新的實(shí)例,不會(huì)在container內(nèi)share,于是很容易出現(xiàn)循環(huán)依賴。
例如:如果A的init需要B,B的init需要A。那么A初始化的時(shí)候會(huì)先創(chuàng)建一個(gè)A的實(shí)例,然后試圖創(chuàng)建B的實(shí)例,注入給A;但是創(chuàng)建B的時(shí)候又會(huì)需要新創(chuàng)建A(因?yàn)锳不能復(fù)用,每次都創(chuàng)建新的),這樣一直循環(huán)就死循環(huán)啦。Graph (the default scope)
這個(gè)英文說得不是很清楚,按照我們小哥哥的說法就是,每次解循環(huán)的時(shí)候只有一個(gè)實(shí)例,也就是當(dāng)出現(xiàn)了圈圈依賴,某個(gè)實(shí)例再次被請(qǐng)求的時(shí)候,返回給請(qǐng)求者之前已經(jīng)創(chuàng)建過的實(shí)例。
例如:如果A的init需要B,B的init需要A。那么A初始化的時(shí)候會(huì)先創(chuàng)建一個(gè)A的實(shí)例,然后試圖創(chuàng)建B的實(shí)例,注入給A;B創(chuàng)建的時(shí)候會(huì)先創(chuàng)建一個(gè)實(shí)例,然后再請(qǐng)求一個(gè)A對(duì)象依賴注入給B,此時(shí)A不會(huì)重新再次創(chuàng)建啦,會(huì)拿上次建好的直接用。這就是一次解依賴。Container
類似單例,自從第一次請(qǐng)求以后就一直存在在container里面,以后只要請(qǐng)求就都返回同一個(gè)實(shí)例。Weak
當(dāng)有強(qiáng)引用指向的時(shí)候,這個(gè)實(shí)例就會(huì)存在在container里面被share;當(dāng)沒有引用了以后就被刪掉啦,直到下次有請(qǐng)求的時(shí)候會(huì)再次創(chuàng)建新的實(shí)例。
注意哦,如果你的factory方法返回了一個(gè)value type(例如struct),也就是其實(shí)這個(gè)實(shí)例不會(huì)在container里面被share的,每次你找container要都會(huì)給你一個(gè)新的,scope也就沒用了。
插一句其實(shí)service不僅可以使protocol,還可以是抽象類
P.S. 接口和protocol的區(qū)別是啥呢?
Objective-C 中的協(xié)議(@protocol),相當(dāng)于 C#, Java 等語言中的接口 (Interface)。協(xié)議本身不實(shí)現(xiàn)任何方法,只是聲明方法,使用協(xié)議的類必須實(shí)現(xiàn)協(xié)議方法。
Objective-C 中的接口(@interface),相當(dāng)于 C#, Java 等語言中的類(Class),是類的一個(gè)聲明,不同與 C#, Java 等語言的接口。
5. 其他特征
Container Hierarchy
如果注冊(cè)給parent container的對(duì)象,可以直接從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就是將多個(gè)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保證同步,這里其實(shí)只是想提醒一下需要考慮多線程的問題。
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;
}
這段代碼其實(shí)就是從NSUserDefaults獲取了一個(gè)id然后加一以后再保存,但是它依賴了NSUserDefaults,這要怎么做單元測(cè)試呢?
2. DI Implement
通過構(gòu)造器注入可以改成:
@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
但是如果我們傳入一個(gè)實(shí)例,其實(shí)強(qiáng)依賴了NSUserDefaults的實(shí)現(xiàn)(假設(shè)需要import NSUserDefaults.h),如果外部想換一個(gè)實(shí)現(xiàn)就不是很容易。
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
用遵從某個(gè)抽象協(xié)議的對(duì)象來注入靈活性更高
當(dāng)然,還有一種做法是,讓_userDefaults成為property以后建立不同子類,每個(gè)子類覆寫自己的_userDefaults的getter返回不同實(shí)例,也可以做到分離。
如果用屬性注入注意可以給default值,也就是懶加載一下,如果外界沒有賦值,則內(nèi)部建一個(gè)default的對(duì)象:(當(dāng)然如果屬性optional可以忽略)
- (NSUserDefaults *)userDefaults
{
if (!_userDefaults) {
_userDefaults = [NSUserDefaults standardUserDefaults];
}
return _userDefaults;
}
注意default value最好不要是第三方庫(kù)的對(duì)象,否則所有用到當(dāng)前對(duì)象的類都需要引入三方庫(kù),沒有很好地解耦
3. DI的一個(gè)常用例子: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.
也就是當(dāng)我們拉出一個(gè)IBOutlets的時(shí)候,其實(shí)是view初始化以后將自己注入到了IBOutlets屬性,其實(shí)就是屬性注入。
4. 又一個(gè)優(yōu)化例子
@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
上面這么寫是很正常可以實(shí)現(xiàn)功能的,但是如果測(cè)試的時(shí)候我希望改變一下session的cache策略,就需要?jiǎng)覤NRCodeHostFetcher的實(shí)現(xiàn)代碼,這個(gè)真的非常不科學(xué),所以可以把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. 討論點(diǎn)
如果test一個(gè)方法需要傳入一堆對(duì)象,都需要用DI注入,那么應(yīng)該把這個(gè)方法分成很多方法分別測(cè)試。
最后感覺其實(shí)DI比較適合要不就是每次創(chuàng)建新的,要不就是同一時(shí)間只有一個(gè)實(shí)例的狀況。如果同一時(shí)間有兩個(gè)就不好控制拿到的是哪一個(gè)啦。
Finally, Keep pluggable modules in the back of your head.