C# 中使用面向切面编程(AOP)中实践代码整洁

简介:

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 的输出。
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# 语言自身的特性和诸多强大的开源工具,使我们更专注于代码功能。

目录
相关文章
|
23天前
|
缓存 C# Windows
C#程序如何编译成Native代码
【10月更文挑战第15天】在C#中,可以通过.NET Native和第三方工具(如Ngen.exe)将程序编译成Native代码,以提升性能和启动速度。.NET Native适用于UWP应用,而Ngen.exe则通过预编译托管程序集为本地机器代码来加速启动。不过,这些方法也可能增加编译时间和部署复杂度。
|
2天前
|
安全 Java 编译器
什么是AOP面向切面编程?怎么简单理解?
本文介绍了面向切面编程(AOP)的基本概念和原理,解释了如何通过分离横切关注点(如日志、事务管理等)来增强代码的模块化和可维护性。AOP的核心概念包括切面、连接点、切入点、通知和织入。文章还提供了一个使用Spring AOP的简单示例,展示了如何定义和应用切面。
27 1
什么是AOP面向切面编程?怎么简单理解?
|
6天前
|
XML Java 开发者
论面向方面的编程技术及其应用(AOP)
【11月更文挑战第2天】随着软件系统的规模和复杂度不断增加,传统的面向过程编程和面向对象编程(OOP)在应对横切关注点(如日志记录、事务管理、安全性检查等)时显得力不从心。面向方面的编程(Aspect-Oriented Programming,简称AOP)作为一种新的编程范式,通过将横切关注点与业务逻辑分离,提高了代码的可维护性、可重用性和可读性。本文首先概述了AOP的基本概念和技术原理,然后结合一个实际项目,详细阐述了在项目实践中使用AOP技术开发的具体步骤,最后分析了使用AOP的原因、开发过程中存在的问题及所使用的技术带来的实际应用效果。
22 5
|
8天前
|
测试技术 C# 数据库
C# 一分钟浅谈:测试驱动开发 (TDD) 实践
【10月更文挑战第18天】测试驱动开发(TDD)是一种软件开发方法论,强调先编写测试代码再编写功能代码,以确保代码质量和可维护性。本文从 TDD 的基本概念入手,详细介绍了其核心步骤——编写测试、运行测试并失败、编写代码使测试通过,以及“红绿重构”循环。文章还探讨了 TDD 的优势,包括提高代码质量、促进设计思考、减少调试时间和文档化。此外,文中分析了常见问题及解决方案,如测试覆盖率不足、测试代码过于复杂、忽视重构和测试依赖过多,并通过一个简单的计算器类的代码案例,展示了 TDD 的实际应用过程。
21 1
|
24天前
|
存储 JSON API
HTTP 请求与响应处理:C#中的实践
【10月更文挑战第4天】在现代Web开发中,HTTP协议至关重要,无论构建Web应用还是API开发,都需要熟练掌握HTTP请求与响应处理。本文从C#角度出发,介绍HTTP基础知识,包括请求与响应结构,并通过`HttpClient`库演示如何发送GET请求及处理响应,同时分析常见错误并提供解决方案,助你更高效地完成HTTP相关任务。
70 2
|
28天前
|
数据采集 C# 数据库
数据验证与错误处理:C#中的实践
【10月更文挑战第1天】在软件开发中,数据验证与错误处理至关重要,不仅能提升程序的健壮性和安全性,还能改善用户体验。本文从基础概念入手,详细介绍了C#中的数据验证方法,包括使用自定义属性和静态方法验证数据,以及常见的错误处理技巧,如Try-Catch-Finally结构和自定义异常。通过具体示例,帮助读者掌握最佳实践,构建高质量应用。
63 3
|
28天前
|
C#
C# 图形验证码实现登录校验代码
C# 图形验证码实现登录校验代码
71 2
|
21天前
|
开发框架 缓存 算法
开源且实用的C#/.NET编程技巧练习宝库(学习,工作,实践干货)
开源且实用的C#/.NET编程技巧练习宝库(学习,工作,实践干货)
|
21天前
|
安全 C# 数据安全/隐私保护
实现C#编程文件夹加锁保护
【10月更文挑战第16天】本文介绍了两种用 C# 实现文件夹保护的方法:一是通过设置文件系统权限,阻止普通用户访问;二是使用加密技术,对文件夹中的文件进行加密,防止未授权访问。提供了示例代码和使用方法,适用于不同安全需求的场景。
|
2月前
|
API C#
C# 一分钟浅谈:文件系统编程
在软件开发中,文件系统操作至关重要。本文将带你快速掌握C#中文件系统编程的基础知识,涵盖基本概念、常见问题及解决方法。文章详细介绍了`System.IO`命名空间下的关键类库,并通过示例代码展示了路径处理、异常处理、并发访问等技巧,还提供了异步API和流压缩等高级技巧,帮助你写出更健壮的代码。
38 2