這是《objc與鴨子物件》的上半部分,《objc與鴨子物件(下)》中介紹了鴨子型別的進階用法、依賴注入以及demo。
我是前言
鴨子型別
(Duck Type)即:“當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子”,換成程式猿語言就是:“當呼叫者知道這個物件能呼叫什麼方法時,管它這個物件到底是什麼類的例項呢”。本文對objc中的鴨子型別物件進行簡單探究,並用一個“只用一個類實現Json Entity”的小demo實踐下這個思路的魔力。進階篇請看下半部分。
objc與鴨子型別
id型別是個大鴨子
鴨子型別是動態語言的特性,編譯時並不決定函式呼叫關係,說白了所有的型別宣告都是給編譯器看的。objc在動態和靜態方面找到了不錯的平衡,既保留了嚴格的靜態檢查也沒破壞執行時的動態特性。
我們知道,向一個objc物件(或Class)發訊息,實際上就是沿著它的isa
指標尋找真正函式地址,所以只要一個物件滿足下面的結構,就可以對它傳送訊息:
1 2 3 |
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中經典的鴨子型別使用場景:
1 2 |
@property (nonatomic, assign) id UITableViewDataSource> dataSource; @property (nonatomic, assign) id UITableViewDelegate> delegate; |
利用鴨子型別設計的介面會給使用者更大的靈活度。同時@protocol
可以用來建立偽繼承
關係
1 2 |
@protocol UIScrollViewDelegateNSObject> @protocol UITableViewDelegateNSObject, UIScrollViewDelegate> |
協議的存在一方面是給
NSProxy
這樣的其他根類使用,同時也給了鴨子協議型別一個根型別,正如給了大部分類一個NSObject根類一樣。說個小插曲,由於objc中Class也是id
型別,形如id
的鴨子型別是可以用Class物件來扮演的,只需要把例項方法替換成類方法,如:
1 2 3 4 5 6 7 8 |
@implementation DataSource + (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 0; } + (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 0; } @end |
設定table view的data source:
1 |
self.tableView.dataSource = (ClassUITableViewDataSource>)[DataSource class]; |
這種非主流寫法合法且執行正常,歸功於objc中加號和減號方法在@selector
中並未體現,在@protocol
中也是形同虛設,這種程式碼我相信沒人真的寫,但確實能體現鴨子型別的靈活性。
[Demo]一個類實現Json Entity
Entity
物件表示某個純資料的結構,如:
1 2 3 4 5 6 |
@interface XXUserEntity : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *sex; @property (nonatomic, assign) NSInteger age; // balabala.... @end |
實際開發中這種類往往對應著server端返回的一個JSON串,如:
1 |
{"name": "sunnyxx", "sex": "boy", "age": 24, ...} |
解析這些對映是個純重複工作,建類、寫屬性、解析…如今已經有JSONModel,Mantle等不錯的框架幫忙。這個demo我們要用鴨子型別的思想去重新設計,把這些Entity類簡化成一個鴨子類。
由於上面的UserEntity
類,只有屬性的getter和setter,這正對應了NSMutableDictionary
的objectForKey:
和setObjectForKey:
,同時,JSON資料也會解析成字典,這就完成了巧妙的對接,下面去實現這個類。
真正幹活的是一個字典,保證封裝性和純粹性,這個類直接使用NSProxy
作為純代理類,只暴露一個初始化方法就好了:
1 2 3 4 5 6 7 8 |
// XXDuckEntity.h @interface XXDuckEntity : NSProxy - (instancetype)initWithJSONString:(NSString *)json; @end // XXDuckEntity.m @interface XXDuckEntity () @property (nonatomic, strong) NSMutableDictionary *innerDictionary; @end |
NSProxy
預設是沒有初始化方法的,也省去了去規避其他初始化方法的麻煩,為了簡單直接初始化時就把json串解開成字典(暫不考慮json是個array):
1 2 3 4 5 6 7 8 9 |
- (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:
1 2 3 4 5 6 7 8 9 10 11 |
- (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]; } |
簽名替換成字典的兩個方法後開始走轉發,在這裡設定引數和對內部字典的真正呼叫:
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 = [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:
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); 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
而非子類化的方式來定義,如:
1 2 3 4 5 6 7 8 9 |
@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 |
當資料從網路層回來時,鴨子型別讓這個物件用起來和真有這麼個類沒什麼兩樣:
1 2 3 |
- (void)requestFinished:(XXDuckEntity *)student { NSLog(@"name: %<a href="http://www.jobbole.com/members/Famous_god">@,</a> school:%@", student.name, student.school); } |
至此,所有的entity被表示成了N個的
.h
檔案加一個XXDuckEntity
類,剩下的就靠想象力了。
這個demo的原始碼將在下半部分之後給出
Reference
http://en.wikipedia.org/wiki/Duck_typing
http://www.informit.com/articles/article.aspx?p=1353396
https://github.com/facebook/facebook-ios-sdk