软件的开发和交付基本是两批人,提高程序的灵活性可以减少开发人员和交付人员的很多工作。试想一下,一个HIS程序中包含了德卡、精伦、华视等多种身份证读卡器的功能,但是具体项目使用哪一种是在代码中写死调用的,如果突然医院要换读卡器,交付人员就得联系开发人员改代码然后编译生成发给现场,现场再发布。
我以前怎么解决这种问题的呢?定义一个身份证读卡器的接口,并在其中写明需要用到的方法,不同厂商的身份证读卡器都继承这个接口,再调用对应厂商的API实现接口中的方法。定义一个枚举,包含已实现的各种读卡器,通过在配置文件增加一项,配置当前项目使用的读卡器类型,在程序中读取配置项,通过判断逻辑来实例化接口,这样交付人员就可以通过修改配置项的方式灵活应对项目读卡器设备更换的问题了。
现在可以通过Unity来实现,利用配置文件来注册接口和实现类,同样可以通过修改配置文件的形式来灵活处理前面提出的读卡器的问题。Unity注册接口和实现类有通过代码中添加 _unityContainer.RegisterType
面向切面编程简称AOP,面向切面编程的目的是分离关注点,通俗的说是指使用 aop 你可以一心关注你的正事,而其他的事交给别人处理。AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。
在工作中经常会用到写日志,例如这样的场景:跟第三方厂商有交互,无论在开发过程还是交付后功能出现问题时都需要排查,排查基本就离不开查看日志。以前常规的做法时在调用第三方前写一次日志,把入参、当前时间等信息写入日志中,调用第三方接口获取到出参后将第三方返回所使用时间、出参等信息写入日志。这种做法的弊端时每个方法都要这样书写一遍,如果突然觉得要暂时关闭日志的话,也不方便快速实现。
接下来直接上代码,通过模拟同一套程序支持SqlServer和Oracle,然后可以通过配置文件切换,并利用切面编程思想实现记录日志。
第一步:通过Nuget安装程序集,使用到的程序集:
第二步:书写接口
namespace Infrastructure.Ioc.IBLL { ////// 数据访问接口 /// public interface IDbInterface { string Insert(int id); string Delete(); string Update(); string Query(); } }
第三步:书写实现类
using Infrastructure.Ioc.IBLL; namespace Infrastructure.Ioc.BLL { public class DbOracle : IDbInterface { public string Delete() { return "Oracle执行删除"; } public string Insert(int id) { return "Oracle执行插入" + id; } public string Query() { return "Oracle执行查询"; } public string Update() { return "Oracle执行更新"; } } }
using Infrastructure.Ioc.IBLL; namespace Infrastructure.Ioc.BLL { public class DbMSSQL : IDbInterface { public string Delete() { return "MSSQL执行删除"; } public string Insert(int id) { return "MSSQL执行插入"+id; } public string Query() { return "MSSQL执行查询"; } public string Update() { return "MSSQL执行更新"; } } }
第四步:AOP日志行为,必须继承自 IInterceptionBehavior
using System; using System.Collections.Generic; using System.IO; using System.Text; using Unity; using Unity.Interception.InterceptionBehaviors; using Unity.Interception.PolicyInjection.Pipeline; using Unity.Interception.PolicyInjection.Policies; namespace Infrastructure.Ioc { public class LogAfterBehavior : IInterceptionBehavior { public bool WillExecute => true; public IEnumerableGetRequiredInterfaces() { return Type.EmptyTypes; } public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext) { StringBuilder param = new StringBuilder(); //入参 for (var i = 0; i < input.Arguments.Count; i++) { param.AppendLine(string.Format("{0}={1}", input.Arguments.ParameterName(i), input.Arguments[i].ToString())); } LogHelper.WriteLog(string.Format("Do somthing before method {0}.{1} invoke by Behavior.Input Params:{2}", input.Target.ToString(), input.MethodBase.Name, param.ToString()));//方法执行前执行 //继续向后执行方法 IMethodReturn methodReturn = getNext()(input, getNext); LogHelper.WriteLog(string.Format("Do somthing after method {0}.{1} invoke by Behavior.Output Params:{2}", input.Target.ToString(), input.MethodBase.Name, methodReturn.ReturnValue.ToString()));//方法执行前执行 return methodReturn; } } public static class LogHelper { public static void WriteLog(string context, string fileName = "debug.log") { string filePath = @"C:\Unity"; if (!System.IO.Directory.Exists(filePath)) { System.IO.Directory.CreateDirectory(filePath); } fileName = System.IO.Path.Combine(filePath, fileName); using (System.IO.FileStream fileStream = System.IO.File.Open(fileName, System.IO.FileMode.Append)) { context = "【" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "】" + context; StreamWriter sw = new StreamWriter(fileStream); sw.BaseStream.Seek(0, SeekOrigin.End); sw.WriteLine(context); sw.WriteLine(Environment.NewLine);//换行 sw.Flush(); sw.Close(); } } } }
第五步:Unity配置文件
第六步:调用
using Infrastructure.Ioc.IBLL; using Microsoft.Practices.Unity.Configuration; using System; using System.Configuration; using System.IO; using System.Windows.Forms; using Unity; namespace IOCTestForm { public partial class Form1 : Form { public Form1() { InitializeComponent(); OnInit(); } private void OnInit() { ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap(); fileMap.ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "Unity.Config");//找配置文件的路径 Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None); UnityConfigurationSection section = (UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);//SectionName可自定义,UnityConfigurationSection.SectionName->unity IUnityContainer container = new UnityContainer(); section.Configure(container, "testContainer");//该名称需跟配置文件保持一致 _unityContainer = container; } private void msSqlButton_Click(object sender, EventArgs e) { IDbInterface db = _unityContainer.Resolve("MsSql"); //IDbInterface db = _unityContainer.Resolve (); db.Query(); MessageBox.Show(db.Insert(1)); } private IUnityContainer _unityContainer; private void oracleButton_Click(object sender, EventArgs e) { IDbInterface db = _unityContainer.Resolve ("Oracle"); //IDbInterface db = _unityContainer.Resolve (); db.Query(); MessageBox.Show(db.Insert(2)); } private void refreshButton_Click(object sender, EventArgs e) { OnInit(); } } }
是通过增加一个Form窗体然后通过按钮点击来进行测试的。可以发现调用部分并不依赖具体实现部分,实现了解耦。
为了演示同一个接口允许有多个实现类注册进容器中,配置文件写成了上述所示,如果项目运行期间只会使用具体某一个,可以注释带Name属性的节点,然后打开注释部分。
实际应用中可以通过修改配置文件, 中的mapTo来实现不同实现类的切换,使用过程中有一个问题就是配置文件在程序运行OnInit中加载后,进行修改没法自动重新加载(如果哪位知道如何自动加载烦请留言告知一声)这里简单通过一个刷新然后重新加载一次配置文件来实现了。
调用 Infrastructure.Ioc.BLL.DbOracle 实现类中的任何方法都会自动写入日志,日志中可以写明调用的哪个实现类的哪个方法,方法入参列表是什么,方法返回值是什么。
调用 Infrastructure.Ioc.BLL.DbMSSQL 实现类中的方法不会写入日志,应该该注册节点下没AOP的配置内容。
日志如下: