iOS 抽象工厂模式
什么是抽象工厂模式
简单了解一下
按照惯例,我们先了解一下什么是抽象工厂模式。抽象工厂模式和工厂方法模式很相似,但是抽象工厂模式将抽象发挥的更加极致,是三种工厂模式中最抽象的一种设计模式。抽象工厂模式,也叫做Kit模式,提供了创建一系列相关抽象子类的接口,而无需指定它们具体的类型。
抽象工厂模式中定义了抽象工厂类,抽象工厂类中定义了每个系列的抽象子类创建所需的方法,这些方法对应着不同类型的抽象子类实例化过程。每个工厂子类都对应着一个系列,工厂子类通过重写这些方法来实例化当前系列的抽象子类。
工厂方法模式中抽象子类都是基于同一个抽象类的,是同一个类型的抽象子类,例如加、减、乘、除都属于运算类型。而抽象工厂模式可能会有多个类型的抽象类,抽象子类分别继承自对应类型的抽象类,相同类型的抽象子类都是属于不同系列的。
抽象工厂模式包含四部分:
- 抽象工厂类:定义创建抽象子类的具体行为,根据系列中不同类型的抽象子类可能会有多种行为。
- 工厂子类:继承自抽象工厂类,根据当前抽象子类对应的系列,重写父类定义的对应行为。对应的抽象子类系列不同,行为的实现方式也不同。
- 抽象类:定义当前类型抽象子类的操作,子类继承父类完成具体的操作。在抽象工厂模式中,可能会有多种抽象类的定义。
- 抽象子类:根据当前类型继承自对应的抽象类,并根据系列的不同重写抽象类定义的实现。
我打算先讲一个例子
我们上面讲了系列的概念,这里将会用一个例子来理解系列和抽象类的关系。假设现在需要用Sqlite和CoreData两种不同的方式进行本地持久化,持久化的内容都是用户信息、搜索信息、设置信息三部分。
就拿Sqlite持久化方式来说,Sqlite就是使用Sqlite数据库持久化方式的系列,下面对应着用户信息、搜索信息、设置信息三个类型,每个类型就是一个抽象类。除了Sqlite这种持久化方式外,还有CoreData这种持久化方式,这是两个不同的持久化方式,所以属于两个不同的系列。
Sqlite和CoreData都代表着不同的系列,其下面都分别对应着用户信息、搜索信息、设置信息三个类型的层级,在这种层级关系中,Sqlite的用户信息抽象子类对应着CoreData的用户信息抽象子类,这两个抽象子类都属于同一个类型,继承自同一个抽象类,分别被不同系列的工厂子类创建。在抽象设计模式中,不同系列相同类型的抽象子类都是一一对应的。
Sqlite和CoreData属于不同的系列,所以是两个不同的工厂子类,这两个工厂子类具有相同的行为,就是用户信息、搜索信息、设置信息三部分的数据持久化,这就是三种不同的持久化类型,也就是我们上面说的类型。这三个行为定义在抽象工厂类中,抽象工厂类中定义每个系列的抽象子类创建方法,Sqlite和CoreData继承自抽象工厂类,并分别实现继承过来的抽象子类创建方法。
通过上面的例子,我们可以清晰的理解工厂类、抽象类、系列三者之间的关系,理解这三者的关系可以有助于我们更好的理解抽象设计模式。
和工厂方法模式有什么不同?
在工厂方法模式中,工厂子类负责抽象子类的实例化,每个工厂子类对应着一个抽象子类,且具有唯一性。而在抽象工厂模式中,一个工厂子类代表一个系列,工厂子类根据当前系列对不同类型的抽象子类进行创建。工厂方法模式中工厂子类对应的是一个类型的抽象子类,抽象工厂模式对应的是一个系列的抽象子类。
工厂方法模式一个工厂子类对应一个抽象子类的设计,会有很大的浪费,产生了过多的类。而抽象工厂模式更好的利用了工厂子类,使每个工厂子类对应着一个系列的抽象子类,这种设计非常适用于两个具有相同结构关系,但是分属于不同系列的系列之间的切换。
总之就是,工厂方法模式是针对单个类型的抽象类,而抽象工厂模式是针对具有相同结构的一系列类型的抽象类。
业务场景
在上面讲到了数据持久化的例子,我们的业务场景也根据上面的例子提出。
在iOS中比较常用的数据持久化方案,应该就包括Sqlite和CoreData了,可能Sqlite的灵活性使其更加受欢迎。业务就是需要用Sqlite和CoreData两种不同的方式进行本地持久化,持久化的内容是用户信息、搜索信息、设置信息三部分。
通过抽象工厂模式实现上面的需求,可以很方便的进行本地持久化方案的切换,下面的例子中将会演示一行代码切换数据持久化方案的例子。
UML类图
我们根据上面的业务场景画了一个UML类图,下面类图中为了让大家看得更清晰,所以用不同颜色的线区分开了对应的类和功能。
下面的黑色箭头是抽象子类和抽象类的继承关系;红色是用户工厂子类对应的抽象子类;黄色是搜索工厂子类对应的抽象子类;绿色是设置工厂子类对应的抽象子类。
抽象工厂模式
在这个UML类图中,我们可以清晰的看出,之前工厂方法模式的工厂子类对应的是单一类型的抽象子类,上面抽象工厂模式的工厂子类对应的是同一系列多个类型的抽象子类,更好的利用了工厂子类,适合更加复杂的业务需求。抽象工厂类的方法定义也和工厂方法模式不太一样,由于工厂方法模式只创建一个抽象子类,所以直接用的类方法定义,抽象方法模式可能会创建多个类型的抽象子类,所以用的实例方法定义。
普通方式代码实现
这里代码实现按照上面举的例子,代码结构也完全按照上面UML类图中画的结构,使整篇文章可以更加统一,更深刻的理解这个设计模式。
代码量比较多,但是为了更好的体现出抽象工厂模式,所以就全贴出来了。
首先创建两个Model类,这两个Model类并不属于抽象工厂模式结构的一部分,只是为了更好的体现出面向模型开发。
//.h
@interface User : NSObject
@property (nonatomic, copy ) NSString *userName;
@property (nonatomic, assign) NSInteger userAge;
@end
//.m
@implementation User
@end
//.h
@interface City : NSObject
@property (nonatomic, copy) NSString *cityName;
@property (nonatomic, copy) NSString *cityCode;
@end
//.m
@implementation City
@end
用户信息系列相关类
//.h
@interface UserInfo : NSObject
- (void)setUserName:(User *)name;
@end
//.m
@implementation UserInfo
- (void)setUserName:(User *)name {}
@end
//.h
@interface SqliteUserInfo : UserInfo
@end
//.m
@implementation SqliteUserInfo
- (void)setUserName:(User *)name {
NSLog(@"这里编写数据库持久化方案");
}
@end
//.h
@interface CoreDataUserInfo : UserInfo
@end
//.m
@implementation CoreDataUserInfo
- (void)setUserName:(User *)name {
NSLog(@"这里编写CoreData持久化方案");
}
@end
搜索信息系列相关类
//.h
@interface SearchInfo : NSObject
- (void)setSearchCity:(City *)city;
@end
//.m
@implementation SearchInfo
- (void)setSearchCity:(City *)city {}
@end
//.h
@interface SqliteSearchInfo : SearchInfo
@end
//.m
@implementation SqliteSearchInfo
- (void)setSearchCity:(City *)city {
NSLog(@"这里编写数据库持久化方案");
}
@end
//.h
@interface CoreDataSearchInfo : SearchInfo
@end
//.m
@implementation CoreDataSearchInfo
- (void)setSearchCity:(City *)city {
NSLog(@"这里编写CoreData持久化方案");
}
@end
设置信息系列相关类
//.h
@interface SettingsInfo : NSObject
- (void)resetAllSettings;
@end
//.m
@implementation SettingsInfo
- (void)resetAllSettings {}
@end
//.h
@interface SqliteSettingsInfo : SettingsInfo
@end
//.m
@implementation SqliteSettingsInfo
- (void)resetAllSettings {
NSLog(@"重置数据库设置信息");
}
@end
//.h
@interface CoreDataSettingsInfo : SettingsInfo
@end
//.m
@implementation CoreDataSettingsInfo
- (void)resetAllSettings {
NSLog(@"重置CoreData设置信息");
}
@end
工厂抽象相关类
//.h
@interface Factory : NSObject
- (UserInfo *)CreateUserInfo;
- (SearchInfo *)CreateSearchInfo;
- (SettingsInfo *)CreateSettingsInfo;
@end
//.m
@implementation Factory
- (UserInfo *)CreateUserInfo {
return nil;
}
- (SearchInfo *)CreateSearchInfo {
return nil;
}
- (SettingsInfo *)CreateSettingsInfo {
return nil;
}
@end
//.h
@interface SqliteFactory : Factory
@end
//.m
@implementation SqliteFactory
- (UserInfo *)CreateUserInfo {
return [SqliteUserInfo new];
}
- (SearchInfo *)CreateSearchInfo {
return [SqliteSearchInfo new];
}
- (SettingsInfo *)CreateSettingsInfo {
return [SqliteSettingsInfo new];
}
@end
//.h
@interface CoreDataFactory : Factory
@end
//.m
@implementation CoreDataFactory
- (UserInfo *)CreateUserInfo {
return [CoreDataUserInfo new];
}
- (SearchInfo *)CreateSearchInfo {
return [CoreDataSearchInfo new];
}
- (SettingsInfo *)CreateSettingsInfo {
return [CoreDataSettingsInfo new];
}
@end
外界使用
- (void)viewDidLoad {
User *user = [User new];
City *city = [City new];
Factory *factory = [SqliteFactory new];
UserInfo *userInfo = [factory CreateUserInfo];
SearchInfo *searchInfo = [factory CreateSearchInfo];
SettingsInfo *settingsInfo = [factory CreateSettingsInfo];
[userInfo setUserName:user];
[searchInfo setSearchCity:city];
[settingsInfo resetAllSettings];
}
到此为止我们就编写完抽象工厂设计模式的代码了,上面抽象工厂模式的示例中定义了三个类型的抽象类UserInfo、SearchInfo、SettingsInfo,抽象子类分别继承不同类型的抽象类,并实现不同系列的持久化代码。这三个类型的抽象类中定义了两种不同的数据持久化方案,分别是Sqlite存储和CoreData存储,这就是两种数据持久化系列,分别用SqliteFactory和CoreDataFactory表示这两个系列。
代码中定义了抽象工厂类Factory类,并定义了三个抽象接口用来实例化不同类型的抽象子类,两个工厂子类SqliteFactory和CoreDataFactory继承自抽象工厂类,内部分别实现了两种不同系列的抽象子类实例化,例如SqliteFactory中会实例化关于Sqlite存储方式的抽象子类,并通过抽象工厂类中定义的抽象接口返回给外界使用。
统一控制工厂子类的切换
这三种工厂设计模式中除了简单工厂模式,工厂方法模式和抽象工厂模式都需要外界实例化不同的工厂子类,这种在外界实例化工厂子类的代码可能会出现在多个地方,而在很多业务需求中都需要我们统一切换某个功能,从代码上来说就是切换工厂子类,怎样可以做到统一切换工厂子类的需求呢?
就拿上面的持久化方案的例子来说,我们定义了两种持久化方案,通过SqliteFactory和CoreDataFactory工厂子类来创建不同的持久化方案。假设现在我们项目比较庞大,代码量比较多,并且在多个地方用到了SqliteFactory工厂子类,现在需求是切换为CoreDataFactory的持久化方案,我们应该怎样快速的切换持久化方案?
其实我们可以通过typedef定义别名的方式切换工厂子类,在其他地方只需要使用我们typedef定义的别名就可以,例如下面代码就可以做到修改一处typedef定义,就修改了整个项目的持久化方案。
还是按照上面的抽象工厂模式的代码,这里只写出外界使用的代码部分
typedef SqliteFactory SaveFactory; //定义的工厂子类别名
@implementation TestTableViewController
- (void)viewDidLoad {
User *user = [User new];
City *city = [City new];
Factory *factory = [SaveFactory new];
UserInfo *userInfo = [factory CreateUserInfo];
SearchInfo *searchInfo = [factory CreateSearchInfo];
SettingsInfo *settingsInfo = [factory CreateSettingsInfo];
[userInfo setUserName:user];
[searchInfo setSearchCity:city];
[settingsInfo resetAllSettings];
}
从上面的代码可以看到,我们定义了一个SaveFactory的工厂子类别名,下面直接通过这个别名进行的工厂子类的实例化。因为如果存储相同的内容,项目中只会出现一种持久化方案,所以我们只需要修改typedef的定义,就可以切换整个项目的持久化方案。
配合反射机制优化代码
对于抽象工厂模式的反射机制,实现方式和之前的简单工厂模式不太一样,我采用的是预编译指令加常量字符串类名反射的方式实现的。别的不多说,先上代码来看看,我这里贴出了主要代码,其他一样的地方我就不往外贴了,不浪费大家时间。
还是按照上面的业务场景,我定义了两种同名不同值的字符串常量,这些常量字符串对应的就是抽象子类的类名,一个条件分支就是一个系列的抽象子类,通过预编译指令#if来进行不同系列的抽象子类间的统一切换,定义了SAVE_DATA_MODE_SQLITE宏定义来控制系列的切换。这种定义方式可以更方便的进行不同系列间的切换,从使用上来看非常像我们用预编译指令替换了之前的工厂子类,实际上从代码的角度上来说这种方式对系列间的切换更加统一和方便。
#define SAVE_DATA_MODE_SQLITE 1
#if SAVE_DATA_MODE_SQLITE
static NSString * const kUserInfoClass = @"SqliteUserInfo";
static NSString * const kSearchInfoClass = @"SqliteSearchInfo";
static NSString * const kSettingsInfoClass = @"SqliteSettingsInfo";
#else
static NSString * const kUserInfoClass = @"CoreDataUserInfo";
static NSString * const kSearchInfoClass = @"CoreDataSearchInfo";
static NSString * const kSettingsInfoClass = @"CoreDataSettingsInfo";
#endif
下面是工厂类的定义,使用反射机制的抽象工厂模式刨除了工厂子类,只用一个工厂类来进行操作子类的实例化,这种方式和之前的简单工厂模式非常相似。不同的是之前的简单工厂模式只需要初始化一个类型的抽象子类,而抽象工厂模式需要初始化多个类型的抽象子类。
由于我们采用了反射机制,并且由预编译指令进行系列间的切换,所以这里就直接使用类方法了,哪里用就直接实例化抽象子类即可,不存在工厂子类之间的选择问题了。
@interface Factory : NSObject
+ (UserInfo *)CreateUserInfo;
+ (SearchInfo *)CreateSearchInfo;
+ (SettingsInfo *)CreateSettingsInfo;
@end
@implementation Factory
+ (UserInfo *)CreateUserInfo {
return [[NSClassFromString(kUserInfoClass) alloc] init];
}
+ (SearchInfo *)CreateSearchInfo {
return [[NSClassFromString(kSearchInfoClass) alloc] init];
}
+ (SettingsInfo *)CreateSettingsInfo {
return [[NSClassFromString(kSettingsInfoClass) alloc] init];
}
@end
通过这种方式进行不同系列的切换,只需要修改一个宏定义的值即可,也就是SAVE_DATA_MODE_SQLITE后面的1切换为0的步骤,这种方式是符合我们开放封闭原则的。以后每个系列增加新的类型后,只需要将新增加的两个类名对应添加在预编译指令中,在工厂方法中扩展一个实例化新类型的方法即可。这种方式对扩展是开放的,对修改是关闭的。
对于上面的示例代码的编写需要注意一下,按照苹果的命名规范,常量的作用域如果只在一个类中,前面就用小写k修饰,如果修饰的常量在其他类中用到,也就是.h文件中用extern修饰的常量,不需要用小写k修饰。我们在苹果的很多官方代码和Kit中也可以看到相同的定义,宏定义也是相同的规则。(extern修饰的常量在.m中不要用static修饰)
项目中如果用到任何预编译指令,在修改重新运行前,一定要clear一下,清除缓存,否则会因为缓存导致bug。
抽象工厂模式的优缺点
优点
抽象工厂模式正如其名字一样,理解起来非常抽象,正是因为这种抽象,使得抽象工厂模式非常强大和灵活,比其他两种工厂设计模式要强大很多。抽象工厂模式可以创建多个系列,并且每个系列抽象子类一一对应,这种强大的功能是其他两种工厂模式都不能实现的。
通过抽象工厂模式统一控制多个系列的抽象子类,可以用多个系列的抽象子类完成一些复杂的需求。例如我们文中说到的本地持久化方案的切换,最后通过我们的不断优化,做到只需要修改一个预编译指令的参数即可切换整个数据持久化方案,这是其他工厂模式所不能完成的。
抽象工厂模式延续了工厂模式的优点,外界接触不到任何类型的抽象子类,而只需要知道不同类型的抽象类即可,抽象子类的创建过程都在工厂子类中。这种设计方式充分的利用了面向对象语言中的多态特性,使灵活性大大提升。而且抽象工厂模式是非常符合开放封闭原则的,对扩展的开放以及对修改的封闭都完美支持。
缺点
抽象工厂模式带来的缺点也是显而易见的,最明显的缺点就是模式比较庞大,所以需要在适合的业务场景使用这种模式,否则会适得其反。