objc与鸭子对象(上)

这是《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/

时间: 2024-11-08 11:55:01

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

把mysql表结构映射到python对象上

ORM mysql的表结构是二维表,用python的数据结构表示出来就是一个列表,每一个记录是一个tuple.如下所示: [('1', ''huangyi),('2', ''letian),('3', 'xiaosi')] 这一行并不便于看出表的结构,可以把它换成对象的形式. class User(object): def __init__(self, id, name): self.id = id self.name = name 得到: [ User('1', 'huangyi'), Use

在其他对象上同步

synchronized 块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this). 在此种方式中,如果获得了synchronized块上同步锁,那么该对象其他的synchronized方法和临界区就不能被调用.因此,如果再this上同步, 临界区的效果就会直接缩小在同步的范围内. 但有时必须在另一个对象上同步,如果这么做,就必须确保所有相关的任务都是在对象上同步的.例如下面例子俩个任务可以同时进入同一个 对象,只要此对象上的方

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象. 语法EDIT Object.defineProperty(obj, prop, descriptor) 参数 obj 需要定义属性的对象. prop 需定义或修改的属性的名字. descriptor 将被定义或修改的属性的描述符. 返回值 返回传入函数的对象,即第一个参数obj 描述EDIT 该方法允许精确添加或修改对象的属性.一般情况下,我们为对象添加属性是通过

JavaScript面向对象编程(2)对象上的特殊属性

js对象都有constructor属性,但是构造函数产生的对象和简单对象的属性有些不同,可自行运行下列代码了解 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml&q

详解AJAX核心 —— XMLHttpRequest 对象 (上)

我要说的内容都是非常基础的内容,高手就免看了,如果看了欢迎给点意见啊.新手或者对低层还不是很了解的人可以看看,帮助理解与记忆. XMLHttpRequest 对象是AJAX功能的核心,要开发AJAX程序必须从了解XMLHttpRequest 对象开始. 了解XMLHttpRequest 对象就先从创建XMLHttpRequest 对象开始,在不同的浏览器中创建XMLHttpRequest 对象使用不同的方法: 先看看IE创建XMLHttpRequest 对象的方法(方法1): var xmlht

java线程 在其他对象上同步、线程本地存储ThreadLocal:thinking in java4 21.3.6

package org.rui.thread.concurrency; /** * 在其他对象上同步 * synchronized 块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象 * :synchronized(this), 在 这种方式中,如果获得了synchronized块上的锁, * 那么该对象其他的synchronized方法和临界区就不能被调用了. * 因此,如果在this上同步,临界区的效果就会直接缩小在同步的范围内. * * 有时必须在另一个

Java Object 对象上的wait(),notify(),notifyAll()方法理解

第一阶段理解(2017-7-27): Java 将wait(),notify(),notifyAll()方法放在Object对象上,也就是说任何一个对象都可以调用这个方法,这与”任何一个对象都有一个内置锁,可以用于线程同步“是照应的.因此,当某个线程要释放cpu争夺权,让自己进入等待状态时,调用 某个锁(对象)上的wait()方法,也就是让当前线程等待其它线程调用该锁上的notify()或notify()方法.线程间通过同步锁来实现等待与唤醒协作.简单例子: 1 package com.lyyc

从头认识java-18.6 synchronized在其他对象上同步和ThreadLocal来消除共享对象的同步问题

这一章节我们来介绍在其他对象上同步与ThreadLocal. 前一章节我们使用了 1.synchronized在其他对象上同步 class ThreadA implements Runnable { private Object object = new Object(); private synchronized void test() throws InterruptedException { System.out.println("dosomething"); Thread.sl

Effective JavaScript Item 36 实例状态只保存在实例对象上

本系列作为EffectiveJavaScript的读书笔记. 一个类型的prototype和该类型的实例之间是"一对多"的关系.那么,需要确保实例相关的数据不会被错误地保存在prototype之上.比如,对于一个实现了树结构的类型而言,将它的子节点保存在该类型的prototype上就是不正确的: function Tree(x) { this.value = x; } Tree.prototype = { children: [], // should be instance stat