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