今天學習下最常見的工廠模式,工廠模式細分下來有三大類:
1. 簡單工廠
2. 工廠模式
3. 抽象工廠模式複製程式碼
他們的目標都是一樣的:封裝物件的建立
。但是實現手段和使用場景卻是不相同。使用的時候三個模式也可以互相替換使用,導致很容易混淆三者。
下面我們來具體看看三者的使用。
簡單工廠模式
準確的說簡單工廠不是一個模式,而是一種程式設計習慣。但是平時使用的非常多,我們就把他歸到模式一類了。
1、定義
提供一個建立物件例項的功能,而無需關心具體實現。被建立的型別可以使介面、抽象類、具體類。
2、UML結構圖及說明
obstractClass:可以實現為抽象類或者具體介面,看實際需要選擇,定義具體類需要實現的功能
concreteClass:實現抽象類所定義功能的具體類,可能會有多個
simpleFactory:簡單工廠,選擇合適的具體類來建立物件返回
client:通過simplefactory來獲取具體的物件
如果對UML圖不瞭解,可以先看看這篇文章:UML類圖幾種關係的總結
3、實際場景運用
3.1、需求
假設我們要實現一個電腦組裝的功能,組裝電腦很重要的一個地方就是根據客戶指定的cpu型別來安裝。假設我們有三種型別的cpu供客戶選擇:apple,intel,AMD。
3.2、普通實現
在客戶端加入如下方法:
client.m檔案
=====================
#import "simpleFactory.h"
#import "interCpu.h"
#import "appleCpu.h"
#import "AMDCpU.h"
@implementation client
-(Cpu *)selectCpuWithType:(NSString *)type{
Cpu *cpu = nil;
if ([type isEqualToString:@"intel"]) {
cpu = [interCpu new];
}else if([type isEqualToString:@"AMD"]){
cpu = [AMDCpU new];
}else{
cpu = [appleCpu new];
}
return cpu;
}
@end複製程式碼
比如像使用inter型別的cpu,只需要如下程式碼:
[self selectCpuWithType@"interCpu"];複製程式碼
這裡我只是展現了核心程式碼,忽略了其他程式碼。你需要建立一個CPU的父類,然後建立三個子類繼承它,分別是interCpu、AMDCpu、appleCpu。
上面的程式碼可以完成功能,根據客戶傳入的type型別來建立相應的cpu具體物件。
3.3、問題
雖然上述程式碼可以完成功能,但是有如下問題:
1、如果要加入其他cpu型別,或者更改cpu型別,那麼必須修改客戶端程式碼。違反了開閉原則(不瞭解的童鞋可以去看設計模式開篇漫談)
2、客戶端知道所有的具體cpu類,耦合度太高。客戶端必須知道所有具體的cpu類,那麼任何一個類的改動都可能會影響到客戶端。
3.4、解決問題
客戶端必須瞭解所有的具體cpu類才能建立物件,但是這會導致上述一系列問題。那麼解決辦法就是把這些物件的建立封裝起來,對客戶端不可見,那麼之後如何改動具體類都不會影響到客戶端。這可以通過簡單工廠來實現。
下面我們來看看使用簡單工廠重寫後的程式碼
引入簡單工廠類:
simpleFactory.h檔案
=======================
#import <Foundation/Foundation.h>
#import "Cpu.h"
@interface simpleFactory : NSObject
-(Cpu *)selectCpuWithType:(NSString *)type;
@end
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
simpleFactory.m檔案
=======================
#import "simpleFactory.h"
#import "interCpu.h"
#import "appleCpu.h"
#import "AMDCpU.h"
@implementation simpleFactory
-(Cpu *)selectCpuWithType:(NSString *)type{
Cpu *cpu = nil;
if ([type isEqualToString:@"intel"]) {
cpu = [interCpu new];
}else if([type isEqualToString:@"AMD"]){
cpu = [AMDCpU new];
}else{
cpu = [appleCpu new];
}
return cpu;
}
@end複製程式碼
客戶端呼叫程式碼:
#import <Foundation/Foundation.h>
#import "simpleFactory.h"
#import "Cpu.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
simpleFactory *factory = [simpleFactory new];
Cpu *cpu = [factory selectCpuWithType:@"interCpu"];
[cpu installCpu];
}
return 0;
}複製程式碼
此時不管是增加還是減少或者修改cpu型別,客戶端程式碼都不用改動,降低了客戶端和具體cpu類的耦合,也遵循了開閉原則
4、反思
細心的一點的童鞋可能發現,你這不是逗我嗎,僅僅是把本來客戶端的程式碼移到了簡單工廠類而已,有什麼改變嗎?
理解這個問題的關鍵在於理解簡單工廠所在的位置。
前面我們把建立具體cpu物件的程式碼放在客戶端,導致一系列問題。我們的目標就是讓客戶端從建立具體物件中解耦出來,讓客戶端不知道物件建立的具體過程。而簡單工廠就是和具體物件封裝在一起,算是一個封裝體內,所以簡單工廠知道具體的實現類是沒有關係的。現在客戶端只要知道簡單工廠和一個抽象類cpu,就可以建立具體物件了,實現瞭解耦。
5、改進
雖然上面使用簡單工廠後,讓客戶端實現瞭解耦,但是如果實現類改變了,我們還是需要需改簡單工廠。有沒有什麼辦法做到即使實現類改變也不需要改變簡單工廠的程式碼呢?
在java中可以使用反射或者IoC/DI來實現,在iOS種我們有更簡單的方法,一個方法足矣,具體見程式碼
-(Cpu *)selectCpuWithType:(NSString *)type{
Cpu *cpu = (Cpu *)[NSClassFromString(type)new];
if ([cpu isKindOfClass:[Cpu class]] && cpu) {
return cpu;
}else{
return nil;
}
}複製程式碼
客戶端程式碼不需要改動,是不是簡單了很多?
6、簡單工廠優缺點
優點
幫助封裝
簡單工廠雖然簡單,但是幫我們實現了封裝物件建立的過程,讓我們可以實現面向介面程式設計。
解耦
客戶端不需要知道具體實現類,也不需要知道建立過程。只需要知道簡單工廠類就可以建立具體物件,實現瞭解耦
缺點
1.增加客戶端複雜度
如果是通過引數來選擇建立具體的物件,那麼客戶端就必須知道每個引數的含義,也就暴露了內部實現
2.不方便擴充套件
如果實現類改變,那麼還是需要修改簡單工廠,可以通過文中的方法來避免這個問題。或者使用下節我們講的工廠方法來解決
7、簡單工廠本質
簡單工廠的本質:選擇實現
簡單的工廠的本質在於選擇,而不是實現,實現是由具體類完成的,不要在簡單工廠完成。簡單工廠的目的是讓客戶端通過自己這個中介者來選擇具體的實現,從而讓客戶端和具體實現解耦,任何實現方面的變化都被簡單工廠遮蔽,客戶端不會知道。
簡單工廠的實現難點在於如何“選擇實現”,前面講到的是靜態傳遞引數。其實還可以在執行過程中從記憶體或者資料庫動態選擇引數來實現,具體程式碼就不演示了,只是讀取引數的方式不同,其他都一樣。
8、何時使用簡單工廠
- 想完全封裝隔離具體實現
讓外部只能通過抽象類或者介面來操作,上面的例子中,就是隻能操作抽象類cpu,而不能操作具體類。此時可以使用簡單工廠,讓客戶端通過簡單工廠來選擇建立具體的類,不需要建立的具體過程。
- 想把建立物件的職責集中管理起來
一個簡單工廠可以建立許多相關或者不相關的物件,所以可以把物件的建立集中到簡單工廠來集中管理。
完整程式碼見文末。
工廠模式
1、問題
讓我們回到最原始的程式碼:
client.m檔案
=====================
#import "simpleFactory.h"
#import "interCpu1179.h"
#import "appleCpu1179.h"
#import "AMDCpU1179.h"
@implementation client
-(Cpu *)selectCpuWithType:(NSString *)type{
Cpu *cpu = nil;
if ([type isEqualToString:@"intel1179"]) {
cpu = [interCpu1179 new];
}else if([type isEqualToString:@"intel753"]){
cpu = [interCpu753 new];
}else if([type isEqualToString:@"AMD1179"]){
cpu = [AMDCpU1179 new];
}else if([type isEqualToString:@"AMD753"]){
cpu = [AMDCpu753 new];
}else if([type isEqualToString:@"apple1179"]){
cpu = [appleCpu1179 new];
}else if([type isEqualToString:@"apple753"]){
cpu = [appleCpu753 new];
}else{
return nil;
}return cpu;
}
@end複製程式碼
仔細看這段程式碼,就會發現一個問題:依賴於具體類。因為必須在這裡完成物件建立,所以不得不依賴於具體類:interCpu、appleCpu、AMDCpu。
這會導致什麼問題呢?簡單來說就是違反了依賴倒置原則,讓高層元件client依賴於底層元件cpu。違反這個原則的後果就是一旦底層元件改動,那麼高層元件也就必須改動,違反了開閉原則。聯絡到上面的這個例子就是如果增加或者修改一個cpu子類,那麼就必須改動上面的程式碼,即使使用了簡單工廠模式,還是要修改簡單工廠的程式碼。
我們先來看看什麼是依賴導致原則:
定義:
要依賴抽象,不要依賴具體
展開來說就是:不能讓高層元件依賴低層元件,而且不管高層還是低層元件,都應該依賴於抽象。
那麼如何才能避免違反這一原則呢?下面有三條建議可以參考下:
- 變數不可以持有具體類的引用,比如new一個物件
- 不要讓類派生自具體類,不然就會依賴於具體類,最好派生自抽象類
- 不要覆蓋基類中已經實現的方法,如果覆蓋了基類方法,就說明該類不適合做基類,基類方法應該是被子類共享而不是覆蓋。
但是要完全遵守上面三條,那就沒法寫程式碼了。所以合適變通才是,而工廠模式就是為了遵循依賴倒置原則而生的。
下面就來看看使用工廠模式如何解決這個問題。
2、定義
定義了一個建立物件的介面,由子類決定例項化哪一個類,讓類的例項化延遲到子類執行。
3、UML結構圖及說明
先記住工廠模式實現了依賴倒置原則,至於如何實現的,暫且按下不表,我們先來看程式碼
4、實際場景運用
還是和簡單工廠的同樣的需求,但是我們根據cpu的針腳個數增加了cpu的分類,比如intelCpu1179、intelCpu753。另外兩個型別的cpu也是如此,分為1179和753兩個型別的cpu。但是這次我們用工廠模式來實現。
定義一個工廠基類,定義一個工廠方法
#import <Foundation/Foundation.h>
#import "Cpu.h"
@interface factory : NSObject
-(Cpu*)createCpuWithType:(NSInteger)type;
@end
=============================
#import "factory.h"
@implementation factory
-(Cpu *)createCpuWithType:(NSInteger)type{
@throw ([NSException exceptionWithName:@"繼承錯誤" reason:@"子類必須重寫該方法" userInfo:nil]);
return nil;
}
@end複製程式碼
下面是具體工廠,繼承自工廠基類,實現工廠方法來建立具體的cpu物件
#import <Foundation/Foundation.h>
#import "factory.h"
@interface intelFactory : factory
@end
===========================
#import "intelFactory.h"
#import "interCpu753.h"
#import "interCpu1179.h"
#import "Cpu.h"
@implementation intelFactory
-(Cpu *)createCpuWithType:(NSInteger)type{
Cpu *cpu = nil;
if (type == 753) {
cpu = [interCpu753 new];
}else{
cpu = [interCpu1179 new];
}
return cpu;
}
@end複製程式碼
上面演示的是intelCpu工廠,另外的AMD和apple的cpu具體工廠類類似,就不貼程式碼了。
客戶端呼叫:
#import <Foundation/Foundation.h>
#import "factory.h"
#import "Cpu.h"
#import "intelFactory.h"
#import "appleFactory.h"
#import "AMDFactory.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
factory *factory = nil;
factory = [intelFactory new];
Cpu *cpu1 = [factory createCpuWithType:753];
[cpu1 installCpu];
Cpu *cpu2 = [factory createCpuWithType:1179];
[cpu2 installCpu];
factory = [AMDFactory new];
Cpu *cpu3 = [factory createCpuWithType:753];
[cpu3 installCpu];
Cpu *cpu4 = [factory createCpuWithType:1179];
[cpu4 installCpu];
}
return 0;
}複製程式碼
如果此時又多了一個cpu型別,比如高通的cpu,那麼只需要新建一個高通cpu的工廠類,繼承自factory類,然後實現工廠方法,就可以了。客戶端也可以根據自己的需要選擇使用哪個工廠,不用修改原有程式碼。符合開閉原則:對修改關閉,對擴充套件開放。
5、如何遵循依賴倒置原則
我們先來看看沒有使用工廠方法,各個類之間的依賴關係
可以看到高層元件client依賴於具體的低層元件cpu類,違反了依賴倒置原則。一般我們把功能的使用者歸到高層元件,把功能的提供者歸到低層元件。
再來看看使用工廠方法後各個類之間的依賴關係
可以看到高層元件client依賴於抽象類cpu,低層元件也就是各種cpu具體類也依賴於抽象類factory,符合依賴倒置原則。其實說白了,就是要針對介面程式設計,而不是針對實現程式設計
。
那麼倒置在哪裡呢?
對比兩個圖,就會發現具體cpu類的箭頭從原來向下變成了向上,也就是說依賴關係發生了倒置。我們來看看為什麼會這樣。
第一個圖裡面,因為我們直接在client裡面去初始化各個cpu類,倒置client就必須依賴這些具體類,依賴關係向下。
第二個圖裡面,每個cpu具體類,都繼承自抽象cpu類,並且實現了抽象cpu的方法installCpu
,此時具體cpu類就依賴於抽象cpu類,依賴關係向上。
現在明白為什麼叫做依賴倒置了吧?這一切都是工廠方法的功勞。
有人要說,這個用簡單工廠也可以實現的呀。是的沒錯,簡單工廠也能實現,其實如果直接在工廠方法的抽象cpu類裡面實現物件的建立,那麼此時工廠模式就是簡單工廠。但是工廠模式有一個簡單工廠模式沒有的功能:遵循開閉原則。如果此時要增加或者修改一個cpu具體類,那麼簡單工廠的程式碼就必須修改,而工廠方法只需要擴充套件就行了,不用修改原有程式碼。
6、工廠模式優缺點
優點
可以在不知道具體實現的情況下程式設計
工廠模式可以讓你在實現功能時候,不需要關心具體物件,只需要使用物件的抽象介面即可,上面例子中client使用的就是cpu抽 象類,而不是具體的cpu類。
更容易擴充套件新版本
如果需要加入新的實現,只需要擴充套件一個新類,然後繼承抽象介面實現工廠方法即可。遵循了開閉原則。
缺點
具體產品和工廠方法耦合,因為在工廠方法中需要建立具體例項,所以它們會耦合
7、何時使用工廠模式
通過工廠模式定義我們知道,工廠模式主要是把物件的建立延遲到子類執行。如何實現的呢?
拿上面的例子來說,當我們呼叫抽象類factory的方法createCpuWithType
的時候,真正執行的是factory的子類,比如intelFactory。做到這點是面嚮物件語言的基本特徵之一:多型,它可以實現父類的同一個方法在不同的子類中有不同的表現。
瞭解了工廠模式的本質,我們就知道在上面情況下可以使用它了
- 一個類不想知道它所需要建立的物件所屬的類,比如client不需要知道intelCpu1179這個具體類
- 一個類希望由他的子類來指定它所建立的物件,比如factory希望IntelFactory建立具體cpu物件
抽象工廠
1、業務場景
假設我們寫了一套系統,底層使用了兩套資料庫:sqlserver和access資料庫。但是針對業務邏輯的程式碼不可能寫兩套,這樣非常麻煩,也不方便擴充套件新的資料庫。我們需要提供一個統一的介面給業務層操作,切換資料庫也不需要修改業務層邏輯。
簡化下需求,假設我們每個資料庫都有user和department兩張表,業務邏輯程式碼如下:
//業務邏輯
[user insert:@"張三"];
[user getUser];
[deparment insert:@"財務"];
[deparment getDepartment];複製程式碼
下面我們就來看看如何使用抽象工廠來實現這個需求
2、需求實現
2.1、建立抽象工廠介面
我們先建立一個抽象介面,在iOS裡面我們使用協議實現。
IFactory.h檔案
========================
@class IUser;
@class IDepartment;
@protocol IFactory <NSObject>
@required
-(IUser*)createUser;
-(IDepartment *)createDepartment;
@end複製程式碼
2.2、建立具體工廠
下面我們來建立兩個具體的工廠,分別針對兩個資料庫,實現抽象工廠的方法,來建立具體的表物件
#import <Foundation/Foundation.h>
#import "IFactory.h"
#import "IUser.h"
@interface SqlServerFactory : NSObject<IFactory>
@end
======================
#import "SqlServerFactory.h"
#import "SqlServerUser.h"
#import "SqlServerDepartment.h"
@implementation SqlServerFactory
-(IUser *)createUser{
return [SqlServerUser new];
}
-(IDepartment *)createDepartment{
return [SqlServerDepartment new];
}
@end複製程式碼
AccessFactory類建立方法類似。
2.3、建立產品
現在我們需要建立具體工廠需要的產品,這裡是兩張表:user和department。但是這兩張表有分為兩個體系,sqlserver的user和department表,access的user和department表。
我們把user表抽象為基類,下面分別實現sqlserver和access的子類user表。department表同理,不再貼程式碼了。
抽象產品類
#import <Foundation/Foundation.h>
@interface IUser : NSObject
-(void)insert:(NSString *)user;
-(void)getUser;
@end
=======================
#import "IUser.h"
@implementation IUser
-(void)insert:(NSString *)user{
@throw ([NSException exceptionWithName:@"繼承錯誤" reason:@"子類沒有實現父類方法" userInfo:nil]);
}
-(void)getUser{
@throw ([NSException exceptionWithName:@"繼承錯誤" reason:@"子類沒有實現父類方法" userInfo:nil]);
}
@end複製程式碼
具體產品類
#import <Foundation/Foundation.h>
#import "IUser.h"
@interface SqlServerUser : IUser
@end
==================
#import "SqlServerUser.h"
@implementation SqlServerUser
-(void)insert:(NSString *)user{
NSLog(@"向sqlserver資料庫插入使用者:%@", user);
}
-(void)getUser{
NSLog(@"從sqlserver資料庫獲取到一條使用者資料");
}
@end複製程式碼
#import <Foundation/Foundation.h>
#import "IUser.h"
@interface AccessUser : IUser
@end
=========================
#import "AccessUser.h"
@implementation AccessUser
-(void)insert:(NSString *)user{
NSLog(@"向access資料庫插入使用者:%@", user);
}
-(void)getUser{
NSLog(@"從access資料庫獲取到一條使用者資料");
}
@end複製程式碼
2.4、客戶端呼叫
#import <Foundation/Foundation.h>
#import "IFactory.h"
#import "IUser.h"
#import "IDepartment.h"
#import "SqlServerFactory.h"
#import "AccessFactory.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
id<IFactory> DBFactory = [AccessFactory new];
IUser *user = [DBFactory createUser];
IDepartment *deparment = [DBFactory createDepartment];
//業務邏輯
[user insert:@"張三"];
[user getUser];
[deparment insert:@"財務"];
[deparment getDepartment];
}
return 0;
}複製程式碼
輸出:
2016-11-22 17:38:30.667 抽象工廠模式[56330:792839] 向access資料庫插入使用者:張三
2016-11-22 17:38:30.668 抽象工廠模式[56330:792839] 從access資料庫獲取到一條使用者資料
2016-11-22 17:38:30.668 抽象工廠模式[56330:792839] 向access資料庫插入部門:財務
2016-11-22 17:38:30.668 抽象工廠模式[56330:792839] 從access資料庫獲取到一條部門資料複製程式碼
此時如果需要切換到sqlserver資料庫,只需要更改如下程式碼
id<IFactory> DBFactory = [AccessFactory new];
改為:
id<IFactory> DBFactory = [SqlServerFactory new];複製程式碼
但是抽象工廠有個缺點:你想下,如果此時我想增加一張工資表,那麼就必須修改抽象工廠介面類IFactory和每個具體工廠類SqlServerFactory、AccessFactory,違反了開閉原則。但是總體來瑕不掩瑜。
3、 實現原理分析
通過上面的例子,我想大家已經認識到抽象工廠的優雅之處,那麼它是如何完成的呢?
我們來把上面的例子做成UML圖,這樣看的更加清晰。
可以看到我們建立了兩個具體工廠,分別是sqlserverFactory和AccessFactory。我們的產品有兩個user和department,每個產品也分為兩個體系:sqlserver的access的。
如果選擇sqlserverFactory,那麼對應的兩個工廠方法就生成sqlserver的user和department表。選擇accessFactory也是如此。
所以我們可以很方便在兩個資料庫之間切換,而不影響業務邏輯,因為業務邏輯都是面向抽象程式設計。再看下業務邏輯的程式碼
id<IFactory> DBFactory = [AccessFactory new];
IUser *user = [DBFactory createUser];
IDepartment *deparment = [DBFactory createDepartment];
//業務邏輯
[user insert:@"張三"];
[user getUser];
[deparment insert:@"財務"];
[deparment getDepartment];複製程式碼
可以看到業務邏輯都是針對抽象類IUesr和IDepartment程式設計,所以他們的子類如何變化,不會影響到業務邏輯。
###4、 抽象工廠定義
提供一個建立一系列相關或者相互依賴的介面,而無需依賴具體類。
好好分析這句話,關鍵的地方就是:一系列相關或者相互依賴的介面。這決定了我們使用抽象工廠的初衷,抽象工廠定義了一系列介面,這些介面必須是相互依賴或者相關的,而不是把一堆沒有什麼關聯的介面放到一起。
回頭看看我們上面的抽象工廠類IFactory定義的介面,是用來建立兩張表,這兩張表是屬於同一個資料庫的,他們之間是相互關聯和依賴的。
後面一句“無需依賴具體類”是怎麼做到的呢?
可以看到抽象工廠類只是定義了介面,而真正去實現這些介面產生具體物件的是具體工廠。客戶端面向的也是抽象工廠類程式設計,所以無需依賴具體類。
我們可以把抽象工廠的定義的方法看做工廠方法,然後具體工廠去實現這些工廠方法,這不就是工廠模式嗎?
所以說抽象工廠包含了具體工廠。
5、思考
工廠模式和抽象工廠模式最大的區別在於,後者的一系列工廠方法是相互依賴或者相關的,而工廠模式雖然也可以定義一些列工廠方法,但是他們之間是沒有關聯的。這是區分他們的重要依據。
其實如果抽象工廠裡面只定義一個工廠方法,也就是隻實現一個產品,那麼久退換為工廠方法了。
記住:
工廠模式建立一種型別的產品,抽象工廠建立一些列相關的產品家族。
6、何時使用抽象工廠
- 客戶端只希望知道抽象介面,而不關心具體產品的實現的時候
- 一個系統需要有多個產品系列中的一個來配置的時候。也就是說可以動態切換產品系列,比如上面的切換兩個資料庫
- 需要強調一系列產品的介面有關聯的時候,以便聯合使用它們。
三個模式對比
抽象工廠模式和工廠模式
工廠模式針對單獨產品的建立,而抽象工廠注重一個產品系列的建立。如果產品系列只有一個產品的 話,那麼抽象工廠就退換到工廠模式了。在抽象工廠中使用工廠方法來提供具體實現,這個時候他們聯 合使用。
工廠模式和簡單工廠
兩者非常類似,都是用來做選擇實現的。不同的地方在於簡單工廠在自身就做了選擇實現。而工廠模式 則是把實現延遲到子類執行。如果把工廠方法的選擇實現直接在父類實現,那麼此時就退化為簡單工廠 模式了。
簡單工廠和抽象工廠
簡單工廠用於做選擇實現,每個產品的實現之間沒有依賴關係。而抽象工廠實現的一個產品系列,相互 之間有關聯。這是他們的區別