對象工廠中心是我生造的一個詞,指的是整個程序中,一類對象由唯一的對象工廠創建。
我們用一個實際的場景來說明。假設要實現一個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);
}
如我們所料,成功創建了TextMessage
和LinkMessage
對象:
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下載。