我是前言
這是《objc與鴨子物件》的下半部分,《objc與鴨子物件(上)》中介紹了鴨子型別和它在objc中的實踐,以及一個使用NSProxy實現JSON Entity的鴨子類。下半部分介紹鴨子物件的進階用法,並簡單介紹由鴨子物件思想衍生出的依賴注入
,實現一個demo。
被誤解了的物件導向
Smalltalk之父或者說物件導向之父(之一)的Alan Kay曾寫過:
I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging” – that is what the kernal of Smalltalk/Squeak is all about.
物件導向的思想的核心並不在於object
或者class
,而在於message
,或者說物件和類只是訊息的載體。物件導向思想將程式按功能和邏輯分成了若干個類,每個類包含自己的程式碼、功能實現並提供對外介面,以黑箱模式執行,使得外部無需瞭解內部而協同使用,分解了複雜度並控制在一個個較小規模中,以訊息作為其間所有的協作方式。
回到主題,理解了message才是全部,鴨子物件又可以更近一層,試想下整個程式,每個類除了知道自己的類之外其他類名一無所知,全部通過協議發訊息:
1 2 3 4 5 6 7 |
- (void)schoolWillStart:(id)school { id teacher = school.teacher; for (id student in scholl.allStudents) { [student handIn:student.homework to:teacher]; } } |
Json Entity的重構
回想上一篇中的JSON Entity類:
1 2 3 4 |
// XXDuckEntity.h @interface XXDuckEntity : NSProxy - (instancetype)initWithJSONString:(NSString *)json; @end |
幹嘛caller要知道有這麼個Class
存在呢?它關心的只是能用哪些message通訊而已。於是把類宣告移動到.m
中,簡化成一個C的建立方法(類工廠方法同樣會暴露類名):
1 2 3 4 5 6 7 |
// XXDuckEntity.h extern id XXDuckEntityCreateWithJSON(NSString *json); // XXDuckEntity.m id XXDuckEntityCreateWithJSON(NSString *json) { return [[XXDuckEntity alloc] initWithJSONString:json]; } |
如果這個類需要提供其他message介面供caller使用,則:
1 2 3 4 5 |
@protocol XXDuckEntity NSObject, NSCopying, NSCoding> @property (nonatomic, copy, readonly) NSString *jsonString; - (void)foo; @end extern id/**/ XXDuckEntityCreateWithJSON(NSString *json); |
被註釋掉是因為真實使用場景會造成型別不匹配造成編譯警告,所以caller使用起來:
1 2 3 4 |
NSString *json = @"{\"name\": \"sunnyxx\", \"sex\": \"boy\", \"age\": 24}"; id entity= XXDuckEntityCreateWithJSON(json); id copied = [entity copy]; NSLog(@"%@, %<a href='http://www.jobbole.com/members/Famous_god'>@,</a> %@", copied.jsonString, copied.name, copied.age); |
這樣重構的鴨子物件不僅隱藏了內部實現是個字典的事實,連它究竟是什麼Class都隱藏了,但程式執行並無影響,騙一騙編譯器罷了。不過這個思路的改變確引出另一個技術思路,那就是依賴注入
。
依賴注入
Dependency Injection
,簡稱DI
,其實在這個場景下叫動態實現注入
更合適。它的思想是將一個“物件”分成三部分,protocol、proxy和implementation,試想有兩個協議,他們定義了彼此間該如何傳送message:
執行時他們都是由proxy物件扮演:
但DI Proxy並不能響應任何message,真正的實現是動態被“注入”到Proxy中的:
由於呼叫層只有協議沒有類名,所以Implement A
實現類並不依賴Implement B
,就像販毒團伙的兩方只靠小弟來交易,完全不知道幕後大哥是誰,這就是所謂的“面向介面程式設計”吧。
Let’s demo it
重點在實現這個Proxy類,按照剛才重構Json Entity類的思路,標頭檔案定義十分精簡:
1 2 3 4 5 |
// XXDIProxy.h @protocol XXDIProxy NSObject> - (void)injectDependencyObject:(id)object forProtocol:(Protocol *)protocol; @end extern id/**/ XXDIProxyCreate(); |
既然都叫Proxy了,再不使用NSProxy
類都對不起它了。這個類使用一個字典來儲存被注入的實現物件,以及與protocol的對應關係:
1 2 3 4 5 |
// XXDIProxy.m 這是個私有類 @interface XXDIProxy : NSProxy XXDIProxy> @property (nonatomic, strong) NSMutableDictionary *implementations; - (id)init; @end |
實現協議內容:
1 2 3 4 5 6 7 |
// XXDIProxy.m - (void)injectDependencyObject:(id)object forProtocol:(Protocol *)protocol { NSParameterAssert(object && protocol); NSAssert([object conformsToProtocol:protocol], @"object %@ does not conform to protocol: %@", object, protocol); self.implementations[NSStringFromProtocol(protocol)] = object; } |
關鍵步驟還是訊息轉發,非常簡單,把收到的訊息轉發給能處理的implementation物件(如果用NSObject的forwardingTargetForSelector
將更加簡單):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { for (id object in self.implementations.allValues) { if ([object respondsToSelector:sel]) { return [object methodSignatureForSelector:sel]; } } return [super methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { for (id object in self.implementations.allValues) { if ([object respondsToSelector:invocation.selector]) { [invocation invokeWithTarget:object]; return; } } [super forwardInvocation:invocation]; } |
有了Proxy類,下面是另外兩個角色的測試程式碼,協議:
1 2 3 |
@protocol XXGirlFriend NSObject> - (void)kiss; @end |
實現類(漢字是可以正常編譯執行的- -):
1 2 3 4 |
@interface 林志玲 : NSObject XXGirlFriend> @end @interface 鳳姐 : NSObject XXGirlFriend> @end |
測試程式碼:
1 2 3 4 5 6 7 8 |
林志玲 *implementA = [林志玲 new]; 鳳姐 *implementB = [鳳姐 new]; id gf = XXDIProxyCreate(); [gf injectDependencyObject:implementA forProtocol:@protocol(XXGirlFriend)]; [gf kiss]; // Log: 林志玲 kissed me [gf injectDependencyObject:implementB forProtocol:@protocol(XXGirlFriend)]; [gf kiss]; // Log: 鳳姐 kissed me |
這個簡單的demo就完成了。
這個demo的原始碼
可以->從這裡下載,have fun.
我是後語
現在有一個完整的依賴注入框架typhoon,感興趣的可以把玩一下。
依賴注入不僅可以解耦依賴關係,也可以更好的Test和Mock,想測試某個物件只需要將實現物件注入成Test物件,想造假資料只需要將response物件替換成一個Mock物件,無需修改呼叫程式碼,天然不刺激~
PS: 實際使用中可不要過度設計哦。。。
Reference
http://c2.com/cgi/wiki?AlanKayOnMessaging
http://www.typhoonframework.org/