设计模式在项目架构中的最佳实践 – 生成器模式/适配器模式
一年多来,做了几个项目,虽然没有什么技术和业务难度,但是也不能白白浪费了自己一年的光景,除了技术也业务外,总有一些知识值得我们去总结和学习;
这么些年埋头于苦干,很少抽出时间来整理提高自己的综合水平.发现了身的不足够,开始重新复习一下大学里的那些理论知识.此时的学习,和当时相比,总有一种醍醐灌顶的感觉;今天就总设计模式这个方面来分析总结一下最近做过的一个项目;
项目的内容和范围比较简单,一家国际物流公司,需要将中国产生的物流订单数据,安装规定的格式(xml)与Global项目进行交互,项目分两个部分,一个是业务网关,负责全球数据接收和分发;一个是转换器功能的系统,将国内的数据转换为符合Schema标准文件发送到业务网关,接收网关发送来的数据并转换为国内标准入库;
项目的要求,及时性,可靠性,准确性,可追溯性,要求比较高,但由于与此次内容无关,我们这里不做讨论,将来有机会写专门的文章来讨论;另一方要求比较高的,就是***可维护性,可变更性***,由于系统可能使用10年以上,加上电商等各种业务模式带来的业务创新,对系统的灵活性要求也比较,为了面对这些不确定的业务,必须有一个良好的系统架构来应对这些问题;
项目简述:
1:项目不是从0开始,以后TMS系统已经用了N年,存在着大量成熟稳定的业务,N年前开发的东西,且业务问题,原则上讲,我们应该尽最大能力集成以后的业务功能,减少新项目的开发和测试工作量;系统已存在的视图和存储过程,80%的功能都可以在稍微修改的基础上进行重用;
2:项目需要从TMS系统中抽取数据,生成符合Schema标准的xml与Global系统交互;Schema文件中存在着大量重复的业务校验;
Schema之一的结构如下:
| -- 运单数据
| | -- 主数据
| | -- 发票数据
| | -- 体积重量数据
| | -- 件数详
细
整个运单由N个节点组成,每个节点又有可能是独立的节点,且由更细的节点组成;
问题描述:
所以在整个业务上,如何做到重用和老的功能,和数据如何与Schema结构如何简单对应,是我们这次重点考虑的内容;
解决方案:
通过设计模式解决业务痛点;
适配器模式:
将已有的存储过程和视图如何转换为xml结构:
业务痛点:
- 旧系统的字段与Schema文件的字段不一致
- 旧系统可能用功能有视图,有存储过程,调用方式不统一
- Schema字段众多,只运单主数据全部字段就有200多个
- 根据以往经验,Schema随时会增减节点和字段
解决方法:
第一个问题解决起来比较简单,也没有投机取巧之处,只能一点一点的进行业务对应,查询结果出来前mapping,第二个问题,为了增加灵活性,设计了一张取数据的的function表:
FunctionName | View/SP | GetDataType | Schema节点 |
---|---|---|---|
获取主数据 | V_GetXXMain | VIEW | 运单数据.主数据 |
获取发票数据 | V_GetXXInvoice | VIEW | 运单数据.发票数据 |
… | … | … | … |
这个时候虽然还没用到设计模式,但已经开始灵活配置了;这个时候,专门设计了共通的Controller来通过传输Function Name和参数来获取相关的数据,Controller根据获取数据的类型(视图/存储过程)来自动拼接参数和调用相应的DBHelper,来获取相应的数据;注意此时获取的数据还是DataTable格式的数据,而我们想要的是XML格式的数据;
按照传统的办法,我们需要将一个个DataTable的字段转换成XML的各个节点拼接起来,理论上这种办法也行,但是几百上千的字段很容易出错,将来业务发生变更时,改动起来也很不方便(需要找到相应的节点,增加或删除一个字段),有没有好的办法呢,是否可以做一个DataTable和XML的转换适配器,来减轻简单重复的工作量,在满足灵活业务需求的同时,还可以把业务与技术分离;
为了解决以上问题的痛点,专门设计了一个DataTable转换为XML类的适配器;思路如下:
-
将XML转换为Class实体,这里用泛型T来表示
-
将获取的数据转换为DataView(其实转不转都无所谓,之所以转换是因为DataView查询起来方便)
-
获取实体的PropertyInfo属性信息
-
循环属性去DataView中获取每个节点对应的数据值
-
通过反射给每个实体动态赋值
-
通过递归,循环给子节点动态赋值
这里有三个技术点比较重要,分别是反射,递归和过滤重复数据.分别解决不同的问题痛点;
public class EntityDataAdapter
{
/// <summary>
/// 动态的将Table数据填充到实体上面,获取List类型
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
/// <param name="obj">实体对象</param>
/// <param name="myDataView">数据</param>
/// <returns></returns>
public static List<T> dymincTable2ListObject<T>(T obj, DataView myDataView) where T : new()
{
List<T> list = new List<T>();
int count = myDataView.Table.Rows.Count;
//如果是list数组,则批量循环赋值
for (int i = 0; i < count; i++)
{
obj = new T();
#region 转换成单条,循环添加
DataTable dtNew = new DataTable();
dtNew = myDataView.ToTable().AsEnumerable().Skip(i).Take(1).CopyToDataTable<DataRow>();
#endregion
obj = dymincTable2Object(obj, dtNew.DefaultView);
list.Add(obj);
}
return list;
}
/// <summary>
/// 动态的将Table数据填充到实体上面
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
/// <param name="obj">实体对象</param>
/// <param name="myDataView">数据</param>
/// <returns></returns>
public static T dymincTable2Object<T>(T obj, DataView myDataView)
{
//将table转换为view,方便操作
//DataView myDataView = new DataView(dt);
//取得类的所有属性
PropertyInfo[] props = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
//定义给属性赋值时的不重复字段
ArrayList arrayList = new ArrayList();
//循环属性进行赋值操作
foreach (PropertyInfo item in props)
{
//如果是基本类型和string 没节点的类型
if (item.PropertyType.IsEquivalentTo(typeof(System.String)) || item.PropertyType.IsEquivalentTo(typeof(System.Int32)) || item.PropertyType.IsEquivalentTo(typeof(System.DateTime)))
{
arrayList.Add(item.Name);
}
else
{
string strClassName;
//如果是非数组的节点,递归调用,数组节点,则调用其他方法
if (item.PropertyType.GenericTypeArguments.Length == 0)
{
// 非数组节点
strClassName = item.PropertyType.FullName;
//如果不是String类型的,通过反射创建一个新的类,递归调用
Type type;
object _obj;
Assembly asmb = Assembly.LoadFrom("Model.dll");//跨程序集的反射
type = asmb.GetType(strClassName);
//type = Type.GetType(strClassName);//通过string类型的strClass获得同名类“type”
_obj = System.Activator.CreateInstance(type);//创建type类的实例 "obj"
//递归调用
object listObj = dymincTable2Object(_obj, myDataView);
//给属性赋值
ReflectSetter(obj, item.Name, listObj);
}
}
}
//过滤重复数据(解决对象和table映射层级或者有笛卡尔的情况)
DataTable newTable = myDataView.ToTable(true, (string[])arrayList.ToArray(typeof(string)));
//装配要赋值的hash
Hashtable hashtable = new Hashtable();
// key value的形式,批量给属性赋值
foreach (DataColumn item in newTable.Columns)
{
hashtable.Add(item.ColumnName, newTable.Rows[0][item.ColumnName].ToString());
}
//动态给对象赋值
ReflectSetter(obj, hashtable);
return obj;
}
/// <summary>
/// 反射设置对象的属性值(批量设置)
/// </summary>
/// <param name="obj"></param>
/// <param name="hashtable">key:propertyName;value:propertyValue</param>
public static void ReflectSetter(object obj, Hashtable hashtable)
{
var type = obj.GetType();
foreach (DictionaryEntry item in hashtable)
{
var propertyInfo = type.GetProperty(item.Key.ToString());
propertyInfo.SetValue(obj, item.Value.ToString());
}
}
/// <summary>
/// 反射获取对象的属性值
/// </summary>
/// <param name="obj"></param>
/// <param name="propertyName"></param>
/// <returns></returns>
public static object ReflectGetter(object obj, string propertyName)
{
var type = obj.GetType();
var propertyInfo = type.GetProperty(propertyName);
var propertyValue = propertyInfo.GetValue(obj);
return propertyValue;
}
/// <summary>
/// 反射设置对象的属性值
/// </summary>
/// <param name="obj"></param>
/// <param name="propertyName"></param>
/// <param name="propertyValue"></param>
public static void ReflectSetter(object obj, string propertyName, object propertyValue)
{
var type = obj.GetType();
var propertyInfo = type.GetProperty(propertyName);
propertyInfo.SetValue(obj, propertyValue);
}
}
生成器模式
运单数据生成方式有N种组合方案
业务痛点:
由于各地,各国的操作习惯不同,系统又有及时性的要求,运单数据在产生的很短时间段内就会被发送到业务网关系统,同理本地Local系统也会接收到不同节点的运单数据;比如有些运单会先录入主信息后发送,有些运单需要录入发票后才能发送等;由于某种原因,在Local接收接收数据时,有可能先接收到了发票信息,而没有主数据信息等;要解决这些当然可以写无数个if else,但是在将来维护和新功能修改时,阅读老代码逻辑将是灾难式的;
解决方法:
这和我们组装电脑很类似,显示器,鼠标,键盘,硬盘,cpu这些都是固定的东西,每个部件都是单独的一个东西,完成特定的功能,组合起来可以完成一个整体的功能;
我们把XML的每个节点构建成一个component(这里又和上面的取数据的的function表结合起来了),当我们需要哪个component,或者接收过来的数据有哪些节点,将这些需要的数据组成一个整体的xml,再进行其他的操作;
扩展阅读:
在生成器(Builder)模式和组合模式(Composite)这两个概念方面,有时候还分不清楚,我的这种模式到底属于Builder模式还是Composite模式,感觉解决方法上面都符合这两个模式的定义,实际在设计开发过程中,也并不在意它是哪种模式,而是关注功能实现;
系统中自定义了一个消息队列,通过轮训的方式来新消息的处理,理论上也可以通过观察者模式来实现更及时的消息处理;
在实现Adapter模式的同时,使用到了IOC的控制理念,将创建xml节点的控制交给了系统通过判断是否有数据来完成生成新的节点;
不足之处:日志与业务的解耦不足,由于需要记录大量的业务日志,没有想到什么好的方法使业务日志与代码的解耦.
借鉴了设计模式的思想,具体的代码实现与教科书上差别较大;