設(shè)計(jì)模式精讀 ~ 單元測(cè)試的利器 ~ 抽象工廠

所屬文章系列:尋找塵封的銀彈:設(shè)計(jì)模式精讀



【動(dòng)機(jī)】

我所見過的代碼中,使用設(shè)計(jì)模式的并不多。如果這些代碼能夠做到從容面對(duì)變化,那它依然是好代碼。

但在實(shí)踐中,當(dāng)我們面對(duì)需求變化的時(shí)候,會(huì)發(fā)現(xiàn)每次應(yīng)對(duì)變化都需要很大的代碼改動(dòng)量,而且很容易出錯(cuò)。再加上缺少單元測(cè)試的保護(hù),只能靠人工測(cè)試來驗(yàn)證代碼是否有效,有些隱蔽的bug就有可能從程序員、測(cè)試員手中溜過,而直接出現(xiàn)在用戶那里。

我們都知道改bug的成本遠(yuǎn)比預(yù)防bug的成本要高,同時(shí)大部分程序員并不喜歡改bug,尤其是改那種“按下葫蘆起了瓢”的bug,所以程序員急需找到一個(gè)方法來解決這些令人頭疼的問題。

抽象工廠模式就是解決需求變化問題的一種方案。

我們先看一段未使用抽象工廠模式的代碼,找找痛點(diǎn)在哪里:

void Client1::DoSomething() {

????file = FileAPI::CreateFile();

????...

}

void Client2::DoSomething() {

????folder = FileAPI::CreateFolder();

????...

}

void Client3::DoSomething() {

????configFile = FileAPI::CreateConfigFile();

????...

}

注:Client1、Client2等是指系統(tǒng)中的某個(gè)類,它們使用FileAPI,就稱它們?yōu)镕ileAPI的Client或叫客戶代碼。

從這段代碼能看出,我們已經(jīng)把文件系統(tǒng)的API作了封裝,這很好。不過,當(dāng)需求變化不斷地到來時(shí),這些看起來還不錯(cuò)的代碼就遇到了麻煩:

1.第一次需求變化

我們現(xiàn)在遇到了一個(gè)新需求:為了提供安全機(jī)制,需要把系統(tǒng)中使用的所有文件都進(jìn)行加密。

最直接的方法是:修改FileAPI類的每一個(gè)函數(shù)的實(shí)現(xiàn)代碼,例如CreateFile、CreateFolder、CreateConfigFile,把每個(gè)函數(shù)中原有的不加密代碼都刪掉,新寫一些加密的代碼。如果有幾十個(gè)這樣的函數(shù),那工作量就有點(diǎn)大了。

改過代碼之后,又發(fā)現(xiàn)類名需要修改:FileAPI這個(gè)類的意義已經(jīng)發(fā)生了變化,如果不改名,那么在其他程序員修改Clien1、Client2等處代碼時(shí),并不知道這些變化,還只是以為FileAPI只是對(duì)OS API進(jìn)行了一個(gè)包裝而已,那么就有可能寫出錯(cuò)誤的代碼。所以應(yīng)該把FileAPI類改為EncodedFileSystem,而且所有客戶代碼都跟著改一遍。

2.第二次需求變化

改完之后,測(cè)試通過,交給用戶。過了一段時(shí)間,又有一個(gè)新需求要做:只有在用戶設(shè)置為“需要加密”時(shí)才對(duì)文件加密,否則就不加密。

最直接的方法是:把剛才刪掉的那些不加密的代碼找回來,并在每個(gè)函數(shù)中加入if判斷。就像下邊的代碼:

void EncodedFileSystem::CreateFile() {

????if (userConfig == ENCODED) {

????????...

????} else {

????????...

????}

}

此時(shí),EncodedFileSystem這個(gè)類的意義已經(jīng)發(fā)生了變化,所以類名應(yīng)該再次修改,改為FileSystemWithPolicy。

面對(duì)第二次需求變化,大部分的代碼修改都是重復(fù)性工作,誰喜歡這種編寫代碼的方式呢?所以有人就在想:有沒有一種方法,當(dāng)我們面對(duì)后續(xù)的需求變化時(shí),讓代碼改動(dòng)量保持最小、最安全?


【模式典型代碼】

答案當(dāng)然是有方法:使用抽象工廠模式。

為了實(shí)現(xiàn)抽象工廠,我們需要找到系統(tǒng)初始化部分的代碼,例如類MyApplication,在這里寫下工廠切換代碼:

class MyApplication {

public:

????void Initialize() {

????????if (userConfig == ENCODED)

????????????fileSystemFactory = EncodedFileSystemFactory::GetInstance();

????????else

????????????fileSystemFactory = FileSystemFactory::GetInstance();

????}

????FileSystemFactory *GetFileSystemFactory() { return fileSystemFactory; }

private:

????FileSystemFactory *fileSystemFactory;

}

class FileSystemFactory {

public:

????virtual File *CreateFile();

????virtual Folder *CreateFolder();

????virtual File *CreateConfigFile();

????virtual File *CreateDataFile();

????...

}

class EncodedFileSystemFactory : public FileSystemFactory {

public:

????virtual File *CreateFile();

????virtual Folder *CreateFolder();

????virtual File *CreateConfigFile();

????virtual File *CreateDataFile();

????...

}

如此一來,再有切換文件系統(tǒng)策略的需求,例如一部分文件加密一部分不加密、文件壓縮等,那么只需要增加新的實(shí)現(xiàn)類,老代碼中只需要修改MyApplication::Initialize即可。

當(dāng)然,客戶代碼也需要修改一下,例如:

void Client1::DoSomething() {

????file = myApplication->GetFileSystemFactory()->CreateFile();

????...

}


【優(yōu)劣對(duì)比】

有人會(huì)提出疑問:這次使用抽象工廠的代碼修改量超過了未使用抽象工廠的代碼量。

情況確實(shí)如此:

使用抽象工廠的代碼量=系統(tǒng)初始化代碼的修改 + 新需求引入的新工廠實(shí)現(xiàn)類 + 客戶代碼的修改

未使用抽象工廠的代碼量=系統(tǒng)初始化代碼的修改 + 新需求引入的已有類的代碼修改。

與未使用抽象工廠的代碼相比,使用抽象工廠的代碼量確實(shí)多出了客戶代碼的修改部分,代碼量雖然有點(diǎn)大,但并不難改,具體來說,把原來的FileAPI類或FileSystemWithPolicy類一刪,就會(huì)導(dǎo)致編譯錯(cuò)誤,根據(jù)編譯錯(cuò)誤一一修改即可,簡(jiǎn)單快捷而且不會(huì)出錯(cuò)。

多做了這么一點(diǎn)工作,獲得的回報(bào)卻是很大的:

1.風(fēng)險(xiǎn)小:以后再有需求變化,只需改動(dòng)系統(tǒng)初始化一處,最多是把新增的類加入進(jìn)來。反觀未使用抽象工廠的代碼,它的修改量雖小,但它是在修改已有代碼。而修改已有代碼的風(fēng)險(xiǎn)遠(yuǎn)比新增代碼要高、測(cè)試量也大,這是因?yàn)槌绦騿T需要花大量的時(shí)間去理解被修改代碼的影響面,而這個(gè)影響面一般都比較大。

2.封裝性好:通過GetFileSystemFactory()能看出,客戶代碼只需要知道有一個(gè)工廠來幫我CreateFile,而不需要知道用什么方式實(shí)現(xiàn)的,而原來的FileAPI的意思是它直接使用OS API,客戶代碼需要關(guān)心我處于的OS是什么以決定它的調(diào)用方式,或者什么時(shí)候該切換文件加密策略。

3.單一職責(zé):每個(gè)工廠的實(shí)現(xiàn)代碼非常清晰,互不影響,它只需要關(guān)心自己的實(shí)現(xiàn)即可。

4.方便單元測(cè)試:參見后文的詳細(xì)討論。


【模式定義】

抽象工廠模式(Abstract Factory):提供一個(gè)創(chuàng)建一系列相關(guān)或相互依賴對(duì)象的接口,而無需指定它們具體的類。

只有當(dāng)我們希望通過工廠來構(gòu)造對(duì)象時(shí),才是抽象工廠模式,如果只是執(zhí)行一個(gè)函數(shù)而不是構(gòu)造對(duì)象,就可能是其他設(shè)計(jì)模式,例如策略模式。而策略模式也是解決需求變化問題的一種方案。


模式類圖

注:該類圖是在《設(shè)計(jì)模式》原書類圖的基礎(chǔ)上,增加了MyApplication,這樣就能更清楚地表達(dá)出整個(gè)系統(tǒng)的運(yùn)作關(guān)系。另外,AbstractFactory::CreateProductA和AbstractFactory::CreateProductB都應(yīng)該像AbstractFactory那樣以斜體字顯示,但我在Visio工具中沒有找到那個(gè)選項(xiàng),請(qǐng)讀者見諒!

在類圖中,我們看到:

1.客戶代碼(Client)只關(guān)心兩樣?xùn)|西:工廠、產(chǎn)品。而且這兩樣?xùn)|西都是抽象的(Abstract),至于如何實(shí)現(xiàn)一個(gè)工廠、一個(gè)產(chǎn)品,客戶不需要關(guān)心。

2.而關(guān)心使用哪個(gè)工廠實(shí)現(xiàn)(ConcreteFactory1)的,一般就是系統(tǒng)初始化部分(例如MyApplication::Initialize),也可能是某個(gè)設(shè)置界面的代碼。

3.關(guān)心使用哪個(gè)產(chǎn)品實(shí)現(xiàn)(ProductA1)的,是某個(gè)工廠實(shí)現(xiàn)(ConcreteFactory1)。通過這種方式實(shí)現(xiàn)了一個(gè)工廠定制的是一個(gè)產(chǎn)品系列(即多個(gè)產(chǎn)品),換到另外一個(gè)工廠就是另外一個(gè)產(chǎn)品系列。

這樣就實(shí)現(xiàn)了:從系統(tǒng)的某一個(gè)視角(例如Client)來看周圍環(huán)境,它只關(guān)心最少的東西,也就是說,它知道的越少,受到各種變化的影響就越小。


【思維進(jìn)階(一):兩個(gè)維度的變化】

每個(gè)設(shè)計(jì)模式背后都有一些原理在支撐。抽象工廠模式的背后是兩個(gè)維度的變化:加密或非加密存儲(chǔ)、切換文件訪問策略。

注:此處的維度可以大致理解為方向。

Marin Fowler在《重構(gòu)》中提到:“如果某個(gè)class經(jīng)常因?yàn)椴煌脑蛟诓煌姆较蛏习l(fā)生變化,Divergent Change就出現(xiàn)了?!盌ivergent Change是指“發(fā)散式變化”,是該書中22種“代碼壞味道”中的一種。

前文未使用抽象工廠的FileAPI類代碼,受到兩個(gè)方向的需求變化,即加密或非加密存儲(chǔ)、切換文件訪問策略的影響,當(dāng)任意一個(gè)需求發(fā)生變化時(shí),這個(gè)類都要進(jìn)行修改。它符合“發(fā)散式變化”壞味道的定義。

發(fā)現(xiàn)了壞味道,就應(yīng)該去修改,不要讓壞味道演變成發(fā)酵甚至腐爛。而抽象工廠就是去除“發(fā)散式變化”這種壞味道的一種方式。加入工廠代碼之后,工廠實(shí)現(xiàn)類如EncodedFileSystemFactory只負(fù)責(zé)加密算法,系統(tǒng)初始化部分如MyApplication::Initialize只負(fù)責(zé)切換文件訪問策略。

在兩個(gè)維度變化的背后就是單一職責(zé)原則,本文不展開對(duì)單一職責(zé)的討論。


【思維進(jìn)階(二):靈活運(yùn)用】

前文的代碼,有兩點(diǎn)與標(biāo)準(zhǔn)的抽象工廠模式有所區(qū)別:

1.把工廠類FileSystemFactory實(shí)現(xiàn)為單件。

2.抽象工廠基類FileSystemFactory并不只是一個(gè)接口,也包括一個(gè)默認(rèn)實(shí)現(xiàn)。

這兩條都體現(xiàn)了設(shè)計(jì)模式的靈活運(yùn)用方式:并不是完全套用設(shè)計(jì)模式的標(biāo)準(zhǔn)形式。就像《設(shè)計(jì)模式》書中62頁提到的:

注意MazeFactory僅是工廠方法的一個(gè)集合。這是最通常的實(shí)現(xiàn)Abstract Factory模式的方式。同時(shí)注意MazeFactory不是一個(gè)抽象類;因此它既作為AbstractFactory也作為ConcreteFactory。

解釋一下:

按照前文類圖的定義,AbstractFactory是指抽象基類,它并沒有實(shí)現(xiàn)代碼,ConcreteFactory是指抽象工廠的實(shí)現(xiàn)類。


【如何用于單元測(cè)試】

工廠模式對(duì)于單元測(cè)試來說,非常實(shí)用。

單元測(cè)試,既然叫“單元”,一般只測(cè)試一個(gè)類,一般是白盒測(cè)試。而在實(shí)踐中,它可以測(cè)試多個(gè)類,有的測(cè)試框架做得比較好,可以讓整個(gè)應(yīng)用系統(tǒng)運(yùn)行起來,就像是用戶打開應(yīng)用程序在使用時(shí)一樣。

這時(shí),單元測(cè)試就變成了集成測(cè)試,那我們可測(cè)試的范圍就大大增加,從而可以模仿用戶的行為來測(cè)試系統(tǒng)的整體行為,也就是可以使用黑盒測(cè)試的手段,此時(shí),白盒測(cè)試與黑盒測(cè)試結(jié)合起來,效果非常好。

為了保證這種系統(tǒng)級(jí)別的單元測(cè)試代碼可以運(yùn)行起來,不單單需要測(cè)試框架的支持,還需要讓被測(cè)試代碼能夠使用一些測(cè)試數(shù)據(jù),而這些測(cè)試數(shù)據(jù)的來源就可以使用偷梁換柱的方法:把真實(shí)對(duì)象偷偷換成假對(duì)象,而這個(gè)假對(duì)象會(huì)提供測(cè)試數(shù)據(jù),這就是業(yè)內(nèi)流行的Fake或Mock的方式。例如:

class MyApplicationTest {

public:

????void Initialize() {

????????fileSystemFactory = FileSystemFactoryMock::GetInstance();

????}


????FileSystemFactory *GetFileSystemFactory() { return fileSystemFactory; }

private:

????FileSystemFactory *fileSystemFactory;

}

class FileSystemFactoryMock : public FileSystemFactory {

public:

virtual File *CreateFile() { //返回一些假數(shù)據(jù),例如File::name = “test1”?};

????virtual Folder *CreateFolder();

????virtual CreateConfigFile();

????virtual CreateDataFile();

????...

}

void TestCreateFile() {

????MyApplicationTest::Initialize();

????Client1::DoSomething();

????ASSERT(Client1::GetFile()->GetName() == “test1”); //注意:我并不使用完全真實(shí)的代碼,因?yàn)檫@樣表達(dá)意圖更為明確

}


作于2018-5-11

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評(píng)論 19 139
  • 設(shè)計(jì)模式匯總 一、基礎(chǔ)知識(shí) 1. 設(shè)計(jì)模式概述 定義:設(shè)計(jì)模式(Design Pattern)是一套被反復(fù)使用、多...
    MinoyJet閱讀 4,073評(píng)論 1 15
  • 今天做各種肉的亞硝酸鹽的測(cè)定。 老師早上拿來一份資料讓我看。 我很高興,因?yàn)榭吹倪^程中我有扣出不懂的概念弄清楚,然...
    親愛的吳小仙閱讀 101評(píng)論 2 0
  • 做了法律這行當(dāng),總得時(shí)時(shí)注意細(xì)節(jié),假若忽視了細(xì)節(jié),可能會(huì)生出嚴(yán)重的錯(cuò)誤或者難以解決的麻煩,到那時(shí)便后悔莫及。 有一...
    夜語山林閱讀 595評(píng)論 0 0
  • 雨落楓葉一片閱讀 306評(píng)論 0 0

友情鏈接更多精彩內(nèi)容