1. 前言
最近在看《架构整洁之道》一书,书中反复提到了面向对象编程的 SOLID 原则(在作者的前一本书《代码整洁之道》也是被大力阐释),而面向切面编程(Aop)作为面向对象编程的有力补充,对实践整洁代码更是如虎添翼。
除了整洁之道系列的影响外,本文还致敬、借鉴、补充了Aspect Oriented Programming (AOP) in C# with SOLID一文。
1. Aop 是什么?
在讨论 Aop 之前,我们可以先看看一段很常见的代码。
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 可以把原来一大串的面向过程的代码重构成多个部分,聚焦于每一小部分,使我们的代码 可读性 和 维护性 更高,避免了 代码重复和代码纠缠 的问题。
2. 装饰器实现 AOP
C# 可以使用的 Aop 框架有很多,在我们谈论他们之前,我们可以先利用语言自带的特性,实现基础的 AOP 效果。 最简单的形式莫过于 装饰器模式 ,它的雏形大致如下:
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;
}
}
}
可以看到装饰器只是在原来的对象上面扩展,符合 开放封闭原则。我们在调用的时候,只需显式创建装饰实例对象。
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>>();
虽然解决了易用性,但是我们很快就发现了另一些不尽人意的地方,装饰器模式只能适用于 特定的类型,约束是比较强的。如果我们希望我们示例中的装饰器可以实现通用,就需要找别的方法了。
3. 动态代理实现 Aop
动态代理是指运行时生成,通过隐式重写方法来附加额外的功能,而其中最流行的莫过于 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 的效果,就需要别的方法了。
4. 编译时织入实现 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 效果,比如说借助代码生成器。
5. 代码生成器实现 AOP 效果
T4是常见的文本生成框架,我们可以使用此工具在设计时生成代码。前面我们提到过装饰器模式有特异性的问题,只能针对特定类型实现 AOP 效果,而借助代码生成器,我们可以直接生成对应的代码模板,避免了重复的劳动。由于我个人对 T4 没什么使用经验,有兴趣的读者可以参考Aspect Oriented Programming (AOP) in C# via T4 一文。
除了 T4 之外,Roslyn 也是一个强有力的工具,已经有人基于 Roslyn 实现 AOP 的效果,将 Roslyn 封装为 dotnet 全局工具 ,针对特定的文件插入指定的代码段,有兴趣的读者可以参考 AOP_With_Roslyn 的代码示例。
结语
AOP 是我们 避免代码重复 和 增强代码可读性 的有力工具,是我们编写整洁代码的有力保证,借助 C# 语言自身的特性和诸多强大的开源工具,使我们更专注于代码功能。
C# 中使用面向切面编程(AOP)中实践代码整洁的更多相关文章
-
[译]如何在ASP.NET Core中实现面向切面编程(AOP)
原文地址:ASPECT ORIENTED PROGRAMMING USING PROXIES IN ASP.NET CORE 原文作者:ZANID HAYTAM 译文地址:如何在ASP.NET Cor ...
-
Spring中的面向切面编程(AOP)简介
一.什么是AOP AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面 ...
-
04 Spring:01.Spring框架简介&;&;02.程序间耦合&;&;03.Spring的 IOC 和 DI&;&;08.面向切面编程 AOP&;&;10.Spring中事务控制
spring共四天 第一天:spring框架的概述以及spring中基于XML的IOC配置 第二天:spring中基于注解的IOC和ioc的案例 第三天:spring中的aop和基于XML以及注解的A ...
-
Spring学习手札(二)面向切面编程AOP
AOP理解 Aspect Oriented Program面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术. 但是,这种说法有些片面,因为在软件工程中,AOP的价值体现的并 ...
-
Spring学习笔记:面向切面编程AOP(Aspect Oriented Programming)
一.面向切面编程AOP 目标:让我们可以“专心做事”,避免繁杂重复的功能编码 原理:将复杂的需求分解出不同方面,将公共功能集中解决 *****所谓面向切面编程,是一种通过预编译方式和运行期动态代理实现 ...
-
Spring框架学习笔记(2)——面向切面编程AOP
介绍 概念 面向切面编程AOP与面向对象编程OOP有所不同,AOP不是对OOP的替换,而是对OOP的一种补充,AOP增强了OOP. 假设我们有几个业务代码,都调用了某个方法,按照OOP的思想,我们就会 ...
-
Spring之控制反转——IoC、面向切面编程——AOP
控制反转——IoC 提出IoC的目的 为了解决对象之间的耦合度过高的问题,提出了IoC理论,用来实现对象之间的解耦. 什么是IoC IoC是Inversion of Control的缩写,译为控制 ...
-
设计模式之面向切面编程AOP
动态的将代码切入到指定的方法.指定位置上的编程思想就是面向切面的编程. 代码只有两种,一种是逻辑代码.另一种是非逻辑代码.逻辑代码就是实现功能的核心代码,非逻辑代码就是处理琐碎事务的代码,比如说获取连 ...
-
【串线篇】面向切面编程AOP
面向切面编程AOP 描述:将某段代码“动态”的切入到“指定方法”的“指定位置”进行运行的一种编程方式 (其底层就是Java的动态代理)spring对其做了简化书写 场景: 1).AOP加日志保存到数据 ...
随机推荐
-
SQL初级语句
一)SQL是什么? 结构化查询语言(Structured Query Language)简称SQL, 是一种特殊目的的编程语言,是一种数据库查询和程序设计语言,用于存取数据以及查询.更新和管理关系数据 ...
-
jsp内置对象作业1-用户登录
题目:编写一个jsp程序,实现用户登录,当用户输入的用户名或密码错误时,将页面重定向到错误提示也,并在该页面显示30秒后,自动返回到用户登录页面. 1.用户登录页面 <%@ page langu ...
-
codeforces 425A Sereja and Swaps(模拟,vector,枚举区间)
题目 这要学习的是如何枚举区间,vector的基本使用(存入,取出,排序等),这题的思路来自: http://www.tuicool.com/articles/fAveE3 //vector 可以用s ...
-
Sharepoint中出现webpart部署之后却找不到的一种情况
今天遇到了一个很奇怪的问题,明明代码没有任何问题,部署之后却没有找到添加的webpart,在(web Parts)Galleries中也找不到,最后才发现可能是和服务器的运行速度有关,就像之前遇到的问 ...
-
[转] git reset简介
http://blog.csdn.net/hudashi/article/details/7664464 http://guibin.iteye.com/blog/1014369 http://hi. ...
-
nginx+fastcgi php 使用file_get_contents、curl、fopen读取localhost本站点.php异常的情况
原文:http://www.oicto.com/nginx_fastcgi_php_file_get_contents/ 参考:http://os.51cto.com/art/201408/44920 ...
-
STL容器vector应用注意事项
[1]提前分配足够空间以免不必要的重新分配和复制代价 关于vector容器重新分配和复制及析构释放的代价,请参见随笔<STL容器之vector>. 应用示例对比代码如下: #include ...
-
Mybatis中在log日志或控制台打印执行的sql
最近在调试代码时,需要查看当前操作执行的sql,在日志里没查到,经过修改logback配置后成功,现记录如下:一.Mybatis版本是3.0.6(我正在用的版本)该版本只需要修改logback.xml ...
-
WebApi的过滤器
代码: using System; using System.Collections.Generic; using System.IO; using System.Linq; using System ...
-
2018-2019-2 20165302 《网络对抗技术》Exp4 恶意代码分析
实验要求 1.监控你自己系统的运行状态,看有没有可疑的程序在运行 2.分析一个恶意软件,就分析Exp2或Exp3中生成后门软件:分析工具尽量使用原生指令或sysinternals,systracer套 ...