最近在看《架构整洁之道》一书,书中反复提到了面向对象编程的 SOLID 原则(在做者的前一本书《代码整洁之道》也是被大力阐释),而面向切面编程(Aop)做为面向对象编程的有力补充,对实践整洁代码更是如虎添翼。html
除了整洁之道系列的影响外,本文还致敬、借鉴、补充了Aspect Oriented Programming (AOP) in C# with SOLID一文。git
在讨论 Aop 以前,咱们能够先看看一段很常见的代码。github
public string GetSomeOne() { try { var result=DosomeThing(); _logger.Information(result); return result; } catch (Exception e) { _logger.Error(e.Message); return null; } } public string GetOtherOne() { try { var result = DosomeThing(); _logger.Information(result); return result; } catch (Exception e) { _logger.Error(e.Message); return null; } }
这是一段很典型的面向过程的代码,咱们能够看到有相同的异常处理逻辑,若是咱们想要避免重复的代码,咱们至少能够把异常处理封装一下:编程
public string GetOtherOne() { return TryRun<String>(()=> DosomeThing()); } public T TryRun<T>(Func<T> action) { try { return action.Invoke(); } catch (Exception e) { _logger.Error(e.Message); return default(T); } }
代码简洁了不少,可是咱们其实是将真实的方法代码与日志代码纠缠在一块儿,违反了 单一责任原则 。有没有一种可能,让咱们不须要在原来的代码上显式调用 TryCache 呢?架构
一个可能的答案是借助 AOP 来解决。使用 AOP,咱们能够在不改变原来代码的前提下,添加额外的单元功能(如异常处理,日志处理、重试机制等)。 AOP 能够把原来一大串的面向过程的代码重构成多个部分,聚焦于每一小部分,使咱们的代码 可读性 和 维护性 更高,避免了 代码重复和代码纠缠 的问题。框架
C# 可使用的 Aop 框架有不少,在咱们谈论他们以前,咱们能够先利用语言自带的特性,实现基础的 AOP 效果。 最简单的形式莫过于 装饰器模式 ,它的雏形大体以下:async
public class TryHandler<TClient>:IMyClient where TClient : IMyClient { private readonly TClient _client; private readonly ILogger _logger; public TryHandler(TClient client, ILogger logger) { _client = client; _logger = logger; } public string GetOtherOne() { try { var result = DosomeThing(); return result; } catch (Exception e) { _logger.Error(e.Message); return null; } } }
能够看到装饰器只是在原来的对象上面扩展,符合 开放封闭原则。咱们在调用的时候,只需显式建立装饰实例对象。ide
var tryClient=new TryHandler<MyClient>(new MyClient()); tryClient.GetOtherOne();
细心的读者可能还会发现,咱们还能够在这个日志装饰器上面再附加一个装饰器,好比一个针对结果处理的装饰器。函数
var resultClient=new ResultHandler<TryHandler<MyClient>>(tryClient);
可是这样的调用方法仍是不尽人意,想象若是某个对象有三四个装饰器,那么咱们建立实例的时候就须要屡次传递。一个解决方法是 借助依赖注入 (DI) ,只需注册一次服务类型,避免经过建立实例来获取对象。另外,对于 .net core自带的 DI 来讲,更便捷的方法是借助开源类库Scrutor 来注册装饰器对象。工具
services.Decorate<IMyClient, TryHandler<MyClient>>(); services.Decorate<IMyClient, ResultHandler<MyClient>>();
虽然解决了易用性,可是咱们很快就发现了另外一些不尽人意的地方,装饰器模式只能适用于 特定的类型,约束是比较强的。若是咱们但愿咱们示例中的装饰器能够实现通用,就须要找别的方法了。
动态代理是指运行时生成,经过隐式重写方法来附加额外的功能,而其中最流行的莫过于 Castle DynamicProxy了。
Castle DynamicProxy 的常规用法是继承 IInterceptor
接口,经过实现 Intercept
方法来处理代理的逻辑。
public class DoSomethingAspect : IInterceptor { public void Intercept(IInvocation invocation) { try { DoSomething(); invocation.Proceed(); } catch (Exception ex) { throw; } } void DoSomething() { } }
在调用的时候,相似装饰器同样须要建立代理实例。
static void Main(string[] args) { var proxyClient = GetInterfaceProxy<IMyClient>(new MyClient(),new DoSomethingAspect()); proxyClient.GetOtherOne(); } static T GetInterfaceProxy<T>(T instance,params IInterceptor[] interceptors) { if (!typeof(T).IsInterface) throw new Exception("T should be an interface"); ProxyGenerator proxyGenerator = new ProxyGenerator(); return (T)proxyGenerator.CreateInterfaceProxyWithTarget(typeof(T), instance, interceptors); }
有不少开源项目在使用 Castle DynamicProxy,其稳定性和可靠性是值得信赖的,更多的使用方法能够参照官方示例或者第三方开源项目的代码。须要特别注意的是,Castle DynamicProxy 只能做用于接口或者虚方法,这是动态代理的特性(局限)。
除了 Castle DynamicProxy 外, AspectCore也是一个不错的选择。AspectCore 的快速简单应用经过继承 AbstractInterceptorAttribute
的 Attribute类来标记并拦截代理对应的接口或者虚方法(更详细的用法能够参考 做者写的使用方法)。
public interface ICustomService { [CustomInterceptor] void Call(); } public class CustomInterceptorAttribute : AbstractInterceptorAttribute { public async override Task Invoke(AspectContext context, AspectDelegate next) { try { Console.WriteLine("Before service call"); await next(context); } catch (Exception) { Console.WriteLine("Service threw an exception!"); throw; } finally { Console.WriteLine("After service call"); } } }
虽然易用性很好,可是要注意使用的场合,若是是在低层次(如基础设施层、应用入口层等)或者特定的应用模块内使用,对总体架构影响不大。若是是在高层次(逻辑层、核心层、领域层等)使用,则会带来没必要要的依赖污染。
因此并非推荐使用这种 Attribute 拦截代理的方式,好在 AspectCore 的设计考虑到解耦的须要,能够在单独配置代理拦截。
serviceCollection.ConfigureDynamicProxy(config => { config.Interceptors.AddTyped<CustomInterceptorAttribute>(Predicates.ForMethod("ICustomService", "Call")); });
可是不论是 Castle DynamicProxy 仍是 AspectCore 都只能做用与接口或者虚方法,这也是动态代理的局限(特性)。若是咱们想要在不受限制地在非虚方法上实现 AOP 的效果,就须要别的方法了。
进行 AOP 的另外一种方法是经过编译时织入,在编译的程序集内部的方法中添加额外的 IL 代码,附加咱们想要的功能。
PostSharp 是其中比较流行的一种,然而因为其商业化的性质,在这里不作过多介绍。开源方面,Fody 是其中的佼佼者。
Fody 在编译时使用 Mono.Cecil 修改 . net 程序集的 IL 代码。若是你没有 IL 代码方面的知识,能够直接使用基于 Fody 开发的插件。其中最流行的插件是Costura和 Virtuosity。Costura 将依赖项做为资源嵌入,实现多个 DLL 文件合并成一个 exe 的功能,而 Virtuosity 则是在构建的时候将全部成员更改成 virtual
,重写 ORM (如EF的导航属性、NHibernate)、 Mock(RhinoMocks、NMock)以及前面提到的动态代理中须要 virtual
的地方为 virtual
。
Fody 中的插件还有不少,除了 Costura 和 Virtuosity 以外,我我的还使用过 MethodDecorator,实现编译时重写类的方法或者构造函数来实现 AOP 的效果。
全部 Fody 的插件,首先都必须引入一个 FodyWeavers.xml
,并声明使用的插件。
<?xml version="1.0" encoding="utf-8"?> <!--FodyWeavers.xml--> <Weavers> <MethodDecorator /> </Weavers>
不一样的插件在后面的使用方法会有所不一样,以 MethodDecorator 为例,咱们须要新建一个特定格式的 Attribute 类,而后标记在特定的类方法上面。
public class TestService { [FodyTestAttribute] public void DoSomething() { } } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Module)] public class FodyTestAttribute : Attribute { protected object InitInstance; protected MethodBase InitMethod; protected Object[] Args; public void Init(object instance, MethodBase method, object[] args) { InitMethod = method; InitInstance = instance; Args = args; } public void OnEntry() { Console.WriteLine("Before"); } public void OnExit() { Console.WriteLine("After"); } public void OnException(Exception exception) { } }
最后还须要一个 AssemblyInfo.cs
来配置哪些 Attribute 类产生做用。
//AssemblyInfo.cs using System; [module: FodyTest]
从新编译生成,在输出中还能够看到 Fody 的输出。
既然咱们能够在编译时织入 IL 代码,那么咱们是否是能够提早生成咱们想要的 AOP 效果,好比说借助代码生成器。
T4是常见的文本生成框架,咱们可使用此工具在设计时生成代码。前面咱们提到过装饰器模式有特异性的问题,只能针对特定类型实现 AOP 效果,而借助代码生成器,咱们能够直接生成对应的代码模板,避免了重复的劳动。因为我我的对 T4 没什么使用经验,有兴趣的读者能够参考Aspect Oriented Programming (AOP) in C# via T4 一文。
除了 T4 以外,Roslyn 也是一个强有力的工具,已经有人基于 Roslyn 实现 AOP 的效果,将 Roslyn 封装为 dotnet 全局工具 ,针对特定的文件插入指定的代码段,有兴趣的读者能够参考 AOP_With_Roslyn 的代码示例。
AOP 是咱们 避免代码重复 和 加强代码可读性 的有力工具,是咱们编写整洁代码的有力保证,借助 C# 语言自身的特性和诸多强大的开源工具,使咱们更专一于代码功能。