工厂设计模式是入门模式,也是使用较多的模式,这一篇就总结下我在测试项目时,看到使用的地方以及编写测试桩时基于此模式的实际运用。
实例一:测试的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_dicserverType =‘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 IServerALIAS = {‘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实例化成相应对象保存起来即可。
通过以上两个实例的练习,对软件设计的开放--关闭原则又有了更深一层的体会,对于扩展是开放的,对于修改是关闭的。绝对的修改关闭是不可能的,无论模块多么的封闭,都会存在一些无法对之封闭的变化。既然是不可能完全封闭,所以就必须对设计的模块应该对哪种变化封闭做出选择,必须先猜测出可能发送的变化种类,然后构造抽象来隔离这些变化。
学习设计模式,绝对是测试人员进阶的必修之课,学习不是目的,学习了而且会灵活的应用,能举一反三,对测试项目的代码看得更深入,编写的测试工具越严谨化,模块化,工具越用越方便才是最终目的。工厂方法的学习大概就花了我快一个月的时间了。平常工作日,是完全没有时间去学习的(饿厂的项目测试力度真的超级重),只能在周末在家把一大半的休息时间花在学习上,才能勉强的赶赶学习进度。但是贵在坚持和不断的思考,还是会有收获的快感。