據(jù)庫(kù)切換場(chǎng)景下的雙模式嵌套解析)
簡(jiǎn)介軟件構(gòu)造課程中抽象工廠模式與單件模式的編程實(shí)現(xiàn)實(shí)驗(yàn)由徐奕學(xué)號(hào)E21614061完成面向軟件工程專業(yè)學(xué)生及設(shè)計(jì)模式初學(xué)者。實(shí)驗(yàn)報(bào)告以公司數(shù)據(jù)庫(kù)模擬場(chǎng)景切入圍繞用戶、部門(mén)、項(xiàng)目三類數(shù)據(jù)操作設(shè)計(jì)抽象接口與MySQL、SQL Server兩套具體實(shí)現(xiàn)并通過(guò)單件模式限制工廠實(shí)例數(shù)量完整呈現(xiàn)從需求分析、UML建模到代碼實(shí)現(xiàn)的全部過(guò)程。資源為1個(gè)PDF文檔壓縮包僅301KB已有122人學(xué)習(xí)。報(bào)告詳述了抽象工廠接口IFactory、具體工廠類SqlserverFactory與MysqlFactory以及SqlserverUser、MysqlProject等數(shù)據(jù)庫(kù)操作實(shí)現(xiàn)類配有實(shí)驗(yàn)時(shí)間、實(shí)驗(yàn)?zāi)康?、原理說(shuō)明、實(shí)驗(yàn)代碼和實(shí)驗(yàn)結(jié)論同時(shí)討論了單件模式通過(guò)靜態(tài)方法或私有構(gòu)造函數(shù)確保全局唯一實(shí)例的常見(jiàn)實(shí)現(xiàn)方式。借助本報(bào)告讀者不僅能獲得一份結(jié)構(gòu)完整的實(shí)驗(yàn)報(bào)告范本還能深入理解如何在運(yùn)行時(shí)動(dòng)態(tài)切換數(shù)據(jù)庫(kù)類型、保持?jǐn)?shù)據(jù)庫(kù)操作接口一致性從而掌握多產(chǎn)品族創(chuàng)建與全局唯一實(shí)例控制的核心設(shè)計(jì)思想。1. 抽象工廠與單件為什么一個(gè)“數(shù)據(jù)庫(kù)切換”需求最后會(huì)演變成雙模式嵌套一個(gè)看起來(lái)只是“公司數(shù)據(jù)庫(kù)支持 MySQL 和 SQL Server 兩種庫(kù)”的需求如果直接寫(xiě)代碼大多數(shù)人第一反應(yīng)是寫(xiě)一堆 if-else判斷當(dāng)前配置是什么數(shù)據(jù)庫(kù)然后分別執(zhí)行對(duì)應(yīng)的 SQL 語(yǔ)句。但這份實(shí)驗(yàn)報(bào)告給出的方案卻是先定義一個(gè)抽象工廠接口再為每種數(shù)據(jù)庫(kù)實(shí)現(xiàn)一套完整的產(chǎn)品族最后還要用單件模式把工廠實(shí)例鎖死成一個(gè)。明明是兩句 if 就能解決的問(wèn)題為什么要繞這么大一圈關(guān)鍵原因在于在真實(shí)系統(tǒng)里用戶表、部門(mén)表、項(xiàng)目表不是三個(gè)孤立的數(shù)據(jù)結(jié)構(gòu)而是一組“產(chǎn)品族”——它們共享同一個(gè)數(shù)據(jù)庫(kù)選型約束。你不可能在 MySQL 下用 MySQL 的用戶表操作、卻又用 SQL Server 的部門(mén)表操作。抽象工廠模式的存在意義就是把這組產(chǎn)品的創(chuàng)建邏輯打包成不可拆散的整體而單件模式則解決了另一個(gè)問(wèn)題既然一個(gè)數(shù)據(jù)庫(kù)對(duì)應(yīng)一套產(chǎn)品族那這套產(chǎn)品族的工廠在進(jìn)程里只需要一個(gè)實(shí)例。兩個(gè)模式疊加表面上是在寫(xiě)“創(chuàng)建對(duì)象的代碼”實(shí)際上是在回答兩個(gè)更底層的問(wèn)題——產(chǎn)品族的一致性怎么保證以及工廠本身的生命周期怎么管理。本文的核心脈絡(luò)先交代清楚第一章講清楚這兩個(gè)模式結(jié)合要解決什么問(wèn)題第二章拆解抽象工廠的產(chǎn)品族創(chuàng)建機(jī)制第三章講單件模式的底層約束也就是構(gòu)造函數(shù)私有化和靜態(tài)指針第四章是避坑排查重點(diǎn)分析這類實(shí)現(xiàn)里最容易翻車的線程安全和實(shí)例管理問(wèn)題第五章回到設(shè)計(jì)取舍討論這套方案在真實(shí)工程中的邊界。2. 抽象工廠模式產(chǎn)品族一致性是怎么被“結(jié)構(gòu)”強(qiáng)制保證的2.1 從 if-else 到抽象工廠變化點(diǎn)在哪里先看最樸素的做法。假設(shè)沒(méi)有設(shè)計(jì)模式代碼會(huì)長(zhǎng)這樣根據(jù)數(shù)據(jù)庫(kù)類型字符串判斷該創(chuàng)建哪個(gè)具體操作對(duì)象。這種寫(xiě)法的問(wèn)題不在于性能而在于“一致性”完全靠程序員自覺(jué)——如果未來(lái)加入一個(gè) Oracle 分支但忘了更新某一處判斷就會(huì)出現(xiàn)用戶表走 MySQL、項(xiàng)目表走 SQL Server 的混亂狀態(tài)。這個(gè) bug 在測(cè)試環(huán)境未必暴露因?yàn)闇y(cè)試環(huán)境通常只配置一種數(shù)據(jù)庫(kù)等部署到生產(chǎn)環(huán)境切換數(shù)據(jù)庫(kù)時(shí)才炸。抽象工廠的思路是反過(guò)來(lái)的不是由調(diào)用方?jīng)Q定創(chuàng)建誰(shuí)而是調(diào)用方只拿到一個(gè)工廠接口由這個(gè)工廠負(fù)責(zé)創(chuàng)建一組完整的、彼此配套的產(chǎn)品。就像下單買(mǎi)套餐而不是單點(diǎn)菜品——套餐的內(nèi)容是固定的你不會(huì)拿到可樂(lè)配米飯這種不協(xié)調(diào)的組合。在實(shí)驗(yàn)代碼里IFactory接口定義了三個(gè)純虛函數(shù)分別創(chuàng)建IUser、IDepartment、IProject這就是產(chǎn)品族的邊界。2.2 接口隔離為什么客戶端只依賴抽象類實(shí)驗(yàn)代碼里有一個(gè)極易被忽略的設(shè)計(jì)點(diǎn)客戶端代碼中User* user new User()創(chuàng)建的是具體數(shù)據(jù)對(duì)象但操作這些對(duì)象的iu、idept、ipro變量它們的類型分別是IUser*、IDepartment*、IProject*。這是整個(gè)模式最核心的約束——客戶端代碼里不能出現(xiàn)任何具體產(chǎn)品類名。// 客戶端只認(rèn)識(shí)抽象接口不認(rèn)識(shí) SQL Server 或 MySQL 的具體類 IFactoryfactory1 MysqlFactory::GetInstance(); IUseriu factory1-CreateUser(); iu-Insert(user); iu-GetUser(1);這段代碼里從factory1拿到手的是IUser*指針真正調(diào)用Insert和GetUser時(shí)實(shí)際執(zhí)行的是MysqlUser的方法。如果哪天要把數(shù)據(jù)庫(kù)從 MySQL 切換到 SQL Server客戶端唯一的改動(dòng)是把MysqlFactory::GetInstance()換成SqlserverFactory::GetInstance()——一行代碼的差異其余全部不變。這就是依賴倒置原則在起作用高層模塊不依賴低層模塊的具體實(shí)現(xiàn)而是雙方都依賴抽象。CreateUser()、CreateDepartment()、CreateProject()三個(gè)純虛函數(shù)的意義還在于它們把“創(chuàng)建”動(dòng)作和“使用”動(dòng)作分離開(kāi)了??蛻舳瞬恢绖?chuàng)建過(guò)程中發(fā)生了什么——是直接 new 還是從對(duì)象池取、是否需要初始化連接這些細(xì)節(jié)全部封裝在具體工廠內(nèi)部。2.3 每個(gè)產(chǎn)品族內(nèi)部的自洽性為什么 MySQL 工廠不能返回 SQL Server 的對(duì)象抽象工廠最容易被新手誤解的地方是以為它只是把多個(gè)工廠方法做了簡(jiǎn)單聚合。實(shí)際上抽象工廠的關(guān)鍵在于“每個(gè)具體工廠內(nèi)部的所有產(chǎn)品必須是屬于同一個(gè)產(chǎn)品族的”。MysqlFactory的CreateUser()返回MysqlUserCreateDepartment()返回MysqlDepartmentCreateProject()返回MysqlProject——這三個(gè)類都實(shí)現(xiàn)了 MySQL 的訪問(wèn)邏輯它們組合在一起才構(gòu)成一個(gè)完整的、可工作的 MySQL 數(shù)據(jù)庫(kù)操作集。如果把MysqlFactory::CreateDepartment()的實(shí)現(xiàn)改成return new SqlserverDepartment()編譯器不會(huì)報(bào)錯(cuò)因?yàn)镾qlserverDepartment也實(shí)現(xiàn)了IDepartment接口語(yǔ)法上是合法的。但程序運(yùn)行后會(huì)出現(xiàn)用戶表走 MySQL、部門(mén)表卻通過(guò) SQL Server 的語(yǔ)法去查——這種“產(chǎn)品族內(nèi)部不一致”的問(wèn)題在抽象工廠模式里被視為架構(gòu)級(jí)錯(cuò)誤。工廠方法模式解決的是“一個(gè)產(chǎn)品”的創(chuàng)建問(wèn)題抽象工廠解決的是“一組產(chǎn)品”的協(xié)調(diào)問(wèn)題兩者的邊界就在這里。3. 單件模式構(gòu)造函數(shù)私有化與靜態(tài)指針的底層機(jī)制3.1 為什么工廠需要單件創(chuàng)建型模式的“實(shí)例唯一性”需求現(xiàn)在考慮第二個(gè)問(wèn)題工廠為什么需要單件直接new MysqlFactory()不行嗎從實(shí)驗(yàn)代碼可以看到MysqlFactory的內(nèi)部狀態(tài)其實(shí)很簡(jiǎn)單——它只有CreateUser等幾個(gè)方法本身不保存數(shù)據(jù)庫(kù)連接或緩存數(shù)據(jù)理論上創(chuàng)建多個(gè)實(shí)例也不會(huì)出邏輯錯(cuò)誤。但這里有一個(gè)資源和管理層面的考慮工廠是產(chǎn)品族的入口如果系統(tǒng)里同時(shí)存在多個(gè)MysqlFactory實(shí)例這些實(shí)例本身沒(méi)有狀態(tài)但它們各自創(chuàng)建的產(chǎn)品對(duì)象可能分布在不同的緩存上下文里極端情況下會(huì)讓“到底走了哪個(gè)工廠”變得難以追蹤。用單件模式限制工廠的唯一性真正收益是兩點(diǎn)一是省掉了重復(fù)創(chuàng)建工廠對(duì)象的開(kāi)銷雖然這個(gè)開(kāi)銷本身很小但設(shè)計(jì)上的潔癖更強(qiáng)調(diào)的是“這個(gè)工廠必須只有一份因?yàn)樗侨植呗缘拇怼薄獢?shù)據(jù)庫(kù)選型是全局決策不應(yīng)該被多個(gè)工廠實(shí)例模糊掉二是為后續(xù)擴(kuò)展留下空間如果未來(lái)工廠需要持有數(shù)據(jù)庫(kù)連接池或配置對(duì)象單件模式保證了狀態(tài)只存在一份。3.2 static 成員與類外初始化的執(zhí)行順序?qū)嶒?yàn)代碼里有一段關(guān)鍵的全局初始化// SqlserverFactory.h 內(nèi)部 static SqlserverFactory* factory;// 類外定義 SqlserverFactory* SqlserverFactory::factory NULL;這個(gè) NULL不是隨意寫(xiě)的。C 的靜態(tài)成員變量必須在類外定義并初始化否則鏈接時(shí)會(huì)報(bào)“未定義引用”的錯(cuò)誤。這里的初始化為 NULL 意味著工廠實(shí)例初始不存在等到第一次調(diào)用GetInstance()時(shí)才真正創(chuàng)建——這就是懶漢式單件的起點(diǎn)。很多初學(xué)者在這里會(huì)犯一個(gè)錯(cuò)把靜態(tài)成員初始化寫(xiě)在類內(nèi)部。但實(shí)際上 C 標(biāo)準(zhǔn)規(guī)定靜態(tài)數(shù)據(jù)成員的聲明只是聲明不分配存儲(chǔ)空間必須到類外定義才真正分配內(nèi)存。這個(gè)細(xì)節(jié)決定了單件模式的初始化入口不在構(gòu)造函數(shù)而在GetInstance靜態(tài)方法里。3.3 構(gòu)造函數(shù) private 的“防御半徑”實(shí)驗(yàn)總結(jié)里明確寫(xiě)著“具體工廠的構(gòu)造函數(shù)需要放在 private 中這樣客戶端不能隨意創(chuàng)建多個(gè)工廠?!边@一句是整個(gè)單件模式的核心機(jī)制。class SqlserverFactory : public IFactory { private: SqlserverFactory() {} static SqlserverFactory* factory; };構(gòu)造函數(shù)是 private 之后外部代碼new SqlserverFactory()會(huì)被編譯器直接攔截——因?yàn)閚ew操作的第一步是調(diào)用構(gòu)造函數(shù)如果構(gòu)造函數(shù)不可訪問(wèn)new表達(dá)式本身就不合法。客戶端唯一能拿到實(shí)例的入口只剩GetInstance()靜態(tài)方法而在這個(gè)方法內(nèi)部做了“存在則返回已有實(shí)例不存在則創(chuàng)建”的判斷。這里需要特別說(shuō)明的是GetInstance()里的邏輯瑕疵。實(shí)驗(yàn)代碼中SqlserverFactory::GetInstance()寫(xiě)了下面這段static IFactory *GetInstance(){ if (factory NULL ){ return new SqlserverFactory(); cout抽象工廠實(shí)例生成endl; } else { coutError, 抽象工廠實(shí)例已有endl; return factory; } }這段代碼有一個(gè)明顯的 bugreturn new SqlserverFactory()執(zhí)行后函數(shù)直接返回后面的cout永遠(yuǎn)不會(huì)執(zhí)行。而且更關(guān)鍵的是new出來(lái)的對(duì)象沒(méi)有賦值給factory所以第一次調(diào)用后factory仍然是 NULL。第二次調(diào)用時(shí)factory NULL仍然成立于是又創(chuàng)建一個(gè)新實(shí)例同時(shí)又因?yàn)閞eturn提前退出cout照樣不打印。對(duì)比MysqlFactory::GetInstance()的正確實(shí)現(xiàn)static IFactory *GetInstance(){ if (factory NULL ){ cout抽象工廠實(shí)例生成endl; factory new MysqlFactory(); } else { coutError, 抽象工廠實(shí)例已有endl; } return factory; }區(qū)別一目了然必須先給factory賦值再返回。SqlserverFactory那份代碼的return寫(xiě)在了賦值之前導(dǎo)致單件完全失效——每一次調(diào)用都會(huì)產(chǎn)生新對(duì)象。這個(gè)細(xì)節(jié)恰恰說(shuō)明了單件模式的本質(zhì)不是寫(xiě)了私有構(gòu)造函數(shù)就算單件必須保證“創(chuàng)建后立即保存引用后續(xù)復(fù)用同一引用”整個(gè)閉環(huán)才成立。4. 避坑指南單件與抽象工廠結(jié)合時(shí)的常見(jiàn)問(wèn)題與排查4.1 現(xiàn)象GetInstance 每次都返回新對(duì)象如果你照著實(shí)驗(yàn)報(bào)告里SqlserverFactory的寫(xiě)法實(shí)現(xiàn)運(yùn)行后發(fā)現(xiàn)打印了多次“抽象工廠實(shí)例生成”說(shuō)明單件失效了。原因很明確return語(yǔ)句在賦值之前執(zhí)行factory成員從未被寫(xiě)入。解決方法是把賦值語(yǔ)句移到return之前并刪除提前返回的分支邏輯。4.2 現(xiàn)象構(gòu)造函數(shù)私有后類外無(wú)法創(chuàng)建對(duì)象有些同學(xué)把構(gòu)造函數(shù)設(shè)為 private 后發(fā)現(xiàn)連GetInstance()內(nèi)部創(chuàng)建對(duì)象也編譯不過(guò)。原因是 C 的訪問(wèn)控制是按類作用域來(lái)算的——GetInstance()是SqlserverFactory的靜態(tài)成員函數(shù)它擁有訪問(wèn)私有構(gòu)造函數(shù)的權(quán)限所以new SqlserverFactory()在該函數(shù)內(nèi)是合法的。如果編譯報(bào)錯(cuò)檢查一下GetInstance是不是寫(xiě)成了非成員函數(shù)或者放在了別的類里。4.3 現(xiàn)象多線程環(huán)境下出現(xiàn)多個(gè)工廠實(shí)例懶漢式單件在單線程下沒(méi)問(wèn)題但兩個(gè)線程同時(shí)第一次調(diào)用GetInstance()時(shí)可能同時(shí)判斷factory NULL然后各自new一次最終得到兩個(gè)實(shí)例。排查方法是在GetInstance入口加鎖或者在 C11 及以后直接用 magic static——函數(shù)內(nèi)的 static 局部變量初始化是線程安全的。4.4 現(xiàn)象析構(gòu)函數(shù)把單件實(shí)例刪了實(shí)驗(yàn)代碼里定義了~SqlserverFactory(){coutSQL Server 抽象工廠實(shí)例刪除endl;}如果有人對(duì)GetInstance返回的指針執(zhí)行delete之后再用同指針調(diào)用CreateUser()就會(huì)觸發(fā)未定義行為。解決方案是避免外部刪除工廠指針正規(guī)做法是私有化析構(gòu)函數(shù)或使用智能指針管理單件生命周期。5. 回歸設(shè)計(jì)取舍抽象工廠嵌套單件究竟解決了什么、犧牲了什么回到最開(kāi)始的問(wèn)題——這個(gè)實(shí)驗(yàn)的價(jià)值不在代碼能跑而在于它逼你回答“為什么不能更簡(jiǎn)單”。抽象工廠把產(chǎn)品族的創(chuàng)建策略封裝成接口使得數(shù)據(jù)庫(kù)切換對(duì)客戶端來(lái)說(shuō)是透明的操作單件模式把工廠的實(shí)例數(shù)量鎖死成一個(gè)保證了全局策略的一致性。兩者結(jié)合本質(zhì)上是把“創(chuàng)建”這個(gè)行為從業(yè)務(wù)代碼里徹底剝離讓業(yè)務(wù)層只面對(duì)抽象接口。但代價(jià)也是明確的。第一個(gè)代價(jià)是類數(shù)量爆炸——每個(gè)產(chǎn)品族需要抽象產(chǎn)品、具體產(chǎn)品、抽象工廠、具體工廠四個(gè)層次這套實(shí)驗(yàn)里還只有三個(gè)產(chǎn)品、兩種數(shù)據(jù)庫(kù)類已經(jīng)超過(guò)十個(gè)。如果產(chǎn)品數(shù)量翻倍類數(shù)量線性增長(zhǎng)維護(hù)成本顯著上升。第二個(gè)代價(jià)是擴(kuò)展新數(shù)據(jù)庫(kù)的代價(jià)不小每增加一種數(shù)據(jù)庫(kù)需要新增三個(gè)具體產(chǎn)品類和一個(gè)具體工廠類同時(shí)還要保證新工廠正確注冊(cè)到選擇邏輯里。第三個(gè)代價(jià)是工廠層次與單件模式的耦合——單件鎖死了工廠實(shí)例但如果未來(lái)要支持多租戶、每個(gè)租戶不同數(shù)據(jù)庫(kù)配置這個(gè)“唯一實(shí)例”反而變成障礙。我一般會(huì)這么理解這個(gè)實(shí)驗(yàn)抽象工廠加單件是設(shè)計(jì)模式組合教學(xué)里最經(jīng)典的一對(duì)搭檔一個(gè)管“創(chuàng)建什么”一個(gè)管“創(chuàng)建幾次”。但在真實(shí)工程里很少有系統(tǒng)直接裸用這種結(jié)構(gòu)——因?yàn)楝F(xiàn)代框架的依賴注入和運(yùn)行時(shí)配置已經(jīng)把這個(gè)需求抽象到了更高層次。比如你只需要在配置文件里寫(xiě)一行db.typemysql框架掃描實(shí)現(xiàn)類自動(dòng)裝配哪還需要手寫(xiě)兩個(gè)工廠類但從教學(xué)角度看手寫(xiě)一遍的意義在于讓你親自體會(huì)到“接口 多實(shí)現(xiàn) 實(shí)例管理”這條鏈路上每一個(gè)決策的代價(jià)。從那以后我每次設(shè)計(jì)多數(shù)據(jù)庫(kù)支持系統(tǒng)都會(huì)先問(wèn)一句是切換整個(gè)產(chǎn)品族還是只切換某個(gè)產(chǎn)品的實(shí)現(xiàn)如果是前者抽象工廠依然是最穩(wěn)的底部架構(gòu)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取