利用Objective-C runtime构造可扩展的对象工厂中心

对象工厂中心是我生造的一个词,指的是整个程序中,一类对象由唯一的对象工厂创建。

我们用一个实际的场景来说明。假设要实现一个IM模块,为App提供即时通讯支持。我们需要定义消息数据,并处理消息的发送和接收。在Objective-C的世界中,可以定义消息基类,然后定义一组子类表示不同类型的消息,比如文字,语音,URL链接消息等。而处理网络通信时,需要将对象数据序列化,就不得不用一个type字段来区别不同的消息类型。

序列化很简单,在基类中定义抽象序列化方法,子类中重写它,输出相应的序列化字符串。但在序列化过程中,消息对象的多态性丢失了,反序列化就需要一个对象工厂,依据type字段来确定需要创建的消息对象的类型。

最简单的工厂实现,就是一串if-else:

if ([type isEqualToString:@"typeA"]){
    return [[ModelA alloc] init];
}else if ([type isEqualToString:@"typeB"]){
    return [[ModelB alloc] init];
}
...

问题是,每增加一种消息类型,就需要修改工厂的实现,增加一个if case。这不符合对扩展开放,对修改关闭的原则。而且工厂类需要依赖所有的数据子类。

不同的消息可能由不同的模块处理。如果某个业务模块需要增加一种消息类型,我们当然不希望这种业务逻辑入侵到下层的IM模块,最好能让业务模块管理自己的消息子类,IM模块在不知道消息子类名字的情况下正确创建消息对象,返回给上层的业务模块。而对象初始化必须知道对象的类型,怎么办呢?

我们可以利用Objective-C的动态特性。在Objective-C中,对象的类型由Class对象来描述,可以给Class发送alloc消息来创建对应的对象。而且利用Objective-C runtime,Class和NSString可以相互转化。

在工厂类内部维护一个字典,保存type字段和对应的类型名称字符串,并对外开放一个注册Class的接口。上层业务模块将自己的子类注册到到工厂类,这样就能够通过type字段得到对应的Class,进而正确地进行对象初始化。

尝试实现一下。定义MessageBase作为消息基类,以及默认的TextMessage。定义MessageFactory作为消息工厂。

Message.h



#import <Foundation/Foundation.h>

static NSString * const MessageTypeText = @"text";

@interface MessageBase : NSObject

@property(nonatomic, strong, readonly)NSString *messageType;

- (instancetype)initWithJsonDict:(NSDictionary *)jsonDict;

@end

@interface TextMessage : MessageBase

@property (nonatomic, strong, readonly)NSString *content;

@end

MessageFactory.h

#import <Foundation/Foundation.h>
#import "Message.h"

@interface MessageFactory : NSObject

+ (void)registerMessageClass:(Class)messageClass forType:(NSString *)messageType;

+ (__kindof MessageBase *)messageByJsonDict:(NSDictionary *)jsonDict;

@end

MessageFactory.m

#import "MessageFactory.h"
#import "Message.h"

static NSString * const MessageTypeKey = @"message_type";

static NSMutableDictionary<NSString *, NSString *>* messageTypeDict;

@implementation MessageFactory

+ (void)registerMessageClass:(Class)messageClass forType:(NSString *)messageType
{
    if (!messageTypeDict) {
        messageTypeDict = [[NSMutableDictionary alloc] init];
    }
    NSString *className = NSStringFromClass(messageClass);
    [messageTypeDict setObject:className forKey:messageType];
    NSLog(@"register class %@ for type %@", className, messageType);
}

+ (__kindof MessageBase *)messageByJsonDict:(NSDictionary *)jsonDict
{
    NSString *type = jsonDict[MessageTypeKey];
    if (!type || ![type isKindOfClass:[NSString class]]) {
        return nil;
    }
    NSString *className = messageTypeDict[type];
    if (!className) {
        return nil;
    }
    MessageBase *message = [(MessageBase *)[NSClassFromString(className) alloc] initWithJsonDict:jsonDict];
    return message;
}

@end

现在,我们定义新的message类型后,只需要在程序开始时调用registerMessageClass:forType:接口注册,MessageFactory就能用相应的字典数据初始化它了。

但调用注册接口还是有些麻烦,也可能会忘记调用。好在NSObject有一个很方便的+(void)load方法。它会在一个类被加载到runtime时调用,父类的方法先被调用,然后子类和cateogory的方法被调用。

我们可以在load方法中完成对象类型的注册。如果需要支持新的类型,只需要给MessageFactory写一个category,实现自己的load方法,在里面注册新的类型即可。比如我们要默认注册TextMessage,并增加对超链接消息LinkMessage的支持:

MessageFactory.m

+ (void)load
{
    if (!messageTypeDict) {
        messageTypeDict = [[NSMutableDictionary alloc] init];
    }
    [self registerMessageClass:[TextMessage class] forType:MessageTypeText];
}

MessageFactory+LinkMessage.m

#import "MessageFactory+LinkMessage.h"
#import "LinkMessage.h"

@implementation MessageFactory (LinkMessage)

+ (void)load
{
    [self registerMessageClass:[LinkMessage class] forType:MessageTypeLink];
}

@end

我们在registerMessageClass:forType:方法、main函数和application: didFinishLaunchingWithOptions:中加了log,可以在程序运行时看到类型注册发生的时间:

2016-11-28 00:29:53.718 FactoryDemo[49999:7451402] register class TextMessage for type text
2016-11-28 00:29:53.723 FactoryDemo[49999:7451402] register class LinkMessage for type link
2016-11-28 00:29:53.723 FactoryDemo[49999:7451402] main called
2016-11-28 00:29:53.846 FactoryDemo[49999:7451402] application didFinishLaunchingWithOptions

可以看到load调用发生在程序启动之前。

简单测试一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSString *textMessageJson = @"{\"message_type\":\"text\",\"content\":\"hello\"}";
    NSString *linkMessageJson = @"{\"message_type\":\"link\",\"link\":\"http://www.baidu.com\"}";
    NSDictionary *textMessageDict = [NSJSONSerialization JSONObjectWithData:[textMessageJson dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
    NSDictionary *linkMessageDict = [NSJSONSerialization JSONObjectWithData:[linkMessageJson dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
    TextMessage *textMessage = [MessageFactory messageByJsonDict:textMessageDict];
    NSLog(@"text message content:%@",textMessage.content);
    LinkMessage *linkMessage = [MessageFactory messageByJsonDict:linkMessageDict];
    NSLog(@"link message url:%@",linkMessage.linkUrl);
    
}

如我们所料,成功创建了TextMessageLinkMessage对象:

2016-11-28 00:29:53.852 FactoryDemo[49999:7451402] text message content:hello
2016-11-28 00:29:53.852 FactoryDemo[49999:7451402] link message url:http://www.baidu.com

如果……Swift?

在Swift中虽然也能继承NSObject来使用Objective-C的runtime,但这毕竟不是Swift style。

利用Swift的闭包特性,也可以实现可扩展的对象工厂中心。

首先定义一个无参数,返回MessageBase的闭包类型:

typealias MessageCreator = () -> MessageBase

MessageFactory中用字典记录type字段和对应的MessageCreator闭包,客户模块将消息子类的创建写在闭包中,注册给MessageFactory即可。收到消息时,MessageFactory根据type字段取出闭包,然后闭着眼睛调用即可生成相应的消息对象。具体的实现就留给读者自己去完成吧。

完整的demo代码可以从我的Github下载。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,577评论 18 399
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,865评论 6 13
  • 对话式UI只是一种聊天机器人的体验,它以一种自然的方式处理语言,就好像你在和另一个人发短信或说话一样。一种典型的技...
    UIPark阅读 577评论 0 0
  • 生老病死 怨憎恨 爱别离 求不得 人间八苦中 人类最畏惧的 似乎是死亡 因为 人类认为 人死如灯灭 一了百了 无...
    十点树洞先生阅读 476评论 0 2