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 可以实现非常细粒度的单元测试。
  • 基于服务容器的依赖注入。可以针对每个命令的执行前和执行后进行拦截处理。
  • 支持命令级的缓存。例如:获取积分排行前十的用户列表。
  • 支持命令集级的事务。

1.1 命令(Command)###

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

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

  1. public class FindUserById : ICommand<User>
  2. {
  3. //- 输入参数
  4. public long Id { get; set; }
  5. //- 输出参数
  6. public User ResultValue { get; set; }
  7. }

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

1.2 执行器(Executor)###

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

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

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

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

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

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

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

1.3 上下文(Context)

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

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

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

1.4 事件(Event)

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

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

2. 快速入门

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

2.1 普通命令

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

  1. public class User
  2. {
  3. public string Username { get; set; }
  4. public string Password { get; set; }
  5. }
  6. public class FindUserById : ICommand<User>
  7. {
  8. public long Id { get; set; }
  9. public User ResultValue { get; set; }
  10. class Executor : IExecutor<FindUserById>
  11. {
  12. public void Execute(IContext context, FindUserById command)
  13. {
  14. if(command.Id == 1)
  15. {
  16. command.ResultValue = new User() { Username = "admin", Password = "123456" };
  17. }
  18. }
  19. }
  20. }

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

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

以上代码最终输出

  1. admin 123456

2.2 泛型命令

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

  1. public interface IPerson
  2. {
  3. string Name { get; set; }
  4. }
  5. public class Student : IPerson
  6. {
  7. public string Name { get; set; }
  8. }
  9. public class Teacher : IPerson
  10. {
  11. public string Name { get; set; }
  12. }

创建命令:

  1. class PersonModify<T> : ICommand where T : IPerson
  2. {
  3. public T Person { get; set; }
  4. class Executor : IExecutor<PersonModify<T>>
  5. {
  6. public void Execute(IContext context, PersonModify<T> command)
  7. {
  8. if(command.Person is Teacher)
  9. {
  10. command.Person.Name = command.Person.Name + "老师";
  11. }
  12. else if(command.Person is Student)
  13. {
  14. command.Person.Name = command.Person.Name + "学生";
  15. }
  16. }
  17. }
  18. }

测试代码:

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

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

3 缓存

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

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

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

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

  1. [Cache("User")]
  2. public class GetDate : ICommand<DateTime>, ICommandCache
  3. {
  4. //- 根据传入的用户编号,获取一个时间
  5. public long UserId { get; set; }
  6. public DateTime ResultValue { get; set; }
  7. class Executor : IExecutor<GetDate>
  8. {
  9. public void Execute(IContext context, GetDate command)
  10. {
  11. command.ResultValue = DateTime.Now.AddDays(command.UserId); //- 当前时间加上 UserId 值的天数
  12. }
  13. }
  14. //- 缓存策略,弹性 3 秒内缓存
  15. ICommandCacheStrategy ICommandCache.CreateStrategy(IContext context)
  16. {
  17. return new CommandCacheStrategy(UserId.ToString(), TimeSpan.FromSeconds(3), this, context);
  18. }
  19. //- 返回需缓存的内容
  20. object ICommandCache.GetCacheValue()
  21. {
  22. return this.ResultValue;
  23. }
  24. //- 设置缓存值,若值不合法必须返回 false,否则执行器永不会执行
  25. bool ICommandCache.SetCacheValue(object value)
  26. {
  27. if(value is DateTime)
  28. {
  29. this.ResultValue = (DateTime)value;
  30. return true;
  31. }
  32. return false;
  33. }

3.2 缓存测试代码

  1. var container = new IocContainer();
  2. var bus = new CommandBus(container);
  3. for(int i = 0; i < 6; i++)
  4. {
  5. //- 0、1、2
  6. Console.WriteLine("{0} -> {1}", i % 3, bus.Execute(new GetDate() { UserId = i % 3 }).ResultValue);
  7. }
  8. Console.WriteLine("开始休眠 3 秒...");
  9. System.Threading.Thread.Sleep(TimeSpan.FromSeconds(3));
  10. Console.WriteLine("结束休眠 3 秒...");
  11. for(int i = 0; i < 6; i++)
  12. {
  13. //- 0、1、2
  14. Console.WriteLine("{0} -> {1}", i % 3, bus.Execute(new GetDate() { UserId = i % 3 }).ResultValue);
  15. }
  16. Console.WriteLine("测试 5 次,每次间隔 2 秒...");
  17. for(int i = 0; i < 5; i++)
  18. {
  19. Console.WriteLine("{0} -> {1}", 99, bus.Execute(new GetDate() { UserId = 99 }).ResultValue);
  20. Console.WriteLine("开始休眠 2 秒,避免缓冲过期...");
  21. System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
  22. }

最终输出结果:

  1. 0 -> 2015/2/6 16:40:46
  2. 1 -> 2015/2/7 16:40:46
  3. 2 -> 2015/2/8 16:40:46
  4. 0 -> 2015/2/6 16:40:46
  5. 1 -> 2015/2/7 16:40:46
  6. 2 -> 2015/2/8 16:40:46
  7. 开始休眠 3 秒...
  8. 结束休眠 3 秒...
  9. 0 -> 2015/2/6 16:40:49
  10. 1 -> 2015/2/7 16:40:49
  11. 2 -> 2015/2/8 16:40:49
  12. 0 -> 2015/2/6 16:40:49
  13. 1 -> 2015/2/7 16:40:49
  14. 2 -> 2015/2/8 16:40:49
  15. 测试 5 次,每次间隔 2 秒...
  16. 99 -> 2015/5/16 16:40:49
  17. 开始休眠 2 秒,避免缓冲过期...
  18. 99 -> 2015/5/16 16:40:49
  19. 开始休眠 2 秒,避免缓冲过期...
  20. 99 -> 2015/5/16 16:40:49
  21. 开始休眠 2 秒,避免缓冲过期...
  22. 99 -> 2015/5/16 16:40:49
  23. 开始休眠 2 秒,避免缓冲过期...
  24. 99 -> 2015/5/16 16:40:49
  25. 开始休眠 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 结尾。

  1. class Simple1Command : ICommand {}
  2. class Simple1Executor : IExecutor<Simple1Command> {}

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

  1. class Simple2 : ICommand {}
  2. class Simple2Executor : IExecutor<Simple2> {}

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

  1. class Simple3<T1, T2> : ICommand
  2. {
  3. //....
  4. class Executor : IExecutor<Simple3<T1, T2>>
  5. {
  6. //....
  7. }
  8. }

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

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

4.2 用户工厂(UserFactory)

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

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

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

然后添加测试代码:

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

以上代码的输出内容是

  1. admin
  2. user

4.3 事件(Event)

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

  1. var container = new IocContainer();
  2. object user = new SimpleUser { Id = 1 };
  3. container.AddService<IUserFactory>(new UserFactory(c => user));
  4. container.GetService<IEventStore>().Register<GetUsername>(new MockEvent<GetUsername>((context, command) =>
  5. {
  6. if(context.User.Id == 1) context.User.Id = 2;
  7. else if(context.User.Id == 2) context.User.Id = 1;
  8. return true;
  9. }, (context, command, exception) =>
  10. {
  11. Console.WriteLine("执行后结果 {0}", command.ResultValue);
  12. }));
  13. var bus = new CommandBus(container);
  14. Console.WriteLine(bus.Execute(new GetUsername()).ResultValue);
  15. user = new SimpleUser { Id = 2 };
  16. Console.WriteLine(bus.Execute(new GetUsername()).ResultValue);

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

  1. 执行后结果 user
  2. user
  3. 执行后结果 admin
  4. 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 一下 :)

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

Aoite 系列(04) - 强劲的 CommandModel 开发模式(上篇)的更多相关文章

  1. webpack4 系列教程(十五):开发模式与webpack-dev-server

    作者按:因为教程所示图片使用的是 github 仓库图片,网速过慢的朋友请移步<webpack4 系列教程(十五):开发模式与 webpack-dev-server>原文地址.更欢迎来我的 ...

  2. webpack4 系列教程(十六):开发模式和生产模式·实战

    好文章 https://www.jianshu.com/p/f2d30d02b719

  3. Aoite 系列 目录

    介绍 本项目从2009年孵化(V->Sofire->Aoite),至今已度过5个年头.一直在优化,一直在重构,一直在商用.有十分完整的单元测试用例.可以放心使用. Aoite on 博客园 ...

  4. 《C#微信开发系列(1)-启用开发者模式》

    1.0启用开发者模式 ①填写服务器配置 启用开发模式需要先成为开发者,而且编辑模式和开发模式只能选择一个(进入微信公众平台=>开发=>基本配置)就可以看到以下的界面: 点击修改配置,会出现 ...

  5. Aoite 系列(02) - 超动感的 Ioc 容器

    Aoite 系列(02) - 超动感的 Ioc 容器 Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案.Aoite.Ioc 是一套解决依赖的最佳实践. 说 ...

  6. 《C#微信开发系列(Top)-微信开发完整学习路线》

    年前就答应要将微信开发的学习路线整理给到大家,但是因为年后回来这段时间学校还有公司那边有很多事情需要兼顾,所以没能及时更新文章.今天特地花时间整理了下,话不多说,上图,希望对大家的学习有所帮助哈. 如 ...

  7. React jQuery公用组件开发模式及实现

    目前较为流行的react确实有很多优点,例如虚拟dom,单向数据流状态机的思想.还有可复用组件化的思想等等.加上搭配jsx语法和es6,适应之后开发确实快捷很多,值得大家去一试.其实组件化的思想一直在 ...

  8. Aoite 系列(03) - 一起来 Redis 吧!

    Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案.Aoite.Data 适用于市面上大多数的数据库提供程序,通过统一封装,可以在日常开发中简单便捷的操作数 ...

  9. Aoite 系列(01) - 比 Dapper 更好用的 ORM

    Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案.Aoite.Data 适用于市面上大多数的数据库提供程序,通过统一封装,可以在日常开发中简单便捷的操作数 ...

随机推荐

  1. Linux启动与登陆环境

    linux启动流程 参考:http://www.ruanyifeng.com/blog/2013/08/linux_boot_process.html 加载内核,首先读入/boot 目录下的内核文件. ...

  2. Access to the path '' is denied 解决

    环境:iis6 使用silverlight做的上传控件上传文件到某共享目录. 已将在目录的共享安全和安全中加了 共享用户的 权限. 但通过浏览器访问共享目录文件报错:Access to the pat ...

  3. the fifth class

      1.实际比背景长,怎么做到的? 2个父级一个做头背景一个做尾背景 2.2层,每次自带背景上下是覆盖关系,如何做到 2层?,子浮动 3.标签 4.border可覆盖:margin-bottom 为负 ...

  4. pip 国内源 gem 国内源

    清华: https://pypi.tuna.tsinghua.edu.cn/simple 豆瓣: http://pypi.douban.com/simple/ 阿里: http://mirrors.a ...

  5. 攻破JAVA NIO技术壁垒

    转载自攻破JAVA NIO技术壁垒 概述 NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector.传统IO基于字节流和字符流进行操作,而NIO基于Channel和 ...

  6. Everything搜索结果显示0 Object

    比较过windows本身的文档搜索功能,Everything的本地文档搜索能力简直令人咋舌,更逆天的是软件本身体积很小. 问题:打开everything时,文件列表消失,软件下方信息为0 object ...

  7. linux split 命令 将一个大的文件拆分成若干小文件

    . 以行数拆分 -l 参数: 原始文件 拆分后文件名前缀 例:以50行对文件进行拆分 big.txt small_ 拆分后会生成 small_aa small_ab small_ac ... . 以大 ...

  8. MATLAB 秒表函数 tic toc 计算程序运行时间

    若需要测试出程序运行所需时间,或对不同的运行方式所需时间进行对比,则可利用秒表函数tic和toc.Tic函数启动定时器,第一个紧跟它的toc函数终止定时器并报告此时定时器的流逝时间.其语法如下:  t ...

  9. spring4+hibernate4+maven环境搭建

    本文主要介绍利用maven搭建spring4+hibernate4开发环境. 首先我们创建一个maven项目,具体步骤就不详细介绍了,看看我们pom.xml文件 <project xmlns=& ...

  10. 【解题报告】BZOJ2550: [Ctsc2004]公式编辑器

    题意:给定一个可视化计算器的操作序列,包括插入数字.字母.运算符.分数.矩阵以及移动光标.矩阵插入行.插入列,输出操作序列结束后的屏显(数学输出). 解法:这题既可以用来提升OI/ACM写大代码模拟题 ...