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 结尾,这并非强制性的规则,并且两种方式都支持):

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}\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.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 一下 :)

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

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. 哈哈,修改PHP5.4.44语法成功

    作为一个脚本语言,面向对象的继承基本上不想用到,强类型比较也想使用==直接比较.作为专业程序员不想让PHP解释器代劳过多. 修改了这个MOD版本,效果杠杠的.

  2. sqlite本地保存数据

    package com.cesecsh.ics.database; import android.content.Context; import android.database.Cursor; im ...

  3. ansible-playbook

    一.ansible-playbook介绍: playbook是由一个或多个"play"组成的列表.play的主要功能在于将事先归为一组的主机装扮成事先通过ansible中的task ...

  4. MAC npm 需要管理员权限问题

    我们在进行mac 命令行安装软件的时候,有的时候会遇见这样的问题:Please try running this command again as root/Administrator. 如图: 解决 ...

  5. pip 安装 lxml 出错

    用pip安装 lxml 老是出错,在公司安装了 wheel,从 http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml 下载了lxml的whl包,pip ins ...

  6. 一点一滴学shell-Shell expr的用法 (转)

    原文链接:http://desheng666.blog.163.com/blog/static/4908408220121643953425/ expr命令一般用于整数值,但也可用于字符串.一般格式为 ...

  7. java面试中问题

    HashMap数据结构 http://blog.csdn.net/weiyouyin/article/details/5693496 HashMap冲突 http://www.blogjava.net ...

  8. Linux Oracle删除归档日志

    今天遇到Oracle报这样的错:ORA-00257 查看了下,原来是Oracle的归档日志满了,解决方案两个 一:增加归档日志大小 二:删除无用的归档日志(我们选择这个方案) 什么也不说了Linux下 ...

  9. 图片轮播 js代码

    <script type="text/javascript"> //图片轮换 $(function () { //------------------ var sWid ...

  10. OpenResty(nginx_lua_module)做ES代理以及备份ES数据

    #user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #erro ...