这是《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 |
回想上一篇中的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/* |
被注释掉是因为真实使用场景会造成类型不匹配造成编译警告,所以caller使用起来:
1 2 3 4 | NSString *json = @"{\"name\": \"sunnyxx\", \"sex\": \"boy\", \"age\": 24}"; id |
这样重构的鸭子对象不仅隐藏了内部实现是个字典的事实,连它究竟是什么Class都隐藏了,但程序运行并无影响,骗一骗编译器罢了。不过这个思路的改变确引出另一个技术思路,那就是依赖注入
。
Dependency Injection
,简称DI
,其实在这个场景下叫动态实现注入
更合适。它的思想是将一个“对象”分成三部分,protocol、proxy和implementation,试想有两个协议,他们定义了彼此间该如何发送message:
运行时他们都是由proxy对象扮演:
但DI Proxy并不能响应任何message,真正的实现是动态被“注入”到Proxy中的:
由于调用层只有协议没有类名,所以Implement A
实现类并不依赖Implement B
,就像贩毒团伙的两方只靠小弟来交易,完全不知道幕后大哥是谁,这就是所谓的“面向接口编程”吧。
重点在实现这个Proxy类,按照刚才重构Json Entity类的思路,头文件定义十分精简:
1 2 3 4 5 | // XXDIProxy.h @protocol XXDIProxy <NSObject> - (void)injectDependencyObject:(id)object forProtocol:(Protocol *)protocol; @end extern id/* |
既然都叫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 |
这个简单的demo就完成了。
这个demo的源码
可以->从这里下载<-,have fun.
现在有一个完整的依赖注入框架typhoon,感兴趣的可以把玩一下。
依赖注入不仅可以解耦依赖关系,也可以更好的Test和Mock,想测试某个对象只需要将实现对象注入成Test对象,想造假数据只需要将response对象替换成一个Mock对象,无需修改调用代码,天然不刺激~
PS: 实际使用中可不要过度设计哦。。。
http://c2.com/cgi/wiki?AlanKayOnMessaging
http://www.typhoonframework.org/