“其實地上本沒有路,走的人多了,也便成了路”——魯迅《故鄉》 這句話很好的描述了設計模式的由來。前輩們通過實踐和總結,將優秀的編程思想沉澱成設計模式,為開發者提供瞭解決問題的思路。除此之外,設計模式還是開發者之間溝通的橋梁,是程式員的語言,比如我說這段代碼用的是單例模式,你就知道它的基本實現和用法。 ...
“其實地上本沒有路,走的人多了,也便成了路”——魯迅《故鄉》
這句話很好的描述了設計模式的由來。前輩們通過實踐和總結,將優秀的編程思想沉澱成設計模式,為開發者提供瞭解決問題的思路。除此之外,設計模式還是開發者之間溝通的橋梁,是程式員的語言,比如我說這段代碼用的是單例模式,你就知道它的基本實現和用法。因此非常有必要弄清楚常用的設計模式。
前輩們有很多優秀的設計模式文章和圖書,而本系列是我的學習筆記,我會儘量清晰易懂的將自己知道的分享出來,如果有不准確的地方請及時指正 ^_^
本文來講解《規約模式(Specification-Pattern)》
什麼是規約模式?
規約模式經常在DDD中使用,用來將業務規則(通常是隱式業務規則)封裝成獨立的邏輯單元,從而將隱式業務規則提煉為顯示概念,並達到代碼復用的目的。
講理論就是很枯燥,比如上面這段定義,雖然說明白了什麼是規約模式,但是又引入了兩個概念:隱式業務規則和顯示概念。太無趣了,我決定皮一下子……
- 什麼是隱式業務規則?假如你開發了一個網站,你的目標用戶是18歲以上人群,你懂的,當地的政策不允許18歲以下瀏覽。那麼你該如何驗證註冊用戶是否符合要求呢?
public ActionResult Register(UserRegisterInfo user){
if(user.Age < 18){
throw new Exception("Too young too simple...");
}
//todo:註冊邏輯
}
在Register方法中if語句就是一條隱式業務規則。
當然這樣寫也能滿足業務規則,但是改天新來一個叫王二的程式員,沒鬧明白為啥要加if判斷、或者沒鬧明白為啥是18,本萬物皆可盤的態度盤了這段代碼,程式仍然照常運行,王二也很開心,但是業務完整性就被破壞了。因此就需要將隱式的業務規則提煉成現實概念。
- 什麼是顯示概念?顯示概念跟隱式業務規則相對應,意味著我們要把上面代碼中的if判斷提煉出來了。
public ActionResult Register(UserRegisterInfo user){
var specification = new UserMustBeAdultSpecification();
if(!specification.IsSatisfiedBy(user)){
throw new Exception("Too young too simple...");
}
//todo:註冊邏輯
}
class UserMustBeAdultSpecification {
private int adultAge;
public UserMustBeAdultSpecification(int adultAge = 18){
this.adultAge = adultAge;
}
public IsSatisfiedBy(UserRegisterInfo user){
return user.Age > this.adultAge;
}
}
我們把if判斷提煉成一個顯示概念,用來確認用戶必須是成人。這樣王二來了以後,也不至於揣著明白裝糊塗。
為什麼需要規約模式?
這裡先說一下為什麼需要把隱式業務規則轉變為顯示概念?通常我們的業務規則不會僅僅驗證一下年齡這麼簡單,例如訂單提交,你可能需要驗證用戶賬號是否可用、訂單商品的庫存是否滿足預定量、配送地址是否完整……如果僅僅是通過一連串的if判斷,那就真的太不利於維護了,並且if嵌套的多了代碼難於理解,不好說明白具體意圖。因此需要將隱式業務規則轉換成顯示概念,這也是DDD的要求。
如果上面的例子還不能很好的打動你,我們再舉一個慄子。
你的網站不僅僅需要註冊吧,它可能還有更新用戶信息的功能,更新的時候我們仍然需要確認用戶必須是成人,看吧,提煉的顯示概念再一次派上用場,達到了代碼復用的目的,這樣就滿足了DRY的要求。
另外,規約模式還有一個更加常用的場景,就是進行數據查詢,繼續往下看……
如何實現規約模式?
規約模式要求我們每個規約都要有一個bool IsSatisfiedBy(model)
方法,用來驗證模型是否滿足規約要求,我們上面的例子就是典型的規約類,但是沒有進行任何抽象。
規約的另一個更常用的用途是進行數據篩選,而我們的篩選條件通常是複雜的,因此規約還要實現鏈式操作。因此需要進行抽象,到達操作一致的目的。
以下代碼來自codeproject,不喜勿噴,熟悉的直接繞道最後一段。
定義介面:
public interface ISpecification<T> {
bool IsSatisfiedBy(T o);
ISpecification<T> And(ISpecification<T> specification);
ISpecification<T> Or(ISpecification<T> specification);
ISpecification<T> Not(ISpecification<T> specification);
}
每個規約實現四個方法:IsSatisfiedBy()、And()、Or()、Not()。IsSatisfiedBy()方法主要實現業務規則,而其它三個則用來將複合業務規則連在一起。
來看它的抽象實現:
public abstract class CompositeSpecification<T> : ISpecification<T>
{
public abstract bool IsSatisfiedBy(T o);
public ISpecification<T> And(ISpecification<T> specification)
{
return new AndSpecification<T>(this, specification);
}
public ISpecification<T> Or(ISpecification<T> specification)
{
return new OrSpecification<T>(this, specification);
}
public ISpecification<T> Not(ISpecification<T> specification)
{
return new NotSpecification<T>(specification);
}
}
對於所有複合規約來說,And()、Or()、Not()方法都是相同的,只有IsSatisfiedBy()方法會有區別。接來下看一下鏈式規約的實現,分別對應And()、Or()、Not()方法:
public class AndSpecification<T> : CompositeSpecification<T>
{
ISpecification<T> leftSpecification;
ISpecification<T> rightSpecification;
public AndSpecification(ISpecification<T> left, ISpecification<T> right) {
this.leftSpecification = left;
this.rightSpecification = right;
}
public override bool IsSatisfiedBy(T o) {
return this.leftSpecification.IsSatisfiedBy(o)
&& this.rightSpecification.IsSatisfiedBy(o);
}
}
public class OrSpecification<T> : CompositeSpecification<T>
{
ISpecification<T> leftSpecification;
ISpecification<T> rightSpecification;
public AndSpecification(ISpecification<T> left, ISpecification<T> right) {
this.leftSpecification = left;
this.rightSpecification = right;
}
public override bool IsSatisfiedBy(T o) {
return this.leftSpecification.IsSatisfiedBy(o)
|| this.rightSpecification.IsSatisfiedBy(o);
}
}
public class NotSpecification<T> : CompositeSpecification<T>
{
ISpecification<T> specification;
public AndSpecification(ISpecification<T> specification) {
this.specification = specification;
}
public override bool IsSatisfiedBy(T o) {
return !this.specification.IsSatisfiedBy(o);
}
}
Linq表達式規約
當規約用於數據查詢時,使用Linq表達式規約將非常有用。但是,這種方式與規約模式的初衷相悖,因為我們再一次把業務規則隱藏在了Linq表達式中。
基於表達式的規約
public class ExpressionSpecification<T> : CompositeSpecification<T> {
private Func<T, bool> expression;
public ExpressionSpecification(Func<T, bool> expression) {
if (expression == null)
throw new ArgumentNullException();
else
this.expression = expression;
}
public override bool IsSatisfiedBy(T o) {
return this.expression(o);
}
}
看完代碼你會發現,只要你願意,表達式規約能夠滿足你幾乎所有需求。因此說它是一種反模式,是違背初衷的用法。為啥還要列出來呢?對於查詢來說太強大了……此處可以寫一篇《論提升規約模式十倍生產力的方法》。
代碼的用法
我們依然複製codeproject上面的代碼:
class Program
{
static void Main(string[] args)
{
List<Mobile> mobiles = new List<Mobile> {
new Mobile(BrandName.Samsung, Type.Smart, 700),
new Mobile(BrandName.Apple, Type.Smart),
new Mobile(BrandName.Htc, Type.Basic),
new Mobile(BrandName.Samsung, Type.Basic) };
ISpecification<Mobile> samsungExpSpec =
new ExpressionSpecification<Mobile>(o => o.BrandName == BrandName.Samsung);
ISpecification<Mobile> htcExpSpec =
new ExpressionSpecification<Mobile>(o => o.BrandName == BrandName.Htc);
ISpecification<Mobile> SamsungHtcExpSpec = samsungExpSpec.Or(htcExpSpec);
ISpecification<Mobile> NoSamsungExpSpec =
new ExpressionSpecification<Mobile>(o => o.BrandName != BrandName.Samsung);
var samsungMobiles = mobiles.FindAll(o => samsungExpSpec.IsStatisfiedBy(o));
var htcMobiles = mobiles.FindAll(o => htcExpSpec.IsStatisfiedBy(o));
var samsungHtcMobiles = mobiles.FindAll(o => SamsungHtcExpSpec.IsStatisfiedBy(o));
var noSamsungMobiles = mobiles.FindAll(o => NoSamsungExpSpec.IsStatisfiedBy(o));
}
}
組合查詢:
ISpecification<Mobile> complexSpec = (samsungExpSpec.Or(htcExpSpec)).And(brandExpSpec);
非Linq用法:
public class PremiumSpecification<T> : CompositeSpecification<T>
{
private int cost;
public PremiumSpecification(int cost) {
this.cost = cost;
}
public override bool IsSatisfiedBy(T o) {
return (o as Mobile).Cost >= this.cost;
}
}
組合用法:
ISpecification<Mobile> premiumSpecification = new PremiumSpecification<Mobile>(600);
ISpecification<Mobile> linqNonLinqExpSpec = NoSamsungExpSpec.And(premiumSpecification);
探討:與CQRS的衝突,該如何選擇?
在《CQRS vs Specification pattern》中,作者指出,規約模式提倡將驗證和查詢復用同一個邏輯單元,而在CQRS中,驗證是在Command中的邏輯,查詢是在Query中的邏輯,CQRS提倡命令和查詢進行分離,從而構建低耦合的系統。
那麼該如何選擇呢?是選擇SRP還是放棄DRY?
參考資料
- https://en.wikipedia.org/wiki/Specification_pattern
- https://www.codeproject.com/Articles/670115/Specification-pattern-in-Csharp
- https://enterprisecraftsmanship.com/posts/specification-pattern-c-implementation/
- https://enterprisecraftsmanship.com/posts/cqrs-vs-specification-pattern/
- https://my.oschina.net/HenuToater/blog/171378