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 一下 :)
點此下載本文的所有示例代碼。