三大工廠模式深入解析


原文地址  http://blog.ximu.site/factory-pattern/

最常見的工廠模式,工廠模式細分下來有三大類:

1. 簡單工廠 2. 工廠模式 3. 抽象工廠模式

他們的目標都是一樣的:封裝對象的創建。但是實現手段和使用場景卻是不相同。使用的時候三個模式也可以互相替換使用,導致很容易混淆三者。

下面我們來具體看看三者的使用。


簡單工廠模式

准確的說簡單工廠不是一個模式,而是一種編程習慣。但是平時使用的非常多,我們就把他歸到模式一類了。

1、定義

提供一個創建對象實例的功能,而無需關心具體實現。被創建的類型可以使接口、抽象類、具體類。

2、UML結構圖及說明

image

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. 解耦

      客戶端不需要知道具體實現類,也不需要知道創建過程。只需要知道簡單工廠類就可以創建具體對象,實現了解耦

  • 缺點

    1.增加客戶端復雜度

    如果是通過參數來選擇創建具體的對象,那么客戶端就必須知道每個參數的含義,也就暴露了內部實現

    2.不方便擴展

    如果實現類改變,那么還是需要修改簡單工廠,可以通過文中的方法來避免這個問題。或者使用下節我們講的工廠方法來解決

7、簡單工廠本質

簡單工廠的本質:選擇實現

簡單的工廠的本質在於選擇,而不是實現,實現是由具體類完成的,不要在簡單工廠完成。簡單工廠的目的是讓客戶端通過自己這個中介者來選擇具體的實現,從而讓客戶端和具體實現解耦,任何實現方面的變化都被簡單工廠屏蔽,客戶端不會知道。

簡單工廠的實現難點在於如何“選擇實現”,前面講到的是靜態傳遞參數。其實還可以在運行過程中從內存或者數據庫動態選擇參數來實現,具體代碼就不演示了,只是讀取參數的方式不同,其他都一樣。

8、何時使用簡單工廠

  1. 想完全封裝隔離具體實現

讓外部只能通過抽象類或者接口來操作,上面的例子中,就是只能操作抽象類cpu,而不能操作具體類。此時可以使用簡單工廠,讓客戶端通過簡單工廠來選擇創建具體的類,不需要創建的具體過程。

  1. 想把創建對象的職責集中管理起來

一個簡單工廠可以創建許多相關或者不相關的對象,所以可以把對象的創建集中到簡單工廠來集中管理。

完整代碼見文末。


工廠模式

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結構圖及說明

image

先記住工廠模式實現了依賴倒置原則,至於如何實現的,暫且按下不表,我們先來看代碼

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、如何遵循依賴倒置原則

我們先來看看沒有使用工廠方法,各個類之間的依賴關系

image

可以看到高層組件client依賴於具體的低層組件cpu類,違反了依賴倒置原則。一般我們把功能的使用者歸到高層組件,把功能的提供者歸到低層組件。

再來看看使用工廠方法后各個類之間的依賴關系

image

可以看到高層組件client依賴於抽象類cpu,低層組件也就是各種cpu具體類也依賴於抽象類factory,符合依賴倒置原則。其實說白了,就是要針對接口編程,而不是針對實現編程

那么倒置在哪里呢?

對比兩個圖,就會發現具體cpu類的箭頭從原來向下變成了向上,也就是說依賴關系發生了倒置。我們來看看為什么會這樣。

第一個圖里面,因為我們直接在client里面去初始化各個cpu類,倒置client就必須依賴這些具體類,依賴關系向下。

第二個圖里面,每個cpu具體類,都繼承自抽象cpu類,並且實現了抽象cpu的方法installCpu,此時具體cpu類就依賴於抽象cpu類,依賴關系向上。

現在明白為什么叫做依賴倒置了吧?這一切都是工廠方法的功勞。

有人要說,這個用簡單工廠也可以實現的呀。是的沒錯,簡單工廠也能實現,其實如果直接在工廠方法的抽象cpu類里面實現對象的創建,那么此時工廠模式就是簡單工廠。但是工廠模式有一個簡單工廠模式沒有的功能:遵循開閉原則。如果此時要增加或者修改一個cpu具體類,那么簡單工廠的代碼就必須修改,而工廠方法只需要擴展就行了,不用修改原有代碼。

6、工廠模式優缺點

  • 優點

    1. 可以在不知道具體實現的情況下編程

      工廠模式可以讓你在實現功能時候,不需要關心具體對象,只需要使用對象的抽象接口即可,上面例子中client使用的就是cpu抽 象類,而不是具體的cpu類。

    2. 更容易擴展新版本

      如果需要加入新的實現,只需要擴展一個新類,然后繼承抽象接口實現工廠方法即可。遵循了開閉原則。

  • 缺點

    具體產品和工廠方法耦合,因為在工廠方法中需要創建具體實例,所以它們會耦合

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圖,這樣看的更加清晰。

image

可以看到我們創建了兩個具體工廠,分別是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、何時使用抽象工廠

  • 客戶端只希望知道抽象接口,而不關心具體產品的實現的時候
  • 一個系統需要有多個產品系列中的一個來配置的時候。也就是說可以動態切換產品系列,比如上面的切換兩個數據庫
  • 需要強調一系列產品的接口有關聯的時候,以便聯合使用它們。

三個模式對比

  • 抽象工廠模式和工廠模式

    工廠模式針對單獨產品的創建,而抽象工廠注重一個產品系列的創建。如果產品系列只有一個產品的 話,那么抽象工廠就退換到工廠模式了。在抽象工廠中使用工廠方法來提供具體實現,這個時候他們聯 合使用。

  • 工廠模式和簡單工廠

    兩者非常類似,都是用來做選擇實現的。不同的地方在於簡單工廠在自身就做了選擇實現。而工廠模式 則是把實現延遲到子類執行。如果把工廠方法的選擇實現直接在父類實現,那么此時就退化為簡單工廠 模式了。

  • 簡單工廠和抽象工廠 簡單工廠用於做選擇實現,每個產品的實現之間沒有依賴關系。而抽象工廠實現的一個產品系列,相互 之間有關聯。這是他們的區別


Demo下載地址

簡單工廠

工廠模式

抽象工廠模式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM