objc与鸭子对象(上)

原文出处:Sunnyxx(@我就叫Sunny怎么了)

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

我是前言

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

objc与鸭子类型

id类型是个大鸭子

鸭子类型是动态语言的特性,编译时并不决定函数调用关系,说白了所有的类型声明都是给编译器看的。objc在动态和静态方面找到了不错的平衡,既保留了严格的静态检查也没破坏运行时的动态特性。

我们知道,向一个objc对象(或Class)发消息,实际上就是沿着它的isa指针寻找真正函数地址,所以只要一个对象满足下面的结构,就可以对它发送消息:

Objective-C

1

2

3structobjc_object{

Classisa;

}*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中经典的鸭子类型使用场景:

Objective-C

1

2@property(nonatomic,assign)idUITableViewDataSource>dataSource;

@property(nonatomic,assign)idUITableViewDelegate>delegate;

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

Objective-C

1

2@protocolUIScrollViewDelegateNSObject>

@protocolUITableViewDelegateNSObject,UIScrollViewDelegate>

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

Objective-C

1

2

3

4

5

6

7

8@implementationDataSource

+(NSInteger)numberOfSectionsInTableView:(UITableView*)tableView{

return0;

}

+(NSInteger)tableView:(UITableView*)tableViewnumberOfRowsInSection:(NSInteger)section{

return0;

}

@end

设置table view的data source:

Objective-C

1

self.tableView.dataSource=(ClassUITableViewDataSource>)[DataSourceclass];

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

[Demo]一个类实现Json Entity

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

Objective-C

1

2

3

4

5

6@interfaceXXUserEntity: NSObject

@property(nonatomic,copy)NSString*name;

@property(nonatomic,copy)NSString*sex;

@property(nonatomic,assign)NSIntegerage;

// balabala....

@end

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

Objective-C

1

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

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

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

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

Objective-C

1

2

3

4

5

6

7

8// XXDuckEntity.h

@interfaceXXDuckEntity: NSProxy

-(instancetype)initWithJSONString:(NSString*)json;

@end

// XXDuckEntity.m

@interfaceXXDuckEntity()

@property(nonatomic,strong)NSMutableDictionary*innerDictionary;

@end

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

Objective-C

1

2

3

4

5

6

7

8

9-(instancetype)initWithJSONString:(NSString*)json

{

NSData*data=[jsondataUsingEncoding:NSUTF8StringEncoding];

idjsonObject=[NSJSONSerializationJSONObjectWithData:dataoptions:NSJSONReadingAllowFragmentserror:nil];

if([jsonObjectisKindOfClass:[NSDictionaryclass]]){

self.innerDictionary=[jsonObjectmutableCopy];

}

returnself;

}

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

Objective-C

1

2

3

4

5

6

7

8

9

10

11-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector

{

SELchangedSelector=aSelector;

if([selfpropertyNameScanFromGetterSelector:aSelector]){

changedSelector=@selector(objectForKey:);

}

elseif([selfpropertyNameScanFromSetterSelector:aSelector]){

changedSelector=@selector(setObject:forKey:);

}

return[[self.innerDictionaryclass]instanceMethodSignatureForSelector:changedSelector];

}

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

Objective-C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21-(void)forwardInvocation:(NSInvocation*)invocation

{

NSString*propertyName=nil;

// Try getter

propertyName=[selfpropertyNameScanFromGetterSelector:invocation.selector];

if(propertyName){

invocation.selector=@selector(objectForKey:);

[invocationsetArgument:&propertyNameatIndex:2];// self, _cmd, key

[invocationinvokeWithTarget:self.innerDictionary];

return;

}

// Try setter

propertyName=[selfpropertyNameScanFromSetterSelector:invocation.selector];

if(propertyName){

invocation.selector=@selector(setObject:forKey:);

[invocationsetArgument:&propertyNameatIndex:3];// self, _cmd, obj, key

[invocationinvokeWithTarget:self.innerDictionary];

return;

}

[superforwardInvocation:invocation];

}

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

Objective-C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19-(NSString*)propertyNameScanFromGetterSelector:(SEL)selector

{

NSString*selectorName=NSStringFromSelector(selector);

NSUIntegerparameterCount=[[selectorNamecomponentsSeparatedByString:@":"]count]-1;

if(parameterCount==0){

returnselectorName;

}

returnnil;

}

-(NSString*)propertyNameScanFromSetterSelector:(SEL)selector

{

NSString*selectorName=NSStringFromSelector(selector);

NSUIntegerparameterCount=[[selectorNamecomponentsSeparatedByString:@":"]count]-1;

if([selectorNamehasPrefix:@"set"]&¶meterCount==1){

NSUIntegerfirstColonLocation=[selectorNamerangeOfString:@":"].location;

return[selectorNamesubstringWithRange:NSMakeRange(3,firstColonLocation-3)].lowercaseString;

}

returnnil;

}

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

Objective-C

1

2

3

4

5

6

7

8

9@protocolXXUserEntityNSObject>

@property(nonatomic,copy)NSString*name;

@property(nonatomic,copy)NSString*sex;

@property(nonatomic,strong)NSNumber*age;

@end

@protocolXXStudentEntityXXUserEntity>

@property(nonatomic,copy)NSString*school;

@property(nonatomic,copy)NSString*teacher;

@end

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

Objective-C

1

2

3-(void)requestFinished:(XXDuckEntity*)student{

NSLog(@"name: %@, school:%@", student.name, student.school);

}

至此,所有的entity被表示成了N个的.h文件加一个XXDuckEntity类,剩下的就靠想象力了。

这个demo的源码将在下半部分之后给出

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容