WinForm/C# AOP 编程思想

wangbaochen · 2021年07月13日 · 176 次阅读

AOP 编程思想

一、什么是 AOP

AOP:Aspect Oriented Programming 的缩写,意为面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 思想的延续。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

二、编程思想的发展路线

1、POP

POP:Procedure Oriented Programming 的缩写,即面向过程编程,是一种以过程为中心的编程思想。

面向过程是分析出解决问题的步骤,然后用函数或者方法,把这些步骤一步一步的实现,使用的时候在一个一个的一次调用函数或者方法,这就是面向过程编程。最开始的时候都是面向过程编程。面向过程是最为实际的一种思考方式。就算是面向对象编程,里面也是包含有面向过程的编程思想,因为面向过程是一种基础的编程思考方式,它从实际出发来考虑如何实现需求。

POP 的不足:面向过程编程,只能处理一些简单的问题,无法处理一些复杂的问题。如果问题很复杂,全部以流程来思考的话,会发现流程很混乱,甚至流程都不能进行下去。

2、OOP

OOP:Object Oriented Programming 的缩写,即面向对象编程。

早期的计算机编程都是面向过程的,因为早期的编程都比较简单。但是随着时间的发展,需求处理的问题越来越多,问题就会越来越复杂,这时就不能简单的使用面向过程的编程了,就出现了面向对象编程。在计算机里面,把所有的东西都想象成一种事物。现实世界中的对象都有一些属性和行为,也就对应计算机中的属性和方法。

面向对象编程就是把构成问题的事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。

OOP 的不足:产生新的需求会导致程序代码不断的进行修改,容易造成程序的不稳定。

如果非常了解 OOP 的话,那么我们应该知道,从对象的组织角度来讲,分类方法都是以继承关系为主线的,我们称为纵向。如果只使用 OOP 思想的话,会带来两个问题:

  1. 共性问题。
  2. 扩展问题,需要对先有类进行扩展时就比较困难了。

OOP 与 POP 的区别:

在对比面向过程的时候,面向对象的方法是把事物最小化为对象,包括属性和方法。当程序的规模比较小的时候,面向过程编程还是有一些优势的,因为这时候程序的流程是比较容易梳理清楚的。以早上去上班为例,过程就是起床、穿衣、刷牙洗脸、去公司。每一步都是按照顺序完成的,我们只需要按照步骤去一步一步的实现里面的方法就行了,最后在依次调用实现的方法即可,这就是面向过程开发。

如果使用面向对象编程,我们就需要抽象出来一个员工类,该员工具有起床、穿衣、刷牙洗脸、去公司的四个方法。但是,最终要实现早上去上班的这个需求的话,还是要按照顺序依次来调用四个方法。最开始的时候,我们是按照面向过程的思想来思考该需求,然后在按照面向对象的思想来抽象出几个方法,最终要实现这个需求,还是要按照面向过程的顺序来实现。

面向对象和面向过程的区别仅仅是在思考问题方式上面的不同。最终你会发现,在你实现这个需求的时候,即使使用了面向对象的思想抽象出来了员工类,但是最后还是要使用面向过程来实现这个需求。

3、AOP

AOP:Aspect Oriented Programming 的缩写,即面向切面编程。是对 OOP 的一种补充,在不修改原始类的情况下,给程序动态添加统一功能的一种技术。

OOP 关注的是将需求功能划分为不同的并且相对独立、封装良好的类,依靠继承和多态来定义彼此的关系。AOP 能够将通用需求功能从不相关的类中分离出来,很多类共享一个行为,一旦发生变化,不需要去修改很多类,只需要去修改这一个类即可。

AOP 中的切面是指什么呢?切面指的是横切关注点。看下面一张图:

OOP 是为了将状态和行为进行模块化。上图是一个商场系统,我们使用 OOP 将该系统纵向分为订单管理、商品管理、库存管理模块。在该系统里面,我们要进行授权验证。像订单、商品、库存都是业务逻辑功能,但是这三个模块都需要一些共有的功能,比如说授权验证、日志记录等。我们不可能在每个模块里面都去写授权验证,而且授权验证也不属于具体的业务,它其实属于功能性模块,并且会横跨多个业务模块。可以看到这里是横向的,这就是所谓的切面。通俗的将,AOP 就是将公用的功能给提取出来,如果以后这些公用的功能发生了变化,我们只需要修改这些公用功能的代码即可,其它的地方就不需要去更改了。所谓的切面,就是只关注通用功能,而不关注业务逻辑,而且不修改原有的类。

AOP 优势:

  1. 将通用功能从业务逻辑中抽离出来,提高代码复用性,有利于后期的维护和扩展。
  2. 软件设计时,抽出通用功能(切面),有利于软件设计的模块化,降低软件架构的复杂度。

AOP 的劣势:

  1. AOP 的对 OOP 思想的一种补充,它无法单独存在。如果说单独使用 AOP 去设计一套系统是不可能的。在设计系统的时候,如果系统比较简单,那么可以只使用 POP 或者 OOP 来设计。如果系统很复杂,就需要使用 AOP 思想。首先要使用 POP 来梳理整个业务流程,然后根据 POP 的流程,去整理类和模块,最后在使用 AOP 来抽取通用功能。

AOP 和 OOP 的区别:

  1. 面向目标不同:OOP 是面向名词领域 (抽象出来一个事物,比如学生、员工,这些都是名词)。AOP 是面向动词领域 (比如鉴权、记录日志,这些都是动作或行为)。
  2. 思想结构不同:OOP 是纵向的 (以继承为主线,所以是纵向的)。AOP 是横向的。
  3. 注重方面不同:OOP 是注重业务逻辑单元的划分,AOP 偏重业务处理过程中的某个步骤或阶段。

POP、OOP、AOP 三种思想是相互补充的。在一个系统的开发过程中,这三种编程思想是不可或缺的。

三、实现 AOP

实现 AOP 有两种方式:

  1. 静态代理实现。所谓静态代理,就是我们自己来写代理对象。
  2. 动态代理实现。所谓动态代理,就是在程序运行时,去生成一个代理对象。

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、动态代理

动态代理实现也有两种方式;

  1. 通过代码织入的方式。例如 PostSharp 第三方插件。我们知道.NET 程序最终会编译成 IL 中间语言,在编译程序的时候,PostSharp 会动态的去修改 IL,在 IL 里面添加代码,这就是代码织入的方式。
  2. 通过反射的方式实现。通过反射实现的方法非常多,也有很多实现了 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 实现了动态代理。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册