转载自: https://www.cnblogs.com/dotnet261010/p/12285867.html ,https://blog.csdn.net/sundenskyqq/article/details/7597610
一、什么是AOP
AOP:Aspect Oriented Programming的缩写,意为面向切面编程。维基百科的解释:是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知机制,能够对被声明为“切点”的代码块进行统一管理与装饰。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。
AOP是OOP思想的延续。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
为什么要学习AOP呢?
AOP的应用场景非常广泛,在一些高级工程师或者架构师的面试过程中,频率出现的比较多。
二、编程思想的发展路线
1、POP
POP:Procedure Oriented Programming的缩写,即面向过程编程,是一种以过程为中心的编程思想。
面向过程是分析出解决问题的步骤,然后用函数或者方法,把这些步骤一步一步的实现,使用的时候在一个一个的一次调用函数或者方法,这就是面向过程编程。最开始的时候都是面向过程编程。面向过程是最为实际的一种思考方式。就算是面向对象编程,里面也是包含有面向过程的编程思想,因为面向过程是一种基础的编程思考方式,它从实际出发来考虑如何实现需求。
POP的不足:面向过程编程,只能处理一些简单的问题,无法处理一些复杂的问题。如果问题很复杂,全部以流程来思考的话,会发现流程很混乱,甚至流程都不能进行下去。
2、OOP
OOP:Object Oriented Programming的缩写,即面向对象编程。
早期的计算机编程都是面向过程的,因为早期的编程都比较简单。但是随着时间的发展,需求处理的问题越来越多,问题就会越来越复杂,这时就不能简单的使用面向过程的编程了,就出现了面向对象编程。在计算机里面,把所有的东西都想象成一种事物。现实世界中的对象都有一些属性和行为,也就对应计算机中的属性和方法。
面向对象编程就是把构成问题的事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
我们以一栋大楼为例来说明OOP的不足。
我们把系统比喻成一栋楼,类或者对象是砖块,砖块组成了一面墙,多面墙构成一间房间,多个房间构成一栋楼。
这就好比一个模块的功能是由多个类实现,模块又组成了某一项服务,多个服务构成了一个完整的系统。一个系统开发完成并不代表就真的完成了,以后肯定会有各种需求的变更出现,需求变更出现以后就要去修改代码,代码都在类里面,这就相当于去修改类。如果是小范围的修改影响还不是很大,如果是大范围的修改,影响就比较大了。即使每次修改都很小,但是如果经常进行修改,影响也会很大。就会造成系统的不稳定。我们得出结论:类应该是固定的,不应该频繁的去修改,甚至是不允许修改。这也是为什么有那么多的设计原则和设计模式。大部分的设计模式都是为了解决这类问题的,即在不修改类的前提下去扩展功能。
OOP的不足:产生新的需求会导致程序代码不断的进行修改,容易造成程序的不稳定。
如果非常了解OOP的话,那么我们应该知道,从对象的组织角度来讲,分类方法都是以继承关系为主线的,我们称为纵向。如果只使用OOP思想的话,会带来两个问题:
1、共性问题。
2、扩展问题,需要对先有类进行扩展时就比较困难了。
OOP与POP的区别:
在对比面向过程的时候,面向对象的方法是把事物最小化为对象,包括属性和方法。当程序的规模比较小的时候,面向过程编程还是有一些优势的,因为这时候程序的流程是比较容易梳理清楚的。以早上去上班为例,过程就是起床、穿衣、刷牙洗脸、去公司。每一步都是按照顺序完成的,我们只需要按照步骤去一步一步的实现里面的方法就行了,最后在依次调用实现的方法即可,这就是面向过程开发。
如果使用面向对象编程,我们就需要抽象出来一个员工类,该员工具有起床、穿衣、刷牙洗脸、去公司的四个方法。但是,最终要实现早上去上班的这个需求的话,还是要按照顺序依次来调用四个方法。最开始的时候,我们是按照面向过程的思想来思考该需求,然后在按照面向对象的思想来抽象出几个方法,最终要实现这个需求,还是要按照面向过程的顺序来实现。
面向对象和面向过程的区别仅仅是在思考问题方式上面的不同。最终你会发现,在你实现这个需求的时候,即使使用了面向对象的思想抽象出来了员工类,但是最后还是要使用面向过程来实现这个需求。
3、AOP
AOP:Aspect Oriented Programming的缩写,即面向切面编程。是对OOP的一种补充,在不修改原始类的情况下,给程序动态添加统一功能的一种技术。
OOP关注的是将需求功能划分为不同的并且相对独立、封装良好的类,依靠继承和多态来定义彼此的关系。AOP能够将通用需求功能从不相关的类中分离出来,很多类共享一个行为,一旦发生变化,不需要去修改很多类,只需要去修改这一个类即可。
AOP中的切面是指什么呢?切面指的是横切关注点。看下面一张图:
OOP是为了将状态和行为进行模块化。上图是一个商场系统,我们使用OOP将该系统纵向分为订单管理、商品管理、库存管理模块。在该系统里面,我们要进行授权验证。像订单、商品、库存都是业务逻辑功能,但是这三个模块都需要一些共有的功能,比如说授权验证、日志记录等。我们不可能在每个模块里面都去写授权验证,而且授权验证也不属于具体的业务,它其实属于功能性模块,并且会横跨多个业务模块。可以看到这里是横向的,这就是所谓的切面。通俗的将,AOP就是将公用的功能给提取出来,如果以后这些公用的功能发生了变化,我们只需要修改这些公用功能的代码即可,其它的地方就不需要去更改了。所谓的切面,就是只关注通用功能,而不关注业务逻辑,而且不修改原有的类。
AOP优势:
- 将通用功能从业务逻辑中抽离出来,提高代码复用性,有利于后期的维护和扩展。
- 软件设计时,抽出通用功能(切面),有利于软件设计的模块化,降低软件架构的复杂度。
AOP的劣势:
- AOP的对OOP思想的一种补充,它无法单独存在。如果说单独使用AOP去设计一套系统是不可能的。在设计系统的时候,如果系统比较简单,那么可以只使用POP或者OOP来设计。如果系统很复杂,就需要使用AOP思想。首先要使用POP来梳理整个业务流程,然后根据POP的流程,去整理类和模块,最后在使用AOP来抽取通用功能。
AOP和OOP的区别:
面向对象的编程(OOP)方法是在面向过程的编程方法基础上进行的改进,而面向方面编程(AOP)方法又是在面向对象编程(OOP)方法的基础上进行改进而来的一种创新的软件开发方法。
- 面向领域不同 :OOP针对问题领域中以及业务处理过程中存在的实体及其属性和操作进行抽象和封装,面向对象的核心概念是纵向结构的,其目的是获得更加清晰高效的逻辑单元划分。 而 AOP则是针对业务处理过程中的切面进行提取。例如,企业开发中经常会面临的种种非功能性需求(操作日志、权限控制、性能监测等等),用面向对象的思路,将业务操作对象的核心功能和对它的其他服务性功能代码分离,即某一个操作在各个模块中都有涉及,这个操作就可以看成“横切”存在于系统当中。在许多情况下,这些操作都是与业务逻辑相关性不强或者不属于逻辑操作的必须部分,而面向对象的方法很难对这种情况做出处理。AOP则将这些操作与业务逻辑分离,使程序员在编写程序时可以专注于业务逻辑的处理,而利用 AOP将贯穿于各个模块间的横切关注点自动耦合进来。
- 思想结构不同:OOP是纵向的(以继承为主线,所以是纵向的)。AOP是横向的。
- 注重方面不同:OOP是注重业务逻辑单元的划分,AOP偏重业务处理过程中的某个步骤或阶段。
- OOP则注重封装,以对象为中心,强调整体性,代码整体变得更规范;但是OOP的耦合性还是很高,一旦代码需要修改,会对很多类造成影响,代码冗余问题严重,而且核心业务中总夹杂着一些它并不关心的通用业务,比如日志记录、性能统计、安全控制、事务处理、异常处理等;AOP正好解决了OOP周边通用业务的解耦问题。
- AOP是OOP的延续和补充。AOP 与 OOP 并不是相互竞争的两种技术, 人们不是为了代替OOP 而提出 AOP, 事实上 AOP 与 OOP 两者互相之间是一个很好的补充和完善。OOP面向对象编程关注的是将需求功能“ 垂直” 划分为不同的并且相对独立, 封装良好的类, 并让它们有着属于自己的行为。至于对象间的关系则依靠继承和多态等来定义,即OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
POP、OOP、AOP三种思想是相互补充的。在一个系统的开发过程中,这三种编程思想是不可或缺的。
三、实现AOP
我们在上面讲解了有关AOP的一些理论知识,那么如何在代码里面实现呢?
实现AOP有两种方式:
- 静态代理实现。所谓静态代理,就是我们自己来写代理对象。
- 动态代理实现。所谓动态代理,就是在程序运行时,去生成一个代理对象。
1、静态代理
实现静态代理需要使用到两种设计模式:装饰器模式和代理模式。
装饰器模式:允许向一个现有的对象添加新的功能,同时又不改变这个现有对象的结构。属于结构型设计模式,它是作为现有类的一种包装。首先会创建一个装饰类,用来包装原有的类,并在保持类的完整性的前提下,提供额外的功能。看下面的例子。
我们首先创建一个User类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace StaticDemo.Model { public class User { public string Name { get; set; } public string Password { get; set; } } }
接着我们创建一个账号服务的接口,里面有一个方法,用来注册一个用户:
using StaticDemo.Model; namespace StaticDemo.Services { /// <summary> /// 接口 /// </summary> public interface IAccountService { /// <summary> /// 注册用户 /// </summary> /// <param name="user"></param> void Reg(User user); } }
然后创建一个类来实现上面的接口:
using StaticDemo.Model; using System; namespace StaticDemo.Services { /// <summary> /// 实现IAccountService接口 /// </summary> public class AccountService : IAccountService { public void Reg(User user) { // 业务代码 之前 或者之后执行一些其它的逻辑 Console.WriteLine($"{user.Name}注册成功"); } } }
我们在创建一个装饰器类:
using StaticDemo.Model; using StaticDemo.Services; using System; namespace StaticDemo { /// <summary> /// 装饰器类 /// </summary> public class AccountDecorator : IAccountService { private readonly IAccountService _accountService; public AccountDecorator(IAccountService accountService) { _accountService = accountService; } public void Reg(User user) { Before(); // 这里调用注册的方法,原有类里面的逻辑不会改变 // 在逻辑前面和后面分别添加其他逻辑 _accountService.Reg(user); After(); } private void Before() { Console.WriteLine("注册之前的逻辑"); } private void After() { Console.WriteLine("注册之后的逻辑"); } } }
我们会发现装饰器类同样实现了IAccountService接口。最后我们在Main方法里面调用:
using StaticDemo.Model; using StaticDemo.Services; using System; namespace StaticDemo { class Program { static void Main(string[] args) { // 实例化对象 IAccountService accountService = new AccountService(); // 实例化装饰器类,并用上面的实例给构造方法传值 var account = new AccountDecorator(accountService); var user = new User { Name = "Rick", Password = "12345678" }; // 调用装饰器类的注册方法,相当于调用实例化对象的注册方法 account.Reg(user); Console.ReadKey(); } } }
运行结果:
下面我们在来看看如何使用代理模式实现。
代理模式:即一个类代表另一个类的功能。我们会创建一个代理类,这个代理类和装饰器类基本一样。看一下代码:
using StaticDemo.Model; using StaticDemo.Services; using System; namespace StaticDemo { /// <summary> /// 代理类 /// </summary> public class ProxyAccount : IAccountService { private readonly IAccountService _accountService; /// <summary> /// 构造函数没有参数 /// 直接在里面创建了AccountService类 /// </summary> public ProxyAccount() { _accountService = new AccountService(); } public void Reg(User user) { before(); _accountService.Reg(user); after(); } private void before() { Console.WriteLine("代理:注册之前的逻辑"); } private void after() { Console.WriteLine("代理:注册之后的逻辑"); } } }
Main方法里面调用:
using StaticDemo.Model; using StaticDemo.Services; using System; namespace StaticDemo { class Program { static void Main(string[] args) { #region 装饰器模式 //// 实例化对象 //IAccountService accountService = new AccountService(); //// 实例化装饰器类,并用上面的实例给构造方法传值 //var account = new AccountDecorator(accountService); //var user = new User { Name = "Rick", Password = "12345678" }; //// 调用装饰器类的注册方法,相当于调用实例化对象的注册方法 //account.Reg(user); #endregion #region 代理模式 var account = new ProxyAccount(); var user = new User { Name = "Tom", Password = "12345678" }; account.Reg(user); #endregion Console.ReadKey(); } } }
运行结果:
可能有的人会发现,装饰器类和代理类很相像,功能也一模一样,仅仅是构造函数不同。那么装饰器模式和代理模式有区别吗?有些东西,形式上看起来区别很小,但实际上他们区别很大。它们在形式上确实一样,不管是装饰器类还是代理类,它们都要实现相同的接口,但是它们在运用的时候还是有区别的。
装饰器模式关注于在一个对象上动态添加方法,而代理模式关注于控制对象的访问。简单来说,使用代理模式,我们的代理类可以隐藏一个类的具体信息。var account = new ProxyAccount();仅看这段代码不看源码,不知道里面代理的是谁。
当使用代理模式的时候,我们常常是在代理类中去创建一个对象的实例: _accountService = new AccountService()。而当我们使用装饰器模式的时候,我们通常是将原始对象作为一个参数传递给装饰器的构造函数。简单来说,在使用装饰器模式的时候,我们可以明确地知道装饰的是谁,而且更重要的是,代理类里面是写死的,在编译的时候就确定了关系。而装饰器是在运行时来确定的。
2、动态代理
动态代理实现也有两种方式;
- 通过代码织入的方式。例如PostSharp第三方插件。我们知道.NET程序最终会编译成IL中间语言,在编译程序的时候,PostSharp会动态的去修改IL,在IL里面添加代码,这就是代码织入的方式。
- 通过反射的方式实现。通过反射实现的方法非常多,也有很多实现了AOP的框架,例如Unity、MVC过滤器、Autofac等。
我们先来看看如何使用PostSharp实现动态代理。PostSharp是一款收费的第三方插件。
首先新创建一个控制台应用程序,然后创建一个订单业务类:
using System; namespace PostSharpDemo { /// <summary> /// 订单业务类 /// </summary> public class OrderBusiness { public void DoWork() { Console.WriteLine("执行订单业务"); } } }
接着在Main方法里面调用:
using System; namespace PostSharpDemo { class Program { static void Main(string[] args) { OrderBusiness order = new OrderBusiness(); // 调用方法 order.DoWork(); Console.ReadKey(); } } }
运行结果:
这时又提出了一个新的需求,要去添加一个日志功能,记录业务的执行情况,按照以前的办法,需要定义一个日志帮助类:
using System; using System.IO; namespace PostSharpDemo { public class LgoHelper { public static void RecoreLog(string message) { string strPath = AppDomain.CurrentDomain.BaseDirectory+"\\log.txt"; using(StreamWriter sw=new StreamWriter(strPath,true)) { sw.WriteLine(message); sw.Close(); } } } }
如果不使用AOP,我们就需要在记录日志的地方实例化Loghelper对象,然后记录日志:
using System; namespace PostSharpDemo { /// <summary> /// 订单业务类 /// </summary> public class OrderBusiness { public void DoWork() { // 记录日志 LgoHelper.RecoreLog("执行业务前"); Console.WriteLine("执行订单业务"); LgoHelper.RecoreLog("执行业务后"); } } }
我们再次运行程序,查看结果:
我们看看日志内容:
这样修改可以实现记录日志的功能。但是上面的方法会修改原先已有的代码,这就违反了开闭原则。而且添加日志也不是业务需求的变动,不应该去修改业务代码。下面使用AOP来实现。首先安装PostSharp,直接在NuGet里面搜索,然后安装即可:
然后定义一个LogAttribute类,继承自OnMethodBoundaryAspect。这个Aspect提供了进入、退出函数等连接点方法。另外,Aspect上必须设置“[Serializable] ”,这与PostSharp内部对Aspect的生命周期管理有关:
using PostSharp.Aspects; using System; namespace PostSharpDemo { [Serializable] [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class LogAttribute: OnMethodBoundaryAspect { public string ActionName { get; set; } public override void OnEntry(MethodExecutionArgs eventArgs) { LgoHelper.RecoreLog(ActionName + "开始执行业务前"); } public override void OnExit(MethodExecutionArgs eventArgs) { LgoHelper.RecoreLog(ActionName + "业务执行完成后"); } } }
然后Log特性应用到DoWork函数上面:
using System; namespace PostSharpDemo { /// <summary> /// 订单业务类 /// </summary> public class OrderBusiness { [Log(ActionName ="DoWork")] public void DoWork() { // 记录日志 // LgoHelper.RecoreLog("执行业务前"); Console.WriteLine("执行订单业务"); // LgoHelper.RecoreLog("执行业务后"); } } }
这样修改以后,只需要在方法上面添加一个特性,以前记录日志的代码就可以注释掉了,这样就不会再修改业务逻辑代码了,运行程序:
在看看日志:
这样就实现了AOP功能。
我们在看看使用Remoting来实现动态代理。
首先还是创建一个User实体类:
namespace DynamicProxy.Model { public class User { public string Name { get; set; } public string Password { get; set; } } }
然后创建一个接口,里面有一个注册方法:
using DynamicProxy.Model; namespace DynamicProxy.Services { public interface IAccountService { void Reg(User user); } }
然后创建接口的实现类:
using DynamicProxy.Model; using System; namespace DynamicProxy.Services { public class AccountService : MarshalByRefObject, IAccountService { public void Reg(User user) { Console.WriteLine($"{user.Name}注册成功"); } } }
然后创建一个泛型的动态代理类:
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Messaging; using System.Runtime.Remoting.Proxies; namespace DynamicProxy { public class DynamicProxy<T> : RealProxy { private readonly T _target; // 执行之前 public Action BeforeAction { get; set; } // 执行之后 public Action AfterAction { get; set; } // 被代理泛型类 public DynamicProxy(T target) : base(typeof(T)) { _target = target; } // 代理类调用方法 public override IMessage Invoke(IMessage msg) { var reqMsg = msg as IMethodCallMessage; var target = _target as MarshalByRefObject; BeforeAction(); // 这里才真正去执行代理类里面的方法 // target表示被代理的对象,reqMsg表示要执行的方法 var result = RemotingServices.ExecuteMessage(target, reqMsg); AfterAction(); return result; } } }
我们看到,这个泛型动态代理类里面有两个泛型委托:BeforeAction、AfterAction。通过构造函数把代理泛型类传递进去。最后调用Invoke方法执行代理类的方法。
最后我们还要创建一个代理工厂类,用来创建代理对象,通过调用动态代理来创建动态代理对象:
using System; namespace DynamicProxy { /// <summary> /// 动态代理工厂类 /// </summary> public static class ProxyFactory { public static T Create<T>(Action before, Action after) { // 实例化被代理泛型对象 T instance = Activator.CreateInstance<T>(); // 实例化动态代理,创建动态代理对象 var proxy = new DynamicProxy<T>(instance) { BeforeAction = before, AfterAction = after }; // 返回透明代理对象 return (T)proxy.GetTransparentProxy(); } } }
我们最后在Main方法里面调用:
using DynamicProxy.Model; using DynamicProxy.Services; using System; namespace DynamicProxy { class Program { static void Main(string[] args) { // 调用动态代理工厂类创建动态代理对象,传递AccountService,并且传递两个委托 var acount = ProxyFactory.Create<AccountService>(before:() => { Console.WriteLine("注册之前"); }, after:() => { Console.WriteLine("注册之后"); }); User user = new User() { Name="张三", Password="123456" }; // 调用注册方法 acount.Reg(user); Console.ReadKey(); } } }
程序运行结果:
这样就利用Remoting实现了动态代理。