.NET架构小技巧(3)——反射,架构人员法宝I


  如题,这是我的心声,反射在我的开发中用的频次还是比较高的,有一本万利的感觉,一段复杂的代码,可以节省大量的时间;但带来的一个问题性能相对较差,所以要选择适合的场景使用。

  关于C#中的反射基本用法,这里不作详细介绍,官网有详细的说明。

  下面是在网上找了一个医保接口的案件,比如有两个业务接口,033,027,这个接口的传输内容类似xml,但又是有区别的,不是严格意义上的xml格式。

医院就诊卡接口规范(业务:033)

输入参数:


    业务编号
    开始日期
    结束日期

输出参数:


    病历编号
    HIS端结算流水
    医保统筹支付(元)
    医保帐户支付(元)
    患者个人自付(元)
    本次结算总额(元)
    结算类别(1消费,0退费)
    操作人员编号
    操作人员姓名
    支付方式
    
        单据编号
        单据类别
        单据金额(元)
    

医院就诊卡接口规范(业务:027)

输入参数:


  交易码(见上表交易代码)
  交易日期(YYYYMMDD)
  
  发票号
  交易类别

输出参数:


    业务编号
    返回值:0 成功,其他失败
    错误描述信息
    本次交易流水
    
        
            收费项目编码
            收费项目名称
            开立科室编码
            开立科室名称
            开立医生编码
            开立医生姓名
            开单时间(yyyy-MM-dd HH:mm:ss)
            收费时间(yyyy-MM-dd HH:mm:ss)
            数量
            有效标志
            收费人员编号
            执行科室编号
            单据编号
        
        ......
    

  这个医保接口的对接方式入参都是一个Request节点,出参都是Response节点,至少是调用dll,还是通过什么协议发送参数,接收参数,是另外一会事,所以咱们重点来说架构小技巧。

  首先思考的是,我们在.net中是面向对应编程,这些接口的输入参数,输出参数应该对应成实体,当然用字符串拼接也可以,但当接口众多时,一点一点拼接,势必会出错率高,同时个性化处理非常麻烦,另一方面,显得比较low。这时,反射大显身手的机会就来了,假如我只要按输入参数,输出参数的属性定义好实体类,再造一个神器,能把实体转成输入参数字符串,也能把输出参数转成实体类,对我们来说,就是完全的OOP了,“辛苦两个转换方法,幸福所有接口”,当然对开发一个完整的医保接口来说,另一个难点是,从现有系统中组织输入参数实体类,和回写输出参数实体,但也不是架构技巧的重点。

  还是上代码吧:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Xml;

namespace ArchitectureDemo03
{
    class Program
    {
        static void Main(string[] args)
        {
            var readCard033 = new ReadCard033
            {
                TradeCode = "033",
                BeginDate = DateTime.Now,
                EndDate = "20200102"
            };
            var readcard033Back = Send(readCard033);
            Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(readcard033Back));


            var readCard027 = new ReadCard027
            {
                Date = "20201212",
                InvoiceNo = "abcd",
                Time = "121212",
                TradeCode = "003",
                TransType = "123"
            };
            var readCard027Back = Send(readCard027);
            Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(readCard027Back));

        }
        /// 
        /// 模拟输入函数与输出xml对应集合,以作mock
        /// 
        static Dictionary BackDic = new Dictionary {
            {"ReadCard033" ,@"
    病历编号
    HIS端结算流水
    1
    2
    3
    4
    1
    操作人员编号
    操作人员姓名
    支付方式
    
        单据编号
        单据类别
        5
    
"},
            { "ReadCard027",$@"
    业务编号
    0
    错误描述信息
    本次交易流水
    
        
            收费项目编码
            收费项目名称
            开立科室编码
            开立科室名称
            开立医生编码
            开立医生姓名
            2020-12-12 13:14:15
            2020-12-12 13:14:15
            10
            有效标志
            收费人员编号
            执行科室编号
            单据编号
         
        
            收费项目编码
            收费项目名称
            开立科室编码
            开立科室名称
            开立医生编码
            开立医生姓名
            2020-12-12 13:14:15
            2020-12-12 13:14:15
            10
            有效标志
            收费人员编号
            执行科室编号
            单据编号
         
    
"}
        };

        /// 
        /// 封装调用接口
        /// 
        /// 输出参数类型
        /// 输入参数
        /// 
        static T Send(Request request) where T : class
        {
            Console.WriteLine("输入参数:");
            Console.WriteLine(request.ToXML());

            var backXML = BackDic[request.GetType().Name];
            return request.ToResponse(typeof(T), backXML) as T;
        }
    }
    /// 
    /// 请求父类
    /// 
    abstract class Request
    {
        public override string ToString()
        {
            var requestSB = new StringBuilder();
            var type = this.GetType();
            //遍历属性,获取属性值
            foreach (var pro in type.GetProperties())
            {
                //处理Request的子类,所有输入参数都应该继承Request
                if (pro.PropertyType.IsSubclassOf(typeof(Request)))
                {
                    requestSB.AppendLine($"<{pro.Name}>");
                    requestSB.AppendLine($"{pro.GetValue(this)}");
                    requestSB.AppendLine($"");
                }
                else
                {
                    //处理DateTime类型属性
                    if (pro.PropertyType.IsAssignableFrom(typeof(DateTime)))
                    {
                        var value = Convert.ToDateTime(pro.GetValue(this)).ToString("yyyyMMddHHMMSS");
                        requestSB.AppendLine($"<{pro.Name}>{value}");
                    }
                    else
                    {
                        requestSB.AppendLine($"<{pro.Name}>{pro.GetValue(this)}");
                    }
                }
            }
            return requestSB.ToString().Trim();
        }
        /// 
        /// 输成xml输入参数
        /// 
        /// 
        public string ToXML()
        {
            return $"\n{this}\n";
        }
        /// 
        /// 输出参数xml转成实体类
        /// 
        /// 输出参数类型
        /// 输出参数xml
        /// 
        public object ToResponse(Type type, string xml)
        {
            xml = $@"<?xml version=""1.0"" encoding=""utf-8""?>{xml}";
            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(xml);
            var instance = Activator.CreateInstance(type);
            foreach (var pro in type.GetProperties())
            {
                //自定义实体类属性,利用在同一个命名空间里来解瘊这个事情
                if (pro.PropertyType.Namespace == this.GetType().Namespace)
                {
                    var xmlElement = xmlDoc.GetElementsByTagName(pro.Name);
                    if (xmlElement.Count > 0)
                    {
                        var response = ToResponse(pro.PropertyType, $"<{pro.Name}>{xmlElement[0].InnerXml}");
                        pro.SetValue(instance, response);
                    }
                }
                else
                {
                    //泛型集合实体类属性
                    if (pro.PropertyType.IsGenericType)
                    {
                        //获取泛型集合属性的类型
                        var subType = pro.PropertyType.GetGenericArguments()[0];
                        var xmlElement = xmlDoc.GetElementsByTagName(pro.Name);
                        if (xmlElement.Count > 0)
                        {
                            //生成泛型集合属性的实体类
                            var list = Activator.CreateInstance(pro.PropertyType) as IList;
                            //把输出字符串中的列表对应数据添加到泛型属性集合中
                            foreach (XmlNode childItem in xmlElement[0].ChildNodes)
                            {
                                if (childItem.ChildNodes.Count > 0)
                                {
                                    var subInstance = ToResponse(subType, $"<{subType.Name}>{xmlElement[0].ChildNodes[0].InnerXml}");
                                    list.Add(subInstance);
                                }
                            }
                            //设置泛型集合属性的值
                            pro.SetValue(instance, list);
                        }
                    }
                    else
                    {
                        //普通属性
                        var xmlElement = xmlDoc.GetElementsByTagName(pro.Name);
                        if (xmlElement.Count > 0)
                        {
                            var value = Convert.ChangeType(xmlElement[0].InnerText, pro.PropertyType);
                            pro.SetValue(instance, value);
                        }
                    }
                }
            }
            return instance;
        }
    }
    /// 
    /// 医院就诊卡接口规范(业务:033)
    /// 
    class ReadCard033 : Request
    {
        /// 
        /// 业务编号
        /// 
        public string TradeCode { get; set; }
        /// 
        /// 开始日期
        /// 
        public DateTime BeginDate { get; set; }
        /// 
        /// 结束日期
        /// 
        public string EndDate { get; set; }

    }
    /// 
    /// 医院就诊卡接口规范(业务:033)输出参数
    /// 
    class ReadCard033Back
    {
        /// 
        /// 病历编号
        /// 
        public string PatientId { get; set; }

        /// 
        /// HIS端结算流水
        /// 
        public string SiHisOrderNo { get; set; }
        /// 
        /// 医保统筹支付(元)
        /// 
        public decimal PubCost { get; set; }
        /// 
        /// 医保帐户支付(元
        /// 
        public decimal PayCost { get; set; }
        /// 
        /// 患者个人自付(元)
        /// 
        public decimal OwnCost { get; set; }
        /// 
        /// 本次结算总额(元)
        /// 
        public decimal TotCost { get; set; }
        /// 
        /// 结算类别(1消费,0退费)
        /// 
        public int TransType { get; set; }
        /// 
        /// 操作人员编号
        /// 
        public string OperCode { get; set; }
        /// 
        /// 操作人员姓名
        /// 
        public string OperName { get; set; }
        /// 
        /// 支付方式
        /// 
        public string PayType { get; set; }
        /// 
        /// 单据信息
        /// 
        public Invoices Invoices { get; set; }

    }
    /// 
    /// 单据
    /// 
    class Invoices
    {
        /// 
        /// 单据编号
        /// 
        public string INum { get; set; }
        /// 
        /// 单据类别
        /// 
        public string InvoiceType { get; set; }
        /// 
        /// 单据金额(元)
        /// 
        public decimal ISum { get; set; }
    }
    /// 
    /// 医院就诊卡接口规范(业务:027)
    /// 
    class ReadCard027 : Request
    {
        /// 
        /// 交易码(见上表交易代码)
        /// 
        public string TradeCode { get; set; }
        /// 
        /// 交易日期(YYYYMMDD
        /// 
        public string Date { get; set; }
        /// 
        /// 交易时间(HHMMSS)
        /// 
        public string Time { get; set; }
        /// 
        /// 发票号
        /// 
        public string InvoiceNo { get; set; }
        /// 
        /// 交易类别
        /// 
        public string TransType { get; set; }
    }
    /// 
    /// 医院就诊卡接口规范(业务:027)输出参数
    /// 
    class ReadCard027Back
    {
        /// 
        /// 业务编号
        /// 
        public string TradeCode { get; set; }
        /// 
        /// 返回值:0 成功,其他失败
        /// 
        public string Result { get; set; }
        /// 
        /// 错误描述信息
        /// 
        public string Err { get; set; }
        /// 
        /// 本次交易流水
        /// 
        public string HospitalTransNO { get; set; }
        /// 
        /// 明细列表
        /// 
        public List Fees { get; set; }

    }
    /// 
    /// 明细
    /// 
    class Fee
    {
        /// 
        /// 收费项目编码
        /// 
        public string fybm { get; set; }
        /// 
        /// 收费项目名称
        /// 
        public string fymc { get; set; }
        /// 
        /// 开立科室编码
        /// 
        public string ksbm { get; set; }
        /// 
        /// 开立科室名称
        /// 
        public string ksmc { get; set; }
        /// 
        /// 开立医生编码
        /// 
        public string ysbm { get; set; }
        /// 
        /// 开立医生姓名
        /// 
        public string ysxm { get; set; }
        /// 
        /// 开单时间(yyyy-MM-dd HH:mm:ss)
        /// 
        public string kdsj { get; set; }
        /// 
        /// 收费时间(yyyy-MM-dd HH:mm:ss)
        /// 
        public string sfsj { get; set; }
        /// 
        /// 数量
        /// 
        public string fysl { get; set; }
        /// 
        /// 有效标志
        /// 
        public string yxbz { get; set; }
        /// 
        /// 收费人员编号
        /// 
        public string czy { get; set; }
        /// 
        /// 执行科室编号
        /// 
        public string zxks { get; set; }
        /// 
        /// 单据编号
        /// 
        public string InvoiceNo { get; set; }
    }
}

  在上面代码中,重点是Request这个抽象类,其实原ToXML中把实体类转成输入参数字符串,ToResponse是把输出字符串转成实体供程序使用。这两个方法的具体实现,在代码中有注释,这是使用反射属性来实现转换的,当然这种转换,用XmlSerializer也可以实现,但这样就失去了对属性的灵活控制,比如demo中的时间类型属性的转换。

  “辛苦两个转换方法,幸福所有接口”,就是ToXML和ToResponse,如果这个医保还有几十个接口,那就只用定义每个业务函数对应的输入参数,输出参数实体类就可以了。这样简化了程序模块的耦合,层次也很清晰,但你可能在问性能了,确实,反射带来灵活的同时就会损失性能,但大部分医保接口都是以dll的形式存在,和his系统配合使用,就是一个时刻只有一个接口在调用,对性能的容忍度还是够用的。

    想要更快更方便的了解相关知识,可以关注微信公众号