IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    objc与鸭子对象(下)

    sunnyxx (sunyuan1713@gmail.com)发表于 2014-09-03 15:49:58
    love 0

    我是前言

    这是《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(@"%@, %@, %@", 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/



沪ICP备19023445号-2号
友情链接