• Aoite 系列(04)


    Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案。Aoite.CommandModel 是一种开发模式,我把它成为“命令模型”,这是一种非常有意思的开发模式。

    【Aoite 系列 目录】

    赶紧加入 Aoite GitHub 的大家庭吧!!

    1. 概述

    CommandModel 的架构并不复杂,核心四大组件分别是:命令(Command)、执行器(Executor)、上下文(Context)和事件(Event)。

    CommandModel 核心是剥离所有运行期的所有依赖,注入执行。它可以运用至传统的三层架构,也可以运用到 DDD(CQRS)架构。

    不是只能应用到三层架构,只是以最传统最简单的三层架构作为比较。CommandModel 支持任何架构、模式,无论是 Web 和 Winform,亦或者 ASP.NET 和 MVC,亦或者三层架构或领域驱动。请看官不要纠结这些问题。

    传统三层架构是这样的(实体层意义上包含 数据库实体视图模型 ):

    传统三层架构

    如果将 CommandModel 加入三层架构,那么它将变成以下架构:

    CommandModel+三层架构

    注入 CommandModel 模式以后,原本的数据访问层不见了,变成了命令层,而命令层是由一个或多个命令(以及对应的一个或多个执行器)组成的集合。

    也就是说,CommandModel 其实是将数据访问层进行粒度分解

    CommandModel 的优点:

    • 简化单元测试工作量。传统三层架构(或延伸的各种结构),在单元测试模拟时,往往需要实现整个接口。通过 CommandModel 可以实现非常细粒度的单元测试。
    • 基于服务容器的依赖注入。可以针对每个命令的执行前和执行后进行拦截处理。
    • 支持命令级的缓存。例如:获取积分排行前十的用户列表。
    • 支持命令集级的事务。

    1.1 命令(Command)###

    命令是一个符合单一职责的设计原则。通过命令的名称(Name)、参数(Properties)和返回结果(Result),它应该非常直观的表达出命令的目的。比如“查询用户编号为?的用户信息”,这就是一个典型的命令。

    以下代码则是一个典型的命令(命令的名称可以以 Command 结尾,也可以不以 Command 结尾,这并非强制性的规则,并且两种方式都支持):

    public class FindUserById : ICommand<User>
    {
    	//- 输入参数
        public long Id { get; set; }
    	//- 输出参数
        public User ResultValue { get; set; }
    }
    

    具有返回值的命令实现 ICommand<TResultValue> 接口,没有返回值则直接实现 ICommand 接口

    1.2 执行器(Executor)###

    如果把命令比作一个方法签名,显然执行器对应的则是方法实现。从这个角度来看,命令(Command)和执行器(Executor)是相互依赖的。

    执行器是单例模式。一个命令若执行了无数次,执行器只会初始化一次。

    比如对应 1.1 节代码的执行器应该是这样:

    public class FindUserByIdExecutor : IExecutor<FindUserById>
    {
        public void Execute(IContext context, FindUserById command)
        {
            //- 业务代码, context 和 command 参数永不为 null 值
        }
    }
    

    每一个执行器都必须实现 IExecutor<TCommand> 接口。

    如果该命令具有返回值,方法实现内部应该有 command.ResultValue = ... 的代码。

    关于命令和执行器是如何绑定关系,请往下查看第 2 节的内容。

    1.3 上下文(Context)

    上下文在每一次命令的执行都会产生新的实例。其接口的定义如下所示:

    // 摘要: 
    //     定义一个执行命令模型的上下文。
    public interface IContext : IContainerProvider
    {
        // 摘要: 
        //     获取正在执行的命令模型。
        ICommand Command { get; }
        //
        // 摘要: 
        //     获取执行命令模型的其他参数,参数名称若为字符串则不区分大小写的序号字符串比较。
        HybridDictionary Data { get; }
        //
        // 摘要: 
        //     获取上下文中的 System.IDbEngine 实例。该实例应不为 null 值,且线程唯一。
        //     * 不应在执行器中开启事务。
        IDbEngine Engine { get; }
        //
        // 摘要: 
        //     获取执行命令模型的用户。该属性可能返回 null 值。
        [Dynamic]
        dynamic User { get; }
    
        // 摘要: 
        //     获取或设置键的值。
        //
        // 参数: 
        //   key:
        //     键。
        //
        // 返回结果: 
        //     返回一个值。
        object this[object key] { get; set; }
    }
    
    • Command:上下文中的抽象命令。
    • Data:临时数据存储的字典,生命周期仅限命令执行期间。
    • Engine:在当前命令模型上下文中的线程上下文引擎上下文。简单的说,就是在当前线程中唯一的数据库操作引擎。
    • User:在整个运行环境中,假设用户已登录授权,这里存储的便是已授权的用户信息。若想实现此功能,必须实现 IUserFactory 接口。

    上下文(Context)在整个 CommandModel 中具有非常特殊的意义。比如通过事件(Event)提前定义特殊数据存储在 Context.Data,执行器再根据不同的特殊数据处理不同的业务逻辑。亦或者,它允许了在同一线程里执行若干个命令,而不会重复、多余打开数据库连接;也可以将定义一个事务范围,控制所有的命令执行有效性。

    1.4 事件(Event)

    事件可以让每一个命令的执行得到有效控制,其的意义类似 HTTP 中 BeginRequestEndRequest

    事件可以做的事情非常多,它让 CommandModel 具备无限扩展的可能。比如常见的命令拦截执行、修改命令参数、命令缓存和日志管理等等……

    2. 快速入门

    上面说了很多概念性的东西,现在让我们实际操作一下,看看 CommandModel 是如何运用的。

    2.1 普通命令

    业务上定义了一个目的:查询用户编号为?的用户信息。完整代码如下所示:

    public class User
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
    
    public class FindUserById : ICommand<User>
    {
        public long Id { get; set; }
        public User ResultValue { get; set; }
    
        class Executor : IExecutor<FindUserById>
        {
            public void Execute(IContext context, FindUserById command)
            {
                if(command.Id == 1)
                {
                    command.ResultValue = new User() { Username = "admin", Password = "123456" };
                }
            }
        }
    }
    

    我们通过控制台来试着执行这个命令:

    var container = new IocContainer();
    var bus = new CommandBus(container);
    var result = bus.Execute(new FindUserById { Id = 1 }).ResultValue;
    Console.WriteLine("{0}	{1}", result.Username, result.Password);
    

    以上代码最终输出

    admin   123456
    

    2.2 泛型命令

    泛型命令是一个具有非常大扩展性的功能。我们来定义几个实体:

    public interface IPerson
    {
        string Name { get; set; }
    }
    
    public class Student : IPerson
    {
        public string Name { get; set; }
    }
    
    public class Teacher : IPerson
    {
        public string Name { get; set; }
    }
    

    创建命令:

    class PersonModify<T> : ICommand where T : IPerson
    {
        public T Person { get; set; }
    
        class Executor : IExecutor<PersonModify<T>>
        {
            public void Execute(IContext context, PersonModify<T> command)
            {
                if(command.Person is Teacher)
                {
                    command.Person.Name = command.Person.Name + "老师";
                }
                else if(command.Person is Student)
                {
                    command.Person.Name = command.Person.Name + "学生";
                }
            
            }
        }
    }
    

    测试代码:

    var container = new IocContainer();
    var bus = new CommandBus(container);
    var person = new Student { Name = "张三" };
    bus.Execute(new PersonModify<Student> { Person = person });
    Console.WriteLine(person.Name);
    

    最终输出结果便是:张三学生

    3 缓存

    CommandModel 默认实现了缓存的功能,支持内存缓存(容器范围内)和 Redis 缓存。由于缓存的示例代码较多,并且其十分重要,所以我单独拿出一个篇章描述缓存。

    使用缓存需要知道的三个重要内容“

    • CacheAttribute:命令必须包含此特性,表示这是具有缓存功能的命令。它还要求使用者提供一个关键参数 group,这是一个不能为空的参数。它的作用是用于区分 key。比如根据部门编号进行缓存,那么 group 则是 Dept,而 key 则是 Id
    • ICommandCache:命令必须实现此接口,此接口有三个作用:获取缓存策略、设置缓存值和获取缓存值。
    • ICommandCacheStrategy:缓存策略,在实现接口 ICommandCache 接口的 CreateStrategy(IContext context) 方法返回值。默认接口实现 CommandCacheStrategy,其特点是:支持绝对间隔过期方式、支持滑动间隔过期方式、支持基于内存的缓存、支持 Redis 的缓存。可以继承这个类,来进行更多的扩展。

    3.1 创建具有缓存效果的命令

    [Cache("User")]
    public class GetDate : ICommand<DateTime>, ICommandCache
    {
    	//- 根据传入的用户编号,获取一个时间
        public long UserId { get; set; }
    
        public DateTime ResultValue { get; set; }
    
        class Executor : IExecutor<GetDate>
        {
            public void Execute(IContext context, GetDate command)
            {
                command.ResultValue = DateTime.Now.AddDays(command.UserId); //- 当前时间加上 UserId 值的天数
            }
        }
    	//- 缓存策略,弹性 3 秒内缓存
        ICommandCacheStrategy ICommandCache.CreateStrategy(IContext context)
        {
            return new CommandCacheStrategy(UserId.ToString(), TimeSpan.FromSeconds(3), this, context);
        }
    
    	//- 返回需缓存的内容
        object ICommandCache.GetCacheValue()
        {
            return this.ResultValue;
        }
    
    	//- 设置缓存值,若值不合法必须返回 false,否则执行器永不会执行
        bool ICommandCache.SetCacheValue(object value)
        {
            if(value is DateTime)
            {
                this.ResultValue = (DateTime)value;
                return true;
            }
            return false;
        }
    

    3.2 缓存测试代码

    var container = new IocContainer();
    var bus = new CommandBus(container);
    
    for(int i = 0; i < 6; i++)
    {
        //- 0、1、2
        Console.WriteLine("{0} -> {1}", i % 3, bus.Execute(new GetDate() { UserId = i % 3 }).ResultValue);
    }
    
    Console.WriteLine("开始休眠 3 秒...");
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(3));
    Console.WriteLine("结束休眠 3 秒...");
    
    for(int i = 0; i < 6; i++)
    {
        //- 0、1、2
        Console.WriteLine("{0} -> {1}", i % 3, bus.Execute(new GetDate() { UserId = i % 3 }).ResultValue);
    }
    
    Console.WriteLine("测试 5 次,每次间隔 2 秒...");
    for(int i = 0; i < 5; i++)
    {
        Console.WriteLine("{0} -> {1}", 99, bus.Execute(new GetDate() { UserId = 99 }).ResultValue);
        Console.WriteLine("开始休眠 2 秒,避免缓冲过期...");
        System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
    }
    

    最终输出结果:

    0 -> 2015/2/6 16:40:46
    1 -> 2015/2/7 16:40:46
    2 -> 2015/2/8 16:40:46
    0 -> 2015/2/6 16:40:46
    1 -> 2015/2/7 16:40:46
    2 -> 2015/2/8 16:40:46
    开始休眠 3 秒...
    结束休眠 3 秒...
    0 -> 2015/2/6 16:40:49
    1 -> 2015/2/7 16:40:49
    2 -> 2015/2/8 16:40:49
    0 -> 2015/2/6 16:40:49
    1 -> 2015/2/7 16:40:49
    2 -> 2015/2/8 16:40:49
    测试 5 次,每次间隔 2 秒...
    99 -> 2015/5/16 16:40:49
    开始休眠 2 秒,避免缓冲过期...
    99 -> 2015/5/16 16:40:49
    开始休眠 2 秒,避免缓冲过期...
    99 -> 2015/5/16 16:40:49
    开始休眠 2 秒,避免缓冲过期...
    99 -> 2015/5/16 16:40:49
    开始休眠 2 秒,避免缓冲过期...
    99 -> 2015/5/16 16:40:49
    开始休眠 2 秒,避免缓冲过期...
    

    3.2 使用 Redis 作为缓存提供程序

    非常简单,只要往 Container(服务容器)添加 IRedisProvider,即刻支持 Redis!默认实现的 RedisProvider 取得是 Aoite.Redis.RedisManager.Context

    4. 进阶内容

    进阶内容包含了更多关于 CommandModel 的内容。

    提醒在多线程中使用了 System.Db.ContextAoite.Redis.RedisManager.Context,你应该在线程结束中调用 GA.ResetContexts。比如说,在 HTTP Application 中,每一个请求结束,都应当调用 GA.ResetContexts(如果你使用了 Aoite.Web 框架,则不需要手工调用)。

    4.1 命令和执行器的映射

    一个命令是如何与执行器进行映射的,其映射的优先级和规则如下:

    1. 命令包含了 BindingExecutorAttribute 特性。此特性可以指定执行器的数据类型(也可以是一个泛型)。
    2. 命令的嵌套类型,并且类型名称为“Executor”。这是推荐的用法
    3. 相同命名空间下,命令名称(若以 Command 为后缀则会去掉 Command)加上“Executor”。

    示例1:命令以 Command 结尾。

    class Simple1Command : ICommand {}
    class Simple1Executor : IExecutor<Simple1Command> {}
    

    示例2:命令不以 Command 结尾。

    class Simple2 : ICommand {}
    class Simple2Executor : IExecutor<Simple2> {}
    

    示例3:泛型+嵌套执行器。

    class Simple3<T1, T2> : ICommand
    {
    	//....
        class Executor : IExecutor<Simple3<T1, T2>>
        {
            //....
        }
    }
    

    示例5:特性+泛型,可以看出执行器的名称是“不符合”规则的。

    [BindingExecutor(typeof(TestSimple4<,>))]
    class Simple4<T1, T2> : ICommand { }
    class TestSimple4<T1, T2> : IExecutor<Simple4<T1, T2>>{}
    

    4.2 用户工厂(UserFactory)

    表示当前用户的方式有两种:第一种是通过命令参数(将当前用户信息作为参数);第二种则是通过执行器的 context.User 属性获取用户信息。本节要讲解的就是如何利用 context.User 获取上下文中的用户。

    假设我们定义了以下命令。

    public class GetUsername : ICommand<string>
    {
    	//-目的:获取当前用户的账号。
        public string ResultValue { get; set; }
    
        class Executor : IExecutor<GetUsername>
        {
            public void Execute(IContext context, GetUsername command)
            {
    			//- 模拟:编号为 1 返回 admin,否则返回 user
                if(context.User.Id == 1) command.ResultValue = "admin";
                else command.ResultValue = "user";
            }
        }
    }
    

    然后添加测试代码:

    var container = new IocContainer();
    
    object user = new { Id = 1 };
    container.AddService<IUserFactory>(new UserFactory(c => user));
    
    var bus = new CommandBus(container);
    Console.WriteLine(bus.Execute(new GetUsername()).ResultValue);
    user = new { Id = 2 };
    Console.WriteLine(bus.Execute(new GetUsername()).ResultValue);
    

    以上代码的输出内容是

    admin
    user
    

    4.3 事件(Event)

    事件由两个部分组成,分别是:事件仓库(EventStore)和事件(Event)。事件仓库负责全局的事件(比如你想对所有命令进行执行前捕获和执行后捕获),事件则针对固定命令类型进行捕获。如果你要全局事件,在程序运行开始就应该手工注册 IEventStore 类型,并继承 EventStore 或实现 IEventStore

    var container = new IocContainer();
    
    object user = new SimpleUser { Id = 1 };
    container.AddService<IUserFactory>(new UserFactory(c => user));
    container.GetService<IEventStore>().Register<GetUsername>(new MockEvent<GetUsername>((context, command) =>
    {
        if(context.User.Id == 1) context.User.Id = 2;
        else if(context.User.Id == 2) context.User.Id = 1;
    
        return true;
    }, (context, command, exception) =>
    {
        Console.WriteLine("执行后结果 {0}", command.ResultValue);
    }));
    
    var bus = new CommandBus(container);
    Console.WriteLine(bus.Execute(new GetUsername()).ResultValue);
    user = new SimpleUser { Id = 2 };
    Console.WriteLine(bus.Execute(new GetUsername()).ResultValue);
    

    经过事件的干扰以后,输出内容变成

    执行后结果 user
    user
    执行后结果 admin
    admin
    

    从执行器可以看出,预期输出的第一个选项应该是 admin,第二个才是 user。通过事件的拦截和处理,CommandModel 可以有效的对数据进行校验、捕获和处理等工作。

    4.4 命令的事务机制

    本节的事务更多指的是 ADO.NET 的事务。 ADO.NET 的事务实现方式有两种方式:第一种是利用 System.Data.Common.DbTransaction 的派生类,第二种则是利用 System.Transactions.TransactionScope 实现事务机制。

    结合 Db.Context 数据库上下文,CommandModel 巧妙的运用第二种方式进行事务的控制,具体代码请看下篇内容。

    5.结束

    下篇内容主要利用命令模型服务(CommandModelServiceBase)做一个完整的示例(含数据库和单元测试Aoite.CommandModel.CommandModelServiceBase 是一个默认 CommandModel 服务(业务逻辑层)的实现(若采用 Aoite.Web 框架,可以通过继承 System.Web.Mvc.XControllerBaseSystem.Web.Mvc.XWebViewPageBase)。

    命令模型服务(CommandModelServiceBase)的主要成员:

    • ICommandBus Bus { get; }:命令总线。
    • IIocContainer Container { get; set; }:服务容器。
    • dynamic User { get; }:执行命令模型的用户。
    • IDisposable AcquireLock(key, timeout = null):一个全局锁的功能,如果获取锁超时将会抛出异常。
    • long Increment(key, increment ):获取指定键的原子递增序列。
    • ITransaction BeginTransaction():开始事务模式。
    • TCommand Execute<TCommand>(command, executing, executed):执行一个命令模型。
    • Task<TCommand> ExecuteAsync<TCommand>(command, executing, executed):以异步的方式执行一个命令模型。

    关于 Aoite.CommandModel 的上篇内容,就到此结束了,如果你喜欢这个框架,不妨点个推荐吧!如果你非常喜欢这个框架,那请顺便到Aoite GitHub Star 一下 :)

    点此下载本文的所有示例代码。

  • 相关阅读:
    执行shell脚本的四种方式(转)
    linux free命令详解(一)
    linux TOP命令各参数详解【转载】
    grep命令
    vim常用命令
    IntelliJ Idea注释模板--类注释、方法注释
    [Chrome]抓请求直接生成可执行代码
    记录Markdown工具Typora
    VSCODE 配置远程开发环境
    [Boost::Polygon]多边形相减得到新的多边形序列
  • 原文地址:https://www.cnblogs.com/sofire/p/aoite_commandmodel_01.html
Copyright © 2020-2023  润新知