程序员小郑刚步入岗位,但是在公司编码过程中没有受到专业的编码规范的培训,编写出来的程序虽然能够完成指定的功能但是比较不统一,偶尔会别出心裁的设计出自己的简化方法。老王这是从事了软件编码十多年了,现在都快到不惑的年龄了,在软件行业摸爬滚打十多年从事过多个行业,接触过不同公司的编码的规范,在软件代码编写中有独到的认识。
有一天有一个小功能的改动,由于这是一个非常重要的基础系统的功能变动,所以即便是一个小的功能变动公司上上下下都投入了非常高的重视程度。这天老王找到小郑告诉了需要修改这个系统并详细的描述了变动需求,他们两花了一个多小时进行了设计,老王并把需要修改的地方都详细的讲述给小郑,做了非常仔细的设计了。小郑按照老王的设计进行类设计编码设计,单元测试最终实现了功能。以下是他的第一个版本,同样把程序分为了经典的三层基业务逻辑层,数据服务层。
这是业务逻辑层
<span style="white-space:pre"> </span>/// <summary>
<span style="white-space:pre"> </span>/// xxxxx
/// </summary>
/// <param name="qsc">xx队列</param>
/// <param name="route">xx队列</param>
/// <returns>返回联系人的电话号码</returns>
public static string ReplaceVirtualTel(CQueue_SC qsc, CQueue_SCRoute route)
{
CooperateServicesDAL dal = new CooperateServicesDAL();
return dal.GetOrderContactMobilePhone(qsc, route);
}
这时数据逻辑层,
/// <summary>
<span style="white-space:pre"> </span>/// xxxxxx
/// </summary>
/// <param name="qsc">乘客队列</param>
/// <param name="route">航班队列</param>
/// <returns>返回联系人的电话号码</returns>
public string GetOrderContactMobilePhone(CQueue_SC qsc, CQueue_SCRoute route)
{
try
{
//判断接口返回的数据里面的电话是不是虚拟电话
if (IsTelViturl(qsc.Tel))
{
//如果是虚拟电话调用接口返回数据
XmlDocument result = InvokeGetRealTel(qsc, route);
//判断接口中联系人是否正确
if (IsContactPersonRight(result, qsc))
{
return result.SelectSingleNode(@"/Root/Return/ContactMobilePhone").InnerText.Trim();
}
}
return qsc.Tel;
}
catch(Exception ex)
{
return qsc.Tel;
}
}
/// <summary>
/// 判断条件:只要联系人1和联系人2有一个人相同那么就认为是正确的。如果没有相同的姓名就认为是错误的。
/// </summary>
/// <param name="result">调用联系人1数据</param>
/// <param name="qsc">来自联系人2的数据</param>
/// <returns>返回true,乘客一致。返回false,乘客不一致</returns>
private bool IsContactPersonRight(XmlDocument result, CQueue_SC qsc)
{
XmlNodeList nodes = result.SelectNodes(@"/Root/Return/PassengerList/Passenger");
List<string> name = qsc.Name.ToString().Split(' ').ToList();
List<string> nameReturn = new List<string>();
foreach (XmlNode xn in nodes)
{
string namert = xn.SelectSingleNode("PsgName").InnerText.Trim();
nameReturn.Add(namert);
}
var commonNumbers = name.Intersect(nameReturn);
if (commonNumbers.Count() > 0)
{
return true;
}
return false;
}
/// <summary>
/// 判断返回的旅客电话是不是就是虚拟的电话
/// </summary>
/// <param name="tel">旅客电话</param>
/// <returns>如果是虚拟电话返回true,否则返回false</returns>
private bool IsTelViturl(string tel)
{
List<string> virtualTel = ConfigurationManager.AppSettings["VirtualTel"].ToString().Split(' ').ToList();
if (virtualTel.Contains(tel)) {
return true;
}
return false;
}
/// <summary>
/// 调用旅客的真实电话号码
/// </summary>
/// <param name="qsc">队列信息</param>
/// <param name="route">队列信息</param>
/// <returns>返回调用接口返回的数据</returns>
private XmlDocument InvokeGetRealTel(CQueue_SC qsc, CQueue_SCRoute route)
{
try
{
SCAirlinesZoneService.Services service = new SCAirlinesZoneService.Services();
string command = string.Format(requestXML);
string result = service.GetOrderContactMobilePhone(command);
XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(result);
if (xmldoc.SelectSingleNode(@"/Root/Error/Code").InnerText.Trim() != "0000")
{
throw new Exception("系统异常或未找到相应数据");
}
return xmldoc;
}
catch(Exception ex)
{
throw ex;
}
}
小郑第一版本的实现过程,他将逻辑层做了非常简单的封装几乎将所有的业务都放入了数据层里面,是不合理的,这对一个有刚入职的员工来说,一不小心图方便就会把代码业务不严格按照分层的原理进行分成编写。老王看见小郑的第一个版本给小郑提出了一些修改的意见,主要思想是让小郑把程序严格按照程序的分层原理将程序严格分层,业务逻辑都放入逻辑层面,数据访问都放入数据层。这样做的好处在哪里呢,如果按照小郑第一个版本那样做,虽然能够实现功能但是如果接口一变化那么从头到尾几乎所有的程序都不再适用新的变化了。需要重新开发一套流程来让新的业务符合条件,程序的可扩展性非常局限。
小郑按照老王的建议进行了第二个版本的编写:
业务逻辑层
#region 内部变量定义
//虚拟号码存储
private static string[] _virtualTels = null;
#endregion
#region 方法定义
/// <summary>
///xxxxxx
/// </summary>
/// <param name="qsc">队列1</param>
/// <param name="route">队列2</param>
/// <returns>返回联系人的电话号码</returns>
public static string ReplaceContactTel(CQueue_SC qsc, CQueue_SCRoute route)
{
try
{
CQueuePassenger pnrQPsg = new CQueuePassenger();
pnrQPsg.NUM = qsc.NUM;
pnrQPsg.ContactTel = qsc.Tel;
pnrQPsg.PassengerNames = qsc.Name.Split(' ');
//判断输入值中的联系人电话,是否虚拟电话。
if (!IsVirtualTel(pnrQPsg.ContactTel))
return pnrQPsg.ContactTel; //如果不是虚拟电话,那么返回(原联系电话)。
//获取原始订单中的数据
CQueuePassenger orderQPsg = CooperateServicesDAL.InvokeGetRealPassenger(qsc, route);
//因此通过比对旅客姓名来决定是否同一份数据。
if (pnrQPsg.AreSame(orderQPsg))
return orderQPsg.ContactTel; //如果是同一份数据,那么返回从订单提取的联系人电话。
//返回真实联系人电话。
return qsc.Tel;
}
catch (Exception ex)
{
//可添加日志记录服务器连接失败
return qsc.Tel;
}
}
/// <summary>
/// 判旅客电话是不是就是虚拟的电话
/// </summary>
/// <param name="tel">数据的旅客电话</param>
/// <returns>如果是虚拟电话返回true,否则返回false</returns>
public static bool IsVirtualTel(string tel)
{
if (_virtualTels == null)
{
//ConfigurationManager.AppSettings["VirtualTel"].ToString().Split(' ');
_virtualTels = ConfigurationManager.AppSettings["VirtualTels"].ToString().Split(','); //从配置文件获取虚拟电话列表
}
return _virtualTels.Contains(tel.Trim());
}
#endregion
}
数据层的代码
/// <summary>
/// 调用方法获取旅客的电话号码
/// </summary>
/// <param name="qsc">队列信息1</param>
/// <param name="route">队列信息2</param>
/// <returns>返回调用接口返回的数据CQueuePassenger对象</returns>
public static CQueuePassenger InvokeGetRealPassenger(CQueue_SC qsc, CQueue_SCRoute route)
{
try
{
CQueuePassenger queuePassenger = new CQueuePassenger();
SCAirlinesZoneService.Services service = new SCAirlinesZoneService.Services();
service.Timeout = 2000;
string command = string.Format(RequestXML);
string result = service.GetOrderContactMobilePhone(command);
<span style="white-space:pre"> </span>XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(result);
if (xmldoc.SelectSingleNode(@"/Root/Error/Code").InnerText.Trim() != "0000")
{
throw new Exception(xmldoc.SelectSingleNode(@"/Root/Error/Message").InnerText.Trim());
}
XmlNodeList nodes = xmldoc.SelectNodes(@"/Root/Return/PassengerList/Passenger");
string passengers = "";
foreach (XmlNode xn in nodes)
{
passengers += xn.SelectSingleNode("PsgName").InnerText.Trim();
passengers += " ";
}
if (!string.IsNullOrEmpty(passengers))
{
passengers = passengers.Trim();
queuePassenger.PassengerNames = passengers.Split(' ');
}
queuePassenger.ContactTel = xmldoc.SelectSingleNode(@"/Root/Return/ContactMobilePhone").InnerText.Trim();
queuePassenger.Num = qsc.Num;
return queuePassenger;
}
catch (Exception ex)
{
throw ex;
}
}
这里个版本我们可以明显看出业务逻辑判断都封装到了业务逻辑层了,然而数据层只负责获取数据,然后把数据封装成为一个对象。这样做的目的在于如果需要替换接口我们只需要更换数据层里面的获取数据的方法,业务逻辑层可以使用工厂方法进行相应逻辑判断,一定程度上面我们获得了程序的重用性,并在程序的可扩展性上面获得了提升。带来的另外一个好处就是提升了代码可测试性。
基本上可以满足老王的要求了,其实我们发现变动并不是很大,最大的问题就是我们有没有意识将自己的程序编写得足够灵活,这样的代码还有修改的余地,还可以编写得更加的灵活通用,理想的情况可以做到非常的通用,比如使用泛型以及反射进行扩展。到了这一步我们的代码编写完了,我们需要考虑如何去测试这个功能的正确性,为了测试我们编写的代码,我们需要拿出当初当初设计这个逻辑的逻辑处理图。
从这幅流程图里面我们可以明确测试点,以增量方式测试我们的程序是否正确。
1.测试接口完整性以及正确性。
2.测试我们的程序是否满足了要求。
根据这两个要求我们的可以得出测试点如下有7点:
在这里我们的测试用例的设计是按照系统流程图来的,覆盖流程图的每一个分支,而且我们的覆盖到每一个可能出现的异常。比如接口我们就考虑测试他的三个方面,目的是为了保证接口这个部件经过测试之后能够被我们完全信任。第二,顺序性测试,先测试了接口然后在测试调用的代码,因为我们的代码建立在接口调用所获取的数据,如果接口未被测试通过,我们可以模拟接口返回的数据进行代码逻辑的测试。1.调用接口成功的情况。
2.调用接口失败的情况。
3.调用接口超时的情况。
4.有虚拟号且姓名匹配的测试。
5.有虚拟号但姓名不匹配的测试。
6.无虚拟号码。
7.配置了多个虚拟号码的情况。
所以小郑在这一次的收获就是“如何以最小的代价做到代码的易扩展性和可测试性”。