欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

设计模式:规约模式(Specification-Pattern)

程序员文章站 2022-11-07 11:39:31
“其实地上本没有路,走的人多了,也便成了路”——鲁迅《故乡》 这句话很好的描述了设计模式的由来。前辈们通过实践和总结,将优秀的编程思想沉淀成设计模式,为开发者提供了解决问题的思路。除此之外,设计模式还是开发者之间沟通的桥梁,是程序员的语言,比如我说这段代码用的是单例模式,你就知道它的基本实现和用法。 ......

“其实地上本没有路,走的人多了,也便成了路”——鲁迅《故乡》

这句话很好的描述了设计模式的由来。前辈们通过实践和总结,将优秀的编程思想沉淀成设计模式,为开发者提供了解决问题的思路。除此之外,设计模式还是开发者之间沟通的桥梁,是程序员的语言,比如我说这段代码用的是单例模式,你就知道它的基本实现和用法。因此非常有必要弄清楚常用的设计模式。

前辈们有很多优秀的设计模式文章和图书,而本系列是我的学习笔记,我会尽量清晰易懂的将自己知道的分享出来,如果有不准确的地方请及时指正 ^_^

本文来讲解《规约模式(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?

参考资料