设计模式在项目架构中的最佳实践 -- 生成器模式适配器模式

设计模式在项目架构中的最佳实践 – 生成器模式/适配器模式

​ 一年多来,做了几个项目,虽然没有什么技术和业务难度,但是也不能白白浪费了自己一年的光景,除了技术也业务外,总有一些知识值得我们去总结和学习;

​ 这么些年埋头于苦干,很少抽出时间来整理提高自己的综合水平.发现了身的不足够,开始重新复习一下大学里的那些理论知识.此时的学习,和当时相比,总有一种醍醐灌顶的感觉;今天就总设计模式这个方面来分析总结一下最近做过的一个项目;

​ 项目的内容和范围比较简单,一家国际物流公司,需要将中国产生的物流订单数据,安装规定的格式(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表:

FunctionNameView/SPGetDataTypeSchema节点
获取主数据V_GetXXMainVIEW运单数据.主数据
获取发票数据V_GetXXInvoiceVIEW运单数据.发票数据

​ 这个时候虽然还没用到设计模式,但已经开始灵活配置了;这个时候,专门设计了共通的Controller来通过传输Function Name和参数来获取相关的数据,Controller根据获取数据的类型(视图/存储过程)来自动拼接参数和调用相应的DBHelper,来获取相应的数据;注意此时获取的数据还是DataTable格式的数据,而我们想要的是XML格式的数据;

​ 按照传统的办法,我们需要将一个个DataTable的字段转换成XML的各个节点拼接起来,理论上这种办法也行,但是几百上千的字段很容易出错,将来业务发生变更时,改动起来也很不方便(需要找到相应的节点,增加或删除一个字段),有没有好的办法呢,是否可以做一个DataTable和XML的转换适配器,来减轻简单重复的工作量,在满足灵活业务需求的同时,还可以把业务与技术分离;

​ 为了解决以上问题的痛点,专门设计了一个DataTable转换为XML类的适配器;思路如下:

  1. 将XML转换为Class实体,这里用泛型T来表示

  2. 将获取的数据转换为DataView(其实转不转都无所谓,之所以转换是因为DataView查询起来方便)

  3. 获取实体的PropertyInfo属性信息

  4. 循环属性去DataView中获取每个节点对应的数据值

  5. 通过反射给每个实体动态赋值

  6. 通过递归,循环给子节点动态赋值

    这里有三个技术点比较重要,分别是反射,递归和过滤重复数据.分别解决不同的问题痛点;

 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节点的控制交给了系统通过判断是否有数据来完成生成新的节点;

​ 不足之处:日志与业务的解耦不足,由于需要记录大量的业务日志,没有想到什么好的方法使业务日志与代码的解耦.

​ 借鉴了设计模式的思想,具体的代码实现与教科书上差别较大;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值