Aoite 系列(04) - 强劲的 CommandModel 开发模式(上篇)
Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案。Aoite.CommandModel 是一种开发模式,我把它成为“命令模型”,这是一种非常有意思的开发模式。
赶紧加入 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 结尾,这并非强制性的规则,并且两种方式都支持):
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 中 BeginRequest 和 EndRequest。
事件可以做的事情非常多,它让 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}\t{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.Context 或 Aoite.Redis.RedisManager.Context,你应该在线程结束中调用 GA.ResetContexts。比如说,在 HTTP Application 中,每一个请求结束,都应当调用 GA.ResetContexts(如果你使用了 Aoite.Web 框架,则不需要手工调用)。
4.1 命令和执行器的映射
一个命令是如何与执行器进行映射的,其映射的优先级和规则如下:
- 命令包含了
BindingExecutorAttribute特性。此特性可以指定执行器的数据类型(也可以是一个泛型)。 - 命令的嵌套类型,并且类型名称为“Executor”。这是推荐的用法。
- 相同命名空间下,命令名称(若以 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.XControllerBase 和 System.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 开发模式(上篇)的更多相关文章
- webpack4 系列教程(十五):开发模式与webpack-dev-server
作者按:因为教程所示图片使用的是 github 仓库图片,网速过慢的朋友请移步<webpack4 系列教程(十五):开发模式与 webpack-dev-server>原文地址.更欢迎来我的 ...
- webpack4 系列教程(十六):开发模式和生产模式·实战
好文章 https://www.jianshu.com/p/f2d30d02b719
- Aoite 系列 目录
介绍 本项目从2009年孵化(V->Sofire->Aoite),至今已度过5个年头.一直在优化,一直在重构,一直在商用.有十分完整的单元测试用例.可以放心使用. Aoite on 博客园 ...
- 《C#微信开发系列(1)-启用开发者模式》
1.0启用开发者模式 ①填写服务器配置 启用开发模式需要先成为开发者,而且编辑模式和开发模式只能选择一个(进入微信公众平台=>开发=>基本配置)就可以看到以下的界面: 点击修改配置,会出现 ...
- Aoite 系列(02) - 超动感的 Ioc 容器
Aoite 系列(02) - 超动感的 Ioc 容器 Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案.Aoite.Ioc 是一套解决依赖的最佳实践. 说 ...
- 《C#微信开发系列(Top)-微信开发完整学习路线》
年前就答应要将微信开发的学习路线整理给到大家,但是因为年后回来这段时间学校还有公司那边有很多事情需要兼顾,所以没能及时更新文章.今天特地花时间整理了下,话不多说,上图,希望对大家的学习有所帮助哈. 如 ...
- React jQuery公用组件开发模式及实现
目前较为流行的react确实有很多优点,例如虚拟dom,单向数据流状态机的思想.还有可复用组件化的思想等等.加上搭配jsx语法和es6,适应之后开发确实快捷很多,值得大家去一试.其实组件化的思想一直在 ...
- Aoite 系列(03) - 一起来 Redis 吧!
Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案.Aoite.Data 适用于市面上大多数的数据库提供程序,通过统一封装,可以在日常开发中简单便捷的操作数 ...
- Aoite 系列(01) - 比 Dapper 更好用的 ORM
Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案.Aoite.Data 适用于市面上大多数的数据库提供程序,通过统一封装,可以在日常开发中简单便捷的操作数 ...
随机推荐
- Django + Apache + 树莓派 搭建内网微信公众号服务器
其实早在微信开放公众号开发平台时就想弄一个自己的公众号服务器,奈何对web服务器搭建和开发一窍不通,只是注册了一下开发者帐号,并没有采取行动,万恶的拖延症. 前一年,开始接触python,打开了神奇世 ...
- 122. Best Time to Buy and Sell Stock(二) leetcode解题笔记
122. Best Time to Buy and Sell Stock II Say you have an array for which the ith element is the price ...
- Python中实现异步并发查询数据库
这周又填了一个以前挖下的坑. 这个博客系统使用Psycopy库实现与PostgreSQL数据库的通信.前期,只是泛泛地了解了一下SQL语言,然后就胡乱拼凑出这么一个简易博客系统. 10月份找到工作以后 ...
- Oracle直方图的详细解析
yuanwen:http://blog.csdn.net/javacoffe/article/details/5578206 Oracle直方图解析 一. 何谓直方图: 直方图是一种统计学上的工 ...
- tomcat 性能优化
tomcat 性能优化tomcat默认参数是为开发环境制定,而非适合生产环境,尤其是内存和线程的配置,默认都很低,容易成为性能瓶颈. tomcat内存优化linux修改TOMCAT_HOME/bin/ ...
- 【转】25个必须记住的SSH命令
1.复制SSH密钥到目标主机,开启无密码SSH登录 ssh-copy-id user@host 如果还没有密钥,请使用ssh-keygen命令生成. 2.从某主机的80端口开启到本地主机2001端口的 ...
- VS2013无调试信息
Debug模式,运行时完全正常,但是一调试就出现对话框,显示出错信息: "无法找到"XXX.exe"的调试信息,或者调试信息不匹配.未使用调试信息生成二进制文件.&quo ...
- SQL Server 2012安装后找不到服务器名称的解决办法!!!
网上说使用localhost即可,确实没错,但是有的仍旧会报出无法找到错误,我在无法通过的时候又重新安装了SQLServer,这次选中全部默认安装,之前使用的是选择安装,然后发现多了几个配置,其中有一 ...
- c# DataTable 转为 List 类型
代码: public class ModelConvertHelper<T> where T : new() { public static IList<T> ConvertT ...
- Java开发Webservice的组件
参考:http://bbs.csdn.net/topics/390900831 转自:http://blog.csdn.net/dragoo1/article/details/50759222 htt ...