利用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下載。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,869評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,754評論 18 399
  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,123評論 6 13
  • 對話式UI只是一種聊天機器人的體驗,它以一種自然的方式處理語言,就好像你在和另一個人發短信或說話一樣。一種典型的技...
    UIPark閱讀 604評論 0 0
  • 生老病死 怨憎恨 愛別離 求不得 人間八苦中 人類最畏懼的 似乎是死亡 因為 人類認為 人死如燈滅 一了百了 無...
    十點樹洞先生閱讀 484評論 0 2