字典轉(zhuǎn)模型

背景介紹

在iOS開發(fā)中,也不知道是誰(shuí)先起頭的,喜歡用Object-C的動(dòng)態(tài)特性。一個(gè)比較普遍的應(yīng)用是JSON解析之后的Dictionary轉(zhuǎn)自定義的Model,將類class轉(zhuǎn)換為struct,取出其中的成員屬性數(shù)組,然后用一個(gè)循環(huán),跟網(wǎng)絡(luò)收到的dictionary進(jìn)行對(duì)照,省去了model.property = dictionary[key]這些據(jù)說是沒有技術(shù)含量的體力活。感覺上是有點(diǎn)高大上了。
為此,gitHub上還出現(xiàn)了像Mantle這樣比較重的第三方庫(kù),當(dāng)然功能會(huì)很多,比如property和key的名字不必相同,可以復(fù)雜一點(diǎn)array套dictionary再套array,同時(shí)還實(shí)現(xiàn)NSCoding和NSCopying協(xié)議,方便使用序列化,和直接=號(hào)copy
這樣真的好嗎?其實(shí)這些都是Object-C可以和C和C++混編這種方便性給慣出來的毛病。在Object-C中混上C的語(yǔ)句,直接調(diào)用iOS底層的API就是技術(shù)高的體現(xiàn)?可以說是也可以說不是。
在Object-C中混入C,直接調(diào)用最底層的runtime,運(yùn)用了Object-C語(yǔ)言的本質(zhì),貌似應(yīng)該是“高深”的技術(shù),同時(shí)也體現(xiàn)了技術(shù)人員為實(shí)現(xiàn)產(chǎn)品人那些奇葩要求而進(jìn)行的不懈努力,從這個(gè)角度講,應(yīng)該是積極正面的。
從另一個(gè)角度講,這是非常壞的習(xí)慣。程序在實(shí)現(xiàn)功能之后,最重要的一條是“可讀性”,這一條怎么強(qiáng)調(diào)都不過分。僅僅為了程序員“偷懶”,或者炫耀所謂的“技術(shù)性”,將Object-C和C混編,搞得不倫不類,是非常愚蠢可笑的做法,這跟“郭美美炫富”的效果沒什么兩樣。從語(yǔ)言的美感,靈活性,效率等角度來說,至今還沒有哪一個(gè)語(yǔ)言能超越C和C++。那么為什么其他的語(yǔ)言,比如Object-C能很好的發(fā)展呢?就是為了限制靈活性,獲得良好的“可讀性”。在舒服地用著Object-C的同時(shí),嵌入底層的c代碼,這跟程序語(yǔ)言發(fā)展的大趨勢(shì)是相反的,這個(gè)除了“偷懶”和“炫技”以外,基本沒什么其他好處。這類人,在普通開發(fā)者來看,是“高手”;但是在真正的高手看來,只能是“半桶水”而已。
說了這么多,只是為了表明自己的觀點(diǎn),iOS開發(fā)只是應(yīng)用開發(fā),專心用UIKit,WebKit,HeathKit,HomeKit....各種上層API實(shí)現(xiàn)具體業(yè)務(wù)就好了,runtime什么的就讓它在底層默默發(fā)揮作用好了。如果是為了體現(xiàn)所謂的“技術(shù)含量”,那么去做C和C++相關(guān)的開發(fā),或者iOS中的“越獄”開發(fā),將會(huì)是更好的選擇。

當(dāng)前現(xiàn)狀

所謂形勢(shì)比人強(qiáng),現(xiàn)在不用點(diǎn)runtime都不好意思說自己是iOS開發(fā)的,從“軟件工程”的角度講,將這些惡心的代碼限制在一定范圍之內(nèi),比如在某個(gè)framework中,也算是一個(gè)既能堅(jiān)持自己想法,又不顯得過于落伍的折中方案。
當(dāng)前的工程選擇的是AutoCoding這個(gè)第三方庫(kù),以源文件方式放在shared_library文件夾中。這個(gè)庫(kù)只是實(shí)現(xiàn)了NSCoding和NSCopying協(xié)議,字典轉(zhuǎn)模型功能是通過NSObject的category來實(shí)現(xiàn)的,相當(dāng)于手寫。這個(gè)庫(kù)在github上star也只有800多,跟Mantle(9581)、JSONModel(5361)、MJExtention(5519)、YYModel(1846)、FastEasyMapping(419)等相比感覺沒什么優(yōu)勢(shì),不知道當(dāng)初選擇的理由是什么。
YYModel的作者寫一篇測(cè)試的軟文來講這個(gè)問題,感覺還是不錯(cuò)的。基本上我也認(rèn)同的他的觀點(diǎn)。Mantle、JSONModel用基類的方式,侵入性太強(qiáng),MJExtention源文件稍微多了一點(diǎn)。YYModel文件少,各方面表現(xiàn)都不錯(cuò),跟手寫也差不了多少。所以最終要用的話,就選YYModel,以源文件的方式集成在自己的工程中。

考慮的方面

功能角度

  1. 字典轉(zhuǎn)模型

  2. 實(shí)現(xiàn)NSCoding協(xié)議,方便做序列化(緩存)

  3. 實(shí)現(xiàn)NSCopying協(xié)議,能用=,用copy方法

兼容性角度

  1. 類型嵌套

  2. 組數(shù)嵌套

  3. 映射,也就是模型的屬性名和字典的key名稱不一致

  4. 容錯(cuò),屬性的類型和字典value的類型不一致

  5. 速度

  6. 侵入性,是否需要繼承指定的基類

其他考慮

  1. framework是一個(gè)隔離帶,這個(gè)功能在framework外面實(shí)現(xiàn)還是在framework里面實(shí)現(xiàn)

  2. 這部分工作放framework外面由業(yè)務(wù)人員自己實(shí)現(xiàn)還是放在framework內(nèi)部有框架人員實(shí)現(xiàn)?

  3. 如果在framework內(nèi)部實(shí)現(xiàn),如何處理內(nèi)外部類型傳遞的問題。在Object-C中,class類型可以傳遞,但是class的實(shí)現(xiàn)無法在framework內(nèi)部替外部代勞。

  4. 是否提供基類?

討論后的方案

  • 將網(wǎng)絡(luò)返回的response或者error原樣傳遞出去,讓framework外部的調(diào)用者有機(jī)會(huì)自己處理

  • 提供類型參數(shù),能字典轉(zhuǎn)模型的就轉(zhuǎn)了吧

  • 提供自定義的基類作為接口,實(shí)現(xiàn)NSCoding和NSCopying協(xié)議,方便外部使用

  • 手寫實(shí)現(xiàn)這些功能比較麻煩,暫時(shí)不采用

  • 第三方庫(kù)采用YYModel,這樣就可以不對(duì)framework外部暴露第三方庫(kù)頭文件。Mantle雖然很強(qiáng),對(duì)于速度的劣勢(shì)也可以忍受,但是必須繼承基類的使用方式在framework的場(chǎng)景下實(shí)在不合適。

現(xiàn)有代碼

現(xiàn)有代碼基本上是手寫實(shí)現(xiàn)了這些功能,跟第三方庫(kù)相比還有差距,但是也有足夠的學(xué)習(xí)意義。

NSCoding, NSCopying協(xié)議實(shí)現(xiàn)

#import <Foundation/Foundation.h>

@interface BaseDataModel : NSObject<NSCoding, NSCopying>

@end
#import "BaseDataModel.h"
#import <objc/runtime.h>

@implementation BaseDataModel

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
        id value = [self valueForKey:key];
        if(!value)continue;
        [aCoder encodeObject:value forKey:[NSString stringWithFormat:@"%@",key]];
    }
    free (properties);
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if(self)
    {
        unsigned int outCount, i;
        objc_property_t *properties = class_copyPropertyList([self class], &outCount);
        for (i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];
            NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
            id value = [aDecoder decodeObjectForKey:key];
            if (!value)continue;
            [self setValue:value forKey:key];
        }
        free (properties);
    }
    return self;
}

- (id)copyWithZone:(NSZone *)zone
{
    id copyObject = [[[self class] allocWithZone:zone] init];
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
        id value = [self valueForKey:key];
        if (!value)continue;
        [copyObject setValue:value forKey:key];
    }
    free (properties);
    return copyObject;
}

@end

字典轉(zhuǎn)模型

這部分相對(duì)比較復(fù)雜,不一定能完全看懂,能了解核心原理就可以了。

+ (id)ac_objectsWithArray:(id)array objectClass:(__unsafe_unretained Class)clazz
{
    NSMutableArray * objects = [NSMutableArray array];
    
    for ( NSDictionary * obj in array )
    {
        if ( [obj isKindOfClass:[NSDictionary class]] )
        {
            id convertedObj = [clazz ac_objectWithDictionary:obj];
            if ( convertedObj ) {
                [objects addObject:convertedObj];
            }
        }
        else
        {
            [objects addObject:obj];
        }
    }
    
    return [objects copy];
}

+ (instancetype)ac_objectWithDictionary:(NSDictionary *)dictionary
{
    id object = [[self alloc] init];
    
    NSDictionary * properties = [object codableProperties];
    
    for ( __unsafe_unretained NSString *property in properties )
    {
        id value = dictionary[property];
        Class clazz = properties[property][@"class"];
        Class subClazz = properties[property][@"subclass"];
        
        if ( value )
        {
            id convertedValue = value;
            
            if ( [value isKindOfClass:[NSArray class]] )
            {
                if ( subClazz != NSNull.null ) {
                    convertedValue = [NSObject ac_objectsWithArray:value objectClass:subClazz];
                }
                // TODO: handle else
            }
            else if ( [value isKindOfClass:[NSDictionary class]] )
            {
                convertedValue = [clazz ac_objectWithDictionary:value];
            }
            
            if ( convertedValue && ![convertedValue isKindOfClass:[NSNull class]] )
            {
                if ( [self conformsToProtocol:@protocol(AutoModelCoding)] )
                {
                    convertedValue = [(id<AutoModelCoding>)self processedValueForKey:property originValue:value convertedValue:convertedValue class:clazz subClass:subClazz];
                }
                
                [object setValue:convertedValue forKey:property];
                
                if ( ![convertedValue isKindOfClass:clazz] )
                {
                    //                    @"Expected '%@' to be a %@, but was actually a %@"
                    NSLog( @"The type of '%@' in <%@> is <%@>, but not compatible with expected <%@>, please see detail in the <AutoModelCoding> protocol.", property, [self class], [value class], clazz );
                }
            }
        }
    }
    
    return object;
}

- (NSDictionary *)codableProperties
{
    __autoreleasing NSDictionary *codableProperties = objc_getAssociatedObject([self class], _cmd);
    if (!codableProperties)
    {
        codableProperties = [NSMutableDictionary dictionary];
        Class subclass = [self class];
        while (subclass != [NSObject class])
        {
            [(NSMutableDictionary *)codableProperties addEntriesFromDictionary:[subclass codableProperties]];
            subclass = [subclass superclass];
        }
        codableProperties = [NSDictionary dictionaryWithDictionary:codableProperties];
        
        //make the association atomically so that we don't need to bother with an @synchronize
        objc_setAssociatedObject([self class], _cmd, codableProperties, OBJC_ASSOCIATION_RETAIN);
    }

    return codableProperties;
}
@protocol AutoModelCoding <NSObject>
+ (id)processedValueForKey:(NSString *)key
               originValue:(id)originValue
            convertedValue:(id)convertedValue
                     class:(__unsafe_unretained Class)clazz
                  subClass:(__unsafe_unretained Class)subClazz;
@end

參考文檔

Mantle–國(guó)外程序員最常用的iOS模型&字典轉(zhuǎn)換框架
iOS JSON 模型轉(zhuǎn)換庫(kù)評(píng)測(cè)
Mantle
YYModel
MJExtension
AutoCoding

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容