这是《objc与鸭子对象》的上半部分,《objc与鸭子对象(下)》中介绍了鸭子类型的进阶用法、依赖注入以及demo。

我是前言


鸭子类型(Duck Type)即:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”,换成程序猿语言就是:“当调用者知道这个对象能调用什么方法时,管它这个对象到底是什么类的实例呢”。本文对objc中的鸭子类型对象进行简单探究,并用一个“只用一个类实现Json Entity”的小demo实践下这个思路的魔力。进阶篇请看下半部分。


objc与鸭子类型

id类型是个大鸭子

鸭子类型是动态语言的特性,编译时并不决定函数调用关系,说白了所有的类型声明都是给编译器看的。objc在动态和静态方面找到了不错的平衡,既保留了严格的静态检查也没破坏运行时的动态特性。
我们知道,向一个objc对象(或Class)发消息,实际上就是沿着它的isa指针寻找真正函数地址,所以只要一个对象满足下面的结构,就可以对它发送消息:

struct objc_object {
Class isa;
} *id;

也就是熟知的id类型,objc在语言层面先天就支持了这个基本的鸭子类型,我们可以将任意一个对象强转为id类型从而向它发送消息,就算它并不能响应这个消息,编译器也无从知晓。
正如这篇文章中对objc对象的简短定义:The best definition for a Smalltalk or Objective-C "object" is "something that can respond to messages. object并非一定是某个特定类型的实例,只要它能响应需要的消息就可以了。

从@interface到@protocol

正如objc先天支持的动态的id类型,@protocol为鸭子类型提供了编译时的强类型检查,实现了Cocoa中经典的鸭子类型使用场景:

@property (nonatomic, assign) id <UITableViewDataSource> dataSource;
@property (nonatomic, assign) id <UITableViewDelegate> delegate;

利用鸭子类型设计的接口会给使用者更大的灵活度。同时@protocol可以用来建立伪继承关系

@protocol UIScrollViewDelegate<NSObject>
@protocol UITableViewDelegate<NSObject, UIScrollViewDelegate>

<NSObject>协议的存在一方面是给NSProxy这样的其他根类使用,同时也给了鸭子协议类型一个根类型,正如给了大部分类一个NSObject根类一样。说个小插曲,由于objc中Class也是id类型,形如id<UITableViewDataSource>的鸭子类型是可以用Class对象来扮演的,只需要把实例方法替换成类方法,如:

@implementation DataSource
+ (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 0;
}
+ (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 0;
}
@end

设置table view的data source:

self.tableView.dataSource = (Class<UITableViewDataSource>)[DataSource class];

这种非主流写法合法且运行正常,归功于objc中加号和减号方法在@selector中并未体现,在@protocol中也是形同虚设,这种代码我相信没人真的写,但确实能体现鸭子类型的灵活性。


[Demo]一个类实现Json Entity

Entity对象表示某个纯数据的结构,如:

@interface XXUserEntity : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) NSInteger age;
// balabala....
@end

实际开发中这种类往往对应着server端返回的一个JSON串,如:

{"name": "sunnyxx", "sex": "boy", "age": 24, ...}

解析这些映射是个纯重复工作,建类、写属性、解析…如今已经有JSONModelMantle等不错的框架帮忙。这个demo我们要用鸭子类型的思想去重新设计,把这些Entity类简化成一个鸭子类。

由于上面的UserEntity类,只有属性的getter和setter,这正对应了NSMutableDictionaryobjectForKey:setObjectForKey:,同时,JSON数据也会解析成字典,这就完成了巧妙的对接,下面去实现这个类。

真正干活的是一个字典,保证封装性和纯粹性,这个类直接使用NSProxy作为纯代理类,只暴露一个初始化方法就好了:

// XXDuckEntity.h
@interface XXDuckEntity : NSProxy
- (instancetype)initWithJSONString:(NSString *)json;
@end
// XXDuckEntity.m
@interface XXDuckEntity ()
@property (nonatomic, strong) NSMutableDictionary *innerDictionary;
@end

NSProxy默认是没有初始化方法的,也省去了去规避其他初始化方法的麻烦,为了简单直接初始化时就把json串解开成字典(暂不考虑json是个array):

- (instancetype)initWithJSONString:(NSString *)json
{
NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if ([jsonObject isKindOfClass:[NSDictionary class]]) {
self.innerDictionary = [jsonObject mutableCopy];
}
return self;
}

NSProxy可以说除了重载消息转发机制外没有别的用法,这也是它被设计的初衷,自己什么都不干,转给代理对象就好。往这个proxy发消息是注定会走消息转发的,首先判断下是不是一个getter或setter的selector:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
SEL changedSelector = aSelector;
if ([self propertyNameScanFromGetterSelector:aSelector]) {
changedSelector = @selector(objectForKey:);
}
else if ([self propertyNameScanFromSetterSelector:aSelector]) {
changedSelector = @selector(setObject:forKey:);
}
return [[self.innerDictionary class] instanceMethodSignatureForSelector:changedSelector];
}

签名替换成字典的两个方法后开始走转发,在这里设置参数和对内部字典的真正调用:

- (void)forwardInvocation:(NSInvocation *)invocation
{
NSString *propertyName = nil;
// Try getter
propertyName = [self propertyNameScanFromGetterSelector:invocation.selector];
if (propertyName) {
invocation.selector = @selector(objectForKey:);
[invocation setArgument:&propertyName atIndex:2]; // self, _cmd, key
[invocation invokeWithTarget:self.innerDictionary];
return;
}
// Try setter
propertyName = [self propertyNameScanFromSetterSelector:invocation.selector];
if (propertyName) {
invocation.selector = @selector(setObject:forKey:);
[invocation setArgument:&propertyName atIndex:3]; // self, _cmd, obj, key
[invocation invokeWithTarget:self.innerDictionary];
return;
}
[super forwardInvocation:invocation];
}

当然还有这两个必不可少的从getter和setter中获取属性名的Helper:

- (NSString *)propertyNameScanFromGetterSelector:(SEL)selector
{
NSString *selectorName = NSStringFromSelector(selector);
NSUInteger parameterCount = [[selectorName componentsSeparatedByString:@":"] count] - 1;
if (parameterCount == 0) {
return selectorName;
}
return nil;
}
- (NSString *)propertyNameScanFromSetterSelector:(SEL)selector
{
NSString *selectorName = NSStringFromSelector(selector);
NSUInteger parameterCount = [[selectorName componentsSeparatedByString:@":"] count] - 1;
if ([selectorName hasPrefix:@"set"] && parameterCount == 1) {
NSUInteger firstColonLocation = [selectorName rangeOfString:@":"].location;
return [selectorName substringWithRange:NSMakeRange(3, firstColonLocation - 3)].lowercaseString;
}
return nil;
}

一个简单的鸭子Entity就完成了,之后所有的Entity都可以使用@protocol而非子类化的方式来定义,如:

@protocol XXUserEntity <NSObject>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, strong) NSNumber *age;
@end
@protocol XXStudentEntity <XXUserEntity>
@property (nonatomic, copy) NSString *school;
@property (nonatomic, copy) NSString *teacher;
@end

当数据从网络层回来时,鸭子类型让这个对象用起来和真有这么个类没什么两样:

- (void)requestFinished:(XXDuckEntity<XXStudentEntity> *)student {
NSLog(@"name: %@, school:%@", student.name, student.school);
}

至此,所有的entity被表示成了N个<Protocol>.h文件加一个XXDuckEntity类,剩下的就靠想象力了。
这个demo的源码将在下半部分之后给出

http://blog.sunnyxx.com/2014/08/24/objc-duck/

objc与鸭子对象(上)的更多相关文章

  1. 通过jQuery Ajax使用FormData对象上传文件

    FormData对象,是可以使用一系列的键值对来模拟一个完整的表单,然后使用XMLHttpRequest发送这个"表单". 在 Mozilla Developer 网站 使用For ...

  2. 在Function对象上扩展method方法

    ;(function() { /** * 在Function对象上扩展method方法 * @param {String} name 扩展的方法名称 * @param {Function} callb ...

  3. Javascript进阶篇——( JavaScript内置对象---上-Date,string,charAt,indexOf,split,substring,substr)笔记整理

    什么是对象JavaScript 中的所有事物都是对象,如:字符串.数值.数组.函数等,每个对象带有属性和方法.对象的属性:反映该对象某些特定的性质的,如:字符串的长度.图像的长宽等:对象的方法:能够在 ...

  4. Effective JavaScript Item 36 实例状态仅仅保存在实例对象上

    本系列作为EffectiveJavaScript的读书笔记. 一个类型的prototype和该类型的实例之间是"一对多"的关系.那么,须要确保实例相关的数据不会被错误地保存在pro ...

  5. IT轮子系列(四)——使用Jquery+formdata对象 上传 文件

    前言 在MVC 中文件的上传,一般都采用控件: <h2>IT轮子四——文件上传</h2> <div> <input type="file" ...

  6. FormData 对象上传二进制文件

    使用jQuery 利用 FormData 上传文件:http://harttle.com/2016/07/04/jquery-file-upload.html     通过FormData对象可以组装 ...

  7. WinForm 绑定到嵌套对象上的属性

    WinFrom 绑定到嵌套对象上的属性 关键字: Windows Forms, DataBindings, Nested Class, 嵌套类 在 WinForm 中很早就已经支持数据绑定, 使用数据 ...

  8. 通过jQuery Ajax使用FormData对象上传文件 (转载)

    XMLHttpRequest Level 2 添加了一个新的接口——FormData.与普通的 Ajax 相比,使用 FormData 的最大优点就是我们可以异步上传二进制文件.jQuery 2.0+ ...

  9. [转] 通过jQuery Ajax使用FormData对象上传文件

    FormData对象,是可以使用一系列的键值对来模拟一个完整的表单,然后使用XMLHttpRequest发送这个"表单". 在 Mozilla Developer 网站 使用For ...

随机推荐

  1. 【disruptor】1、关于disruptor中的SequenceBarrier对象

    首先这个类的uml结构在disruptor中是这样的,里面只有部分的属性对象和函数内容,具体有什么作用,用到了再说,用不到我也不会... 1.那么这个对象有什么用呢? 注意我们这个类中有哪些属性: 我 ...

  2. 搭建Golang开发环境

    Go语言是谷歌 2009 年首次推出并在 2012 年正式发布的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性.谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 ...

  3. bigdata-02-hadoop2.8.4-resourceHA安装

    1, 电脑环境准备 1), 关闭selinux vim /etc/selinux/config SELINUX=disabled 2), 时间同步 yum -y install chrony 修改时间 ...

  4. html中img图片居中

    直接看代码 style="display:block; margin:0 auto;" 可以看到,蓝色的代码是让 块状元素居中 而红色的代码,是让img转成块状元素 ···原来im ...

  5. python采用pika库使用rabbitmq总结,多篇笔记和示例

    这一段时间学习了下rabbitmq,在学习的过程中,发现国内关于python采用pika库使用rabbitmq的资料很少,官网有这方面的资料,不过是都英文的.于是笔者结合自己的理解,就这方面内容写了一 ...

  6. [Golang] struct Tag说明

    在处理json格式字符串的时候,经常会看到声明struct结构的时候,属性的右侧还有小米点括起来的内容.形如 type User struct { UserId int `json:"use ...

  7. js实现四叉树算法

    最近在看canvas动画方面教程,里面提到了采用四叉树检测碰撞.之前也看到过四叉树这个名词,但是一直不是很懂.于是就又找了一些四叉树方面的资料看了看,做个笔记,就算日后忘了,也可以回来看看. Quad ...

  8. Core Animation之基础介绍

    了解了图层,现在学习核心动画. Core Animation是直接作用在CALayer上的,并非UIView. 一.使用步骤 1.使用它需要先添加QuartzCore.framework框架和引入主头 ...

  9. 客服端与服务端APP支付宝支付接口联调的那些坑

    根据支付宝官方提供的文档的建议: TIPS:这一步应在商户服务端完成,商户服务端直接将组装和签名后的请求串orderString传给客户端,客户端直接传给SDK发起请求.文档和Demo是为了示例效果在 ...

  10. SQL - 先安装SQL2008 R2后安装AD导致无法正常登陆数据库(无法启动MSSQLSERVER)

    分析原因:安装AD后,系统改为使用域用户登陆,原先安装SQL时设置的“本地用户”信息已经修改,当前(域)用户没有权限访问MSSQLSERVER实例文件夹或整个SQL文件夹. 解决方法: 1.打开“服务 ...