一起來學設計模式-----工廠模式的實踐
工廠設計模式是入門模式,也是使用較多的模式,這一篇就總結下我在測試項目時,看到使用的地方以及編寫測試樁時基於此模式的實際運用。
實例一:測試的c++項目——簡單工廠+宏函數反射的使用
由於公司對業務和代碼要求保密,在這是不能貼業務代碼更不能直接給業務UML類圖,所以在這我做了一個類似案例的舉例。常測試的一個系統,是一個c++編寫的後臺系統。即是一個服務器端,又是一個客戶端;系統在接收上層的業務請求後,根據實際請求組包發送請求給其他接口服務器。其他接口服務器裏,以服務器為單位,都是提供2個接口,所以在封裝報文的時候,就要根據具體接口服務器的接口內容進行封裝,每個接口都是不一樣的。
那問題來了,怎麽封裝報文呢?
在上層業務的請求裏,字段:InterfaceServer(接口服務器),InterfaceType(接口類型)。比如上層業務請求:{“InterfaceServer”:1;“InterfaceType”:A},表示要調用接口服務器1的A接口,中間的服務器解析請求後,判斷是接口服務器1的A接口,就構造該類數據報文;如果解析是接口服務器2的B接口,則構造該數據報文;無論是哪個接口服務器,都有2個相同的方法,就是調用A,和調用B接口,由此可以抽象一個Iserver接口類,裏面有2個虛函數。每個繼承接口基類的類Iserver_A,都必須實現這2個接口,編寫接口特定的報文構造方法,並調用接口服務器。那工廠是什麽樣的呢?怎麽獲取接口服務器的實例呢?系統用了c++的宏函數完成了接口服務器的自動註冊。在每個接口服務器Iserver_A的實現類中,調用這個註冊的宏函數,把接口服務器,接口服務器字符串化後的兩個參數都放到一個map中,然後在工廠方法裏根據接口服務器就能獲取實例。
這樣的實現,程序在編譯之後,就能完成所有子類的接口服務器的自動註冊,因為每個子類接口服務器都調用了宏函數,把接口服務器的servertype和字符串化後的實例一起保存在了map<int, IServerInterface*> m_bankIterfaces中,程序在運行時,每次根據前端的業務請求的InterfaceServer,就能直接獲取該類接口服務器的實例,再根據InterfaceType,調用具體業務內的接口,構造報文,再調用接口服務器即可。在這個情況下,如果需要增加一個接口服務器,只需要增加該類接口服務器的cpp文件,調用宏函數完成自動註冊,實現自己的業務,就能在工廠類被調用,無須修改工廠類。實使用工廠模式+宏反射完成這類設計。
實例二:測試樁python項目----簡單工廠+DB配置
今年做的第一件事就是測試樁的編寫,這個測試樁就是模擬實例一的接口服務器(實例一接口服務器的測試樁),因為接口服務器是別的公司提供的,往往搭建環境上就會耗時很久,所以就要編寫一個測試樁,在之前就熟悉了組包過程的設計,測試樁在解析包的時候肯定也能使用相同的設計模式,但是python是沒有宏映射機制的,那這怎麽做呢?
對於測試樁,接收的全都是報文,測試樁本身是不知道報文是屬於哪個接口服務器的哪個接口的,但是人知道,人可以根據報文內容來識別。於是最開始可以在db配置一張表,裏面有每個報文的關鍵字段(能保證獨一無二),測試樁收到請求後,都會在請求中尋找一遍所有的關鍵字,找到關鍵字後,返回對應的servertype(int型數據),然後在工廠模式內,能根據servertype得到對應接口服務器的實例,接著再把實際請求轉發到對應接口的實際業務層處理A接口和B接口即可。那怎麽得到這個實例呢?c++沒有宏。於是想了好幾個方法:
方法一:使用python的全局變量
定義一個python的全局變量,全局變量的定義最好是單獨放在一個文件內,切忌不要把全局變量的定義和可執行文件(main)放在一起,這樣一定會有問題,這個問題我也遇到了,感興趣的可以參考下 XXXXX。然後每個使用到這個全局變量的地方,在文件尾部定義好server類名和servertype的對應值即可。業務模塊包含以下:
其中,IServer是基於業務的一層抽象,IServer_A和IServer_B是具體的業務。代碼如下:
IServer.py
from abc import ABCMeta, abstractmethod
class IServer: @abstractmethod def DoWithA(self): pass @abstractmethod def DoWithB(self): pass
IServer_A.py
from IServer import * from ServerRegister import GLOBAL_class_dic serverType =‘1001‘ class IServer_A(IServer): def DoWithA(self): print ‘Server_A do with interface A‘ def DoWithB(self): print ‘Server_A do with interface B‘ GLOBAL_class_dic[serverType] = IServer_A
IServer_B.py
from IServer import *
from ServerRegister import GLOBAL_class_dic
serverType =‘1002‘
class IServer_B(IServer):
def DoWithA(self):
print ‘Server_B do with interface A‘
def DoWithB(self):
print ‘Server_B do with interface B‘
GLOBAL_class_dic[serverType] = IServer_B
子類IServer_A在使用全局變量時,只需要從全局變量文件內import全局變量,然後在代碼的最後寫入即可。在代碼最後的寫入,IServer_B不再是一個字符串,已經是一個實例化的類對象。再來看看全局變量模塊是怎麽定於和獲取子類的全局變量的值的
ServerRegister.py
import threading import os from misc import Misc global GLOBAL_class_dic GLOBAL_class_dic ={} print ‘GLOBAL_class_dic in define is:‘, GLOBAL_class_dic print ‘the id of GLOBAL_class_dic in define is:‘, id(GLOBAL_class_dic) def getBankALIAS(): path1 = os.path.join(os.getcwd()) for fpath in Misc.sys_list_files(path1): Misc.sys_import_module(fpath) GLOBAL_timer_getBankALIAS = threading.Timer(1, getBankALIAS())
全局變量文件,裏面就是全局變量的定義和全局變量值在不同子類業務文件內的值的獲取。然後簡單的使用threading模塊的Timer,對子業務文件進行import,子業務文件又對全局變量進行了增加,python的dictionary數據類型又是一種可變數據類型,所以全局變量的dict值能正確的填入。然後在main函數所在的可執行文件中,直接使用全局變量即可。
CreatFactory.py
#coding:UTF-8 print __name__ from ServerRegister import * def CreateServer(serverType): if GLOBAL_class_dic.has_key(serverType): return GLOBAL_class_dic[serverType] else: return ‘no‘ if __name__ == ‘__main__‘: # 接收到報文後,根據報文的內容,從db中獲取到serverType,假設獲取到的serverType=1001 print ‘main‘ print ‘the id of GLOBAL_class_dic in MAIN is:‘, id(GLOBAL_class_dic) serverType = ‘1001‘ server = CreateServer(serverType) server.DoWithA(server())
實際運行結果如下:
這種思路,主要還是基於以前對c++代碼的理解寫的,實際使用全局變量,一不註意就會出現很多問題。之前我有遇到的問題是,全局變量定義和使用放在一個文件中,程序在運行的過程中,加載了兩次全局變量(兩個內存地址),第二次加載的全局變量和子業務使用的是同一個,第一次加載的全局變量和main函數使用的是同一個,這導致在使用全局變量時,全局變量裏的值還是空的。詳細原因分析,可以參考:xxxxx
方法二:子業務定義相同變量,程序掃描指定路徑,獲取文件內的變量和方法,找到該變量後,保存起來,然後創建相應的對象即可。這是項目內常使用的方法,不容易出錯。代碼結構如下:
IServer.py
from abc import ABCMeta, abstractmethod
class IServer:
@abstractmethod
def DoWithA(self):
pass
@abstractmethod
def DoWithB(self):
pass
IServer_A.py
import IServer
ALIAS = {‘1001‘:‘IServer_A‘};
class IServer_A(IServer.IServer):
def __init__(self):
pass
def DoWithA(self):
print ‘Server_A do with interface A‘
def DoWithB(self):
print ‘Server_A do with interface B‘
IServer_B.py
import IServer
ALIAS = {‘1002‘:‘IServer_B‘};
class IServer_B(IServer.IServer):
def __init__(self):
pass
def DoWithA(self):
print ‘Server_B do with interface A‘
def DoWithB(self):
print ‘Server_B do with interface B‘
子類IServer_A跟方法一不同的地方在於,變量ALIAS 可以定義在文件任何位置,IServer_B必須與類名一致,因為代碼後期會為IServer_A創建對象。
server_framework.py
import os import sys from Server_02.Baselib.misc import Misc; class StubFrameWork(object): def __init__(self): self._server_alias={} self._server_dt={} def initialize(self): self._append_alias_of(os.path.join(SRCPATH, ‘Server‘),self._server_alias) print self._server_alias self._init_server(); print self._server_dt def _init_server(self,): for servertype,server_name in self._server_alias.items(): obj = Misc.sys_create_object(server_name, server_name); if obj is None: raise Exception(‘none‘ % server_name); else: self._server_dt[servertype] = obj; def _append_alias_of(self, folder,dt): for fpath in Misc.sys_list_files(folder): if fpath.find(‘.pyc‘) > 0 or fpath.find(‘__init__.py‘) > 0: continue; module_name = Misc.sys_import_module(fpath); module_obj = sys.modules[module_name]; if ‘ALIAS‘ in dir(module_obj): alias_dt = getattr(module_obj, ‘ALIAS‘); for key in alias_dt.keys(): dt[key] = alias_dt[key] else: pass def CreateServer(self,serverType): if self._server_dt.has_key(serverType): return self._server_dt[serverType] else: return ‘no‘ if __name__ == ‘__main__‘: mfw_path = os.path.join(os.getcwd(), __file__) SRCPATH = os.path.dirname(mfw_path); sfw= StubFrameWork() sfw.initialize() serverType = ‘1001‘ server = sfw.CreateServer(serverType) server.DoWithA()
可執行腳本掃描server下的文件後,遍歷每個文件下的變量和方法,獲取ALIAS變量的值,並保存在框架對象的成員變量中,然後變量該成員變量,將str實例化成相應對象保存起來即可。
通過以上兩個實例的練習,對軟件設計的開放--關閉原則又有了更深一層的體會,對於擴展是開放的,對於修改是關閉的。絕對的修改關閉是不可能的,無論模塊多麽的封閉,都會存在一些無法對之封閉的變化。既然是不可能完全封閉,所以就必須對設計的模塊應該對哪種變化封閉做出選擇,必須先猜測出可能發送的變化種類,然後構造抽象來隔離這些變化。
學習設計模式,絕對是測試人員進階的必修之課,學習不是目的,學習了而且會靈活的應用,能舉一反三,對測試項目的代碼看得更深入,編寫的測試工具越嚴謹化,模塊化,工具越用越方便才是最終目的。工廠方法的學習大概就花了我快一個月的時間了。平常工作日,是完全沒有時間去學習的(餓廠的項目測試力度真的超級重),只能在周末在家把一大半的休息時間花在學習上,才能勉強的趕趕學習進度。但是貴在堅持和不斷的思考,還是會有收獲的快感。
一起來學設計模式-----工廠模式的實踐