Design Patterns: Dependency Injection

原文 : Design Patterns: Dependency Injection

即使相依性注入是一個很稀少教給初心者的話題,他是個值得更多關注的設計模式

許多開發者避免相依性注入,因為他們並不曉得他的意義,或者因為他們認為不需要它。

在這篇文章,我會嘗試說服你相依性注入的價值所在。

要做到這一點,我將會以相依性注入最簡單的形式,來說明它多麼的簡單。

1. What Is Dependency Injection?

很多人都寫了關於相依性注入,且有一堆為了簡化相依性注入的工具和 Library 。

這裏有一條引言,然而,that captures the confusion many people have around dependency injection.

“Dependency Injection” is a 25-dollar term for a 5-cent concept. - James Shore

一旦你掌握了相依性注入背後的原理,你將會懂剛剛那段引言。

我們先用範例來展示它的概念。

iOS App 有許多的相依物件,且你甚至沒有察覺你的應用程式可能依賴於相依物件,就是這樣,你不認為他們是相依物件

下面有一段程式片段。

這段實作包含一個叫做 saveList: 的方法。

你可以指出他的相依物件嗎?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)saveList:(NSArray *)list {
if ([list isKindOfClass:[NSArray class]]) {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:list forKey:@"list"];
}
}
@end

最經常被忽視的相依物件,通常是我們最常依賴的。

saveList: 方法中,我們儲存 array 到 userDefaults,透過 NSUserDefaults 物件。

我們藉由使用 standardUserDefaults 方法存取 shared defaults object。

如果你熟悉 iOS 或 OS X 開發,那你很可能是最熟悉 NSUserDefaults 物件。

存放資料到 userDefaults 是個快速,簡單且可靠。

感謝 standardUserDefaults 方法,我們能在專案的任何位置存取 userDefaults。

我們可以在任何時間,任何地方使用 standardUserDefaults 回傳的 singleton。


Singleton? 任何時間且任何位置? 你有聞到什麼嗎?

我不只聞到了相依物件,我也聞到了壞習慣。

在這篇文章,我不想要討論濫用 singletons,但很重要的是,我們應該保守的使用 singletons。

Take a look at the following example for clarification.

多數人習慣使用 userDefaults 且不認為他是個相依物件。

但他當然是相依物件。

我們一般使用的 [NSNotificationCenter defaultCenter] 也一樣是個相依物件。

讓我們用下面的範例來澄清。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

#pragma mark -
#pragma mark Initialization
- (instancetype)init {
self = [super init];

if (self) {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}

return self;
}

#pragma mark -
#pragma mark Memory Management
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark -
#pragma mark Notification Handling
- (void)applicationWillEnterForeground:(NSNotification *)notification {
// ...
}

@end

上面是個非常常見的使用情境。

我們對 view controller 添加 notifications 的 observer,並且在 dealloc 的時候移除 observer。

這添加另一個相依物件給 ViewController class,相依物件通常會被忽略或無視 (overlooked or ignored)。


overlooked : 沒注意到而忽視。

ignored : 有注意到的忽視。

你可能會問自己 What is the problem? 或者好一點的 Is there a problem?

讓我們先從第一個問題開始!

What Is the Problem?

基於上面的範例,它似乎沒什麼問題。

雖然這不完全對。

view controller 依賴於 shared defaults object 和 the default notification center 來完成它的工作。

這是個問題嗎?幾乎每個物件都依賴其他的物件來完成他的工作。

問題在於相依物件過於隱性。

對於新加入到你的專案的開發者,無法從 class interface 檢視,因此並不知道 view controller 依賴於哪些相依物件。

Testing the ViewController class will also prove to be tricky since we don’t control the NSUserDefaults and NSNotificationCenter classes.

我們來看看一些這個問題的解決方案。

換句話說,讓我們看相依性注入如何幫我們解決這個問題。


2. Injecting Dependencies

如同剛剛簡介提到的,相依性注入是一個非常簡單的概念。

James Shore 之前寫一篇相當棒的文章

這邊有另外一條 James Shore 的引言,關於相依性注入是什麼的核心概念。

Dependency injection means giving an object its instance variables. Really. That’s it. - James Shore

這邊有幾種完成這的方法,但很重要的是,要先理解上面那段引言的意義。

讓我們來看看,如何把相依性注入應用到 ViewController class。

首先,我們先取代在 init 方法中的 [NSUserDefaults standardUserDefaults],用 NSNotificationCenter 的 property 替代。

This is what the updated interface of the ViewController class looks like after this addition.

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (weak, nonatomic) NSNotificationCenter *defaultCenter;

@end

這也意味著我們在 initialize ViewController 時,要做些額外的工作。

如同 James 文章提到,我們掌握了 ViewController instance 的變數。

That is how simple dependency injection is.

It’s a fancy name for a straightforward concept.

1
2
3
4
5
// Initialize View Controller
ViewController *viewController = [[ViewController alloc] init];

// Configure View Controller
[viewController setNotificationCenter:[NSNotificationCenter defaultCenter]];

As a result of this change, the implementation of the ViewController class changes.

下面是用了相依性注入之後 init 及 dealloc 使用 default notification center 的樣子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma mark -
#pragma mark Initialization
- (instancetype)init {
self = [super init];

if (self) {
[self.notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}

return self;
}

#pragma mark -
#pragma mark Memory Management
- (void)dealloc {
[_notificationCenter removeObserver:self];
}

注意到我們在 dealloc 方法中沒有用 self

這是考慮到壞習慣,他可能導致些非預期的結果。

這邊有一個問題。你能把它指出來嗎?


在 ViewController class 的 initializer 中,我們幫 view controller 添加 notificationCenter 的 observer。

在初始化過程中,然而,notificationCenter 尚未被設定。

我們可以在初始方法中,把相依物件丟進去來解決這個問題。

看起來就像這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma mark -
#pragma mark Initialization
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter {
self = [super init];

if (self) {
// Set Notification Center
[self setNotificationCenter:notificationCenter];

// Add Observer
[self.notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}

return self;
}

確保它可以運作,我們也需要更新 ViewController 的 interface。

我們省略了 notificationCenter 的 property 並且加了初始方法。

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

#pragma mark -
#pragma mark Initialization
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter;

@end

在實作檔(.m 檔),我們建立了 class extension 且宣告了 notificationCenter 的 property。

通過這樣做,我們的 ViewController 有私有的 notificationCenter property 且只能透過新的初始方法做設定。

這是另一個該牢記的好習慣,只有暴露需要的 property 為 public

1
2
3
4
5
6
7
#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) NSNotificationCenter *notificationCenter;

@end

初始化 ViewController 的實例,我們依賴於剛剛建立的初始化方法。

1
2
// Initialize View Controller
ViewController *viewController = [[ViewController alloc] initWithNotificationCenter:[NSNotificationCenter defaultCenter]];

3. Benefits

What have we accomplished by explicitly injecting the notification center object as a dependency?

Clarity

ViewController 的 interface 明確展示出,它依賴於 NSNotificationCenter

如果你是 iOS 或 OS X 的新開發者,這個的複雜度看起來只有小贏一點。

然而,當你的專案逐漸變大,你將會感激專案裡每一個 clarity 的小元素。

明確低定義相依物件將幫助你專案清晰明瞭。

Modularity

當你開始使用相依性注入,你的 code 會變得更模組化。

儘管我們注入了特定的類別給 ViewController,我們也可以注入符合特定的 protocol。

如果你採用這個途徑,我們可以更簡單的抽換它的 implementation。

1
2
3
4
5
6
7
8
9
10
11
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (strong, nonatomic) id<MyProtocol> someObject;

#pragma mark -
#pragma mark Initialization
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter;

@end

上面的 ViewController 的 interface,我們定義了另一個相依物件

這個相依物件是符合 MyProtocol 的物件。

這地方就是相依性注入強悍所在。

ViewController 不用管那個物件是什麼型態,它只要求這個物件採用了 MyProtocol

這讓我們的 code 有更多的模組化,彈性以及可測試性。

Testing

儘管測試對 iOS 和 OS X 開發者必不是很普遍,測試是個主要的議題 that gains in importance and popularity。

採納相依性注入,可以讓你的 code 更容易測試。

你會怎麼測試下面的初始方法?That’s going to be tricky. Right?

1
2
3
4
5
6
7
8
9
10
11
12
#pragma mark -
#pragma mark Initialization
- (instancetype)init {
self = [super init];

if (self) {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}

return self;
}

第二個初始方法,然而,他讓測試變得更簡單了。

看一下這個初始方法如何在測試裡運行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma mark -
#pragma mark Initialization
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter {
self = [super init];

if (self) {
// Set Notification Center
[self setNotificationCenter:notificationCenter];

// Add Observer
[self.notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}

return self;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma mark -
#pragma mark Tests for Initialization
- (void)testInitWithNotificationCenter {
// Create Mock Notification Center
id mockNotificationCenter = OCMClassMock([NSNotificationCenter class]);

// Initialize View Controller
ViewController *viewController = [[ViewController alloc] initWithNotificationCenter:mockNotificationCenter];

XCTAssertNotNil(viewController, @"The view controller should not be nil.");

OCMVerify([mockNotificationCenter addObserver:viewController selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]);
}

上面的測試用了 OCMock Library,一個相當優秀的 mocking library

我們傳入一個 mock 物件而不是 NSNotificationCenter,且驗證在初始方法中確實有呼叫 addObserver: selector: name: object: 方法。

這邊有幾個測試 notification handling 的方法,this is—by far—the easiest I’ve come across.

It adds a bit of overhead by injecting the notification center object as a dependency,但是在我的觀點來看,它提供的效益勝過增加的複雜度。

4. Third Party Solutions

The two most popular libraries are Typhoon and Objection.

我希望我已經說服你,相依性注入是個簡單的概念。

然而,有一些流行的 frameworks and libraries 讓相依性注入更強大且更容易去管你複雜的專案。

如果你第一次用相依性注入,我會強烈低建議開始使用在這篇教學的 techniques outlined。

首先你需要正確了解它的概念,在你依賴於第三方解決方案之前,如 Typhoon 或 Objection。

Conclusion

這篇的主旨是要讓相依性注入,對於新開發者或不熟悉相依性注入概念的開發者變得更容易理解

我希望我有說服你相依性注入價值所在,and the simplicity of the underlying idea.

這邊有一些很棒的資源關於相依性注入。

James Shore 的相依性注入文章對每一位開發者是必讀的。

Graham Lee 也寫了很棒的文章針對 iOS 和 OS X 開發者。