规格模式

不知道大家是否了解LINQ技术,LINQ(LanguageINtegrated Query,语言集成查询),它提供了类似于SQL语法的遍历、筛选等功能,能完成对对象的查询,就像通过SQL语句查询数据库一样。在JAVA的世界中也有类似的辅助框架,如:JoSQL、Quaere都可以提供类似于LINQ语言。
这次要讲的主题与LINQ有很大关系,它是实现LINQ的核心。想想SQL语句中什么是最复杂的,是where后面的查询条件,看看自己写的SQL语句基本上都是一长串的条件判断,中间一堆的and、or、not逻辑符。我们今天的任务就是要实现条件语句的解析,该部分实现了,基本上LINQ语法已经实现了一大半。

我们以一个案例来讲解该技术,在内存中有10个User对象,根据不同的条件查找出用户,比如姓名包含某个字符、年龄小于多少岁等条件,类似这样的SQL:

Select * From User where name like '%国庆%';

查找出姓名中包含“国庆”两个字的用户,这在关系型数据库中很容易实现,但是在对象群中怎么实现这样的查询呢?好,看似很简单,先设计一个用户类,然后提供一个用户查找工具类,类图非常容易,如下图


用户类

public class User implements Serializable{
    private static final long serialVersionUID = -6570632837959798670L;

    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    //getter and setter
}

User只是一个简单的BO业务对象
用户操作对象接口

public interface IUserProvider {
    /**
     * 依据用户名查找用户
     * @param name
     * @return
     */
    List<User> findUserByNameEqual(String name);

    /**
     * 年龄大于指定年龄的用户
     * @param age
     * @return
     */
    List<User> findUserByAgeThan(int age);
}

在这里只定义了两个查询实现,分别是名字相同的用户和年龄大于指定年龄的用户,大家都知道,相似的查询条件还有很多,比如名字中包含指定字符、年龄小于指定年龄等,我们仅以实现这两个查询作为代表
用户操作类

public class UserProvider implements IUserProvider{
    /**
     * 用户列表
     */
    private List<User> userList;

    public UserProvider(List<User> userList) {
        this.userList = userList;
    }

    @Override
    public List<User> findUserByNameEqual(String name) {
        List<User> list = new ArrayList<>();
        for (User u : userList) {
            if (u.getName().equalsIgnoreCase(name)) {
                list.add(u);
            }
        }
        return list;
    }
    
    @Override
    public List<User> findUserByAgeThan(int age) {
        List<User> list = new ArrayList<>();
        for (User u : userList) {
            if (u.getAge() > age) {
                list.add(u);
            }
        }
        return list;
    }
}

通过for循环遍历一个动态数组,判断用户是否符合条件,将符合条件的用户放置到另外一个数组中,比较简单。
场景类

public class Client {
    public static void main(String[] args) {
        List<User> userList = new ArrayList<>();
        userList.add(new User("苏大",3));
        userList.add(new User("牛二",8));
        userList.add(new User("张三",10));
        userList.add(new User("李四",15));
        userList.add(new User("王五",18));
        userList.add(new User("赵六",20));
        userList.add(new User("马七",25));
        userList.add(new User("杨八",30));
        userList.add(new User("侯九",35));
        userList.add(new User("布十",40));

        //定义用户查询类
        IUserProvider userProvider = new UserProvider(userList);

        打印出年龄大于20岁的用户
        System.out.println("=====年龄大于20岁的用户=====");
        for (User u : userProvider.findUserByAgeThan(20)) {
            System.out.println(u);
        }
    }
}

框架已经做成,但是这样的一个框架基本上是不能适应业务变化的,为什么呢?业务变化虽然无规则,但是可以预测,比如我们这个查询,今天要查找年龄大于20岁的用户,明天要查找年龄小于30岁的用户,后天要查找姓名中包含“国庆”两个字的用户,想想看IUserProvider接口是不是要一直修改下去?接口是契约,而且我们一直提倡面向接口编程,但是在这里接口竟然都可以修改,是不是发现设计有很大问题了!

问题发现了,就要想办法解决。再回顾一下编写的代码,注意看findUserByAgeThan和findUserByNameEqual两个方法,两者的代码有什么不同呢?除了if后面的判断条件不同外,就没有不同的地方了,我们一直在说封装变化,这两段程序就仅仅有这一个变化点,我们是不是可以把它封装起来呢?完全可以,把它们两者的共同点抽取出来,if中的条件,我们可以再抽取一个类,用固定的方法来判断当前的User是否符合我们指定的条件。修改后的类图如下:


在该类图中建立了一个规格书接口,它的作用就是定制各种各样的规格,比如名字相等的规格UserByNameEqual、年龄大于基准年龄的规格UserByAgeThan等,然后在用户操作类中采用该规格进行判断。User类没有任何改变
规格书接口

public interface IUserSpecification {
    /**
     * 候选者是否满足要求
     * @param user
     * @return
     */
    boolean isSatisfiedBy(User user);
}

规格书接口只定义一个方法,判断候选用户是否满足条件。再来看姓名相同的规格书,它实现了规格书接口,如代码清单
姓名相同的规格书

public class UserByNameEqual implements IUserSpecification{
    private String name;

    public UserByNameEqual(String name) {
        this.name = name;
    }

    @Override
    public boolean isSatisfiedBy(User user) {
        return user.getName().equalsIgnoreCase(this.name);
    }
}

代码很简单,通过构造函数传递进来基准用户名,然后判断候选用户是否匹配。大于基准年龄的规格书与此类似
接下来看一下用户操作接口
用户操作接口

public interface IUserProvider {
    /**
     * 依据用户规格查询用户
     * @param userSpec
     * @return
     */
    List<User> findUser(IUserSpecification userSpec);

}

只有一个方法——根据指定的规格书查找用户。再来看其实现类
用户操作

public class UserProvider implements IUserProvider{
    /**
     * 用户列表
     */
    private List<User> userList;

    public UserProvider(List<User> userList) {
        this.userList = userList;
    }

    @Override
    public List<User> findUser(IUserSpecification userSpec) {
        List<User> result = new ArrayList<>();
        for (User u : userList) {
            if (userSpec.isSatisfiedBy(u)) {
                result.add(u);
            }
        }
        return result;
    }
}

程序改动很小,仅仅在if判断语句中根据规格书进行判断,我们持续地扩展规格书,有多少查询分类就可以扩展出多少个实现类,而IUserProvider则不需要任何改动,它的一个方法就覆盖了我们刚刚提出的N多查询路径。

大家想想看,如果现在需求变更了,比如需要一个年龄小于基准年龄的用户,该怎么修改?增加一个小于基准年龄的规格书,实现IUserSpecification接口,然后在新的业务中调用即可,别的什么都不需要修改。再比如需要一个类似SQL中like语句的处理逻辑,这个也不难,如代码清单

public class UserByNameLike implements IUserSpecification {
     //like的标记
     private final static String LIKE_FLAG = "%";
     //基准的like字符串
     private String likeStr;
     //构造函数传递基准姓名
     public UserByNameLike(String _likeStr){
             this.likeStr = _likeStr;
     }
     //检验用户是否满足条件
     public boolean isSatisfiedBy(User user) {
             boolean result = false;
             String name = user.getName();
             //替换掉%后的干净字符串
             String str = likeStr.replace("%","");
             //是以名字开头,如'国庆%'
             if(likeStr.endsWith(LIKE_FLAG) &amp;&amp; !likeStr.startsWith(LIKE_FLAG)){
                  result = name.startsWith(str);
             }else if(likeStr.startsWith(LIKE_FLAG) &amp;&amp; !likeStr.endsWith(LIKE_FLAG)){ //类似 '%国庆'
                  result = name.endsWith(str);
             }else{
                  result = name.contains(str); //类似于'%国庆%'
             }
             return result;
     }
}

到目前为止,我们已经设计了一个可扩展的对象查询平台,但是我们还有遗留问题未解决,看看SQL语句,为什么where后面会很长?是因为有AND、OR、NOT这些逻辑操作符的存在,它们可以串联起多个判断语句,然后整体反馈出一个结果来。想想看,我们上面的平台能支持这种逻辑操作符吗?不能,你要说能,那也说得通,需要两次过滤才能实现,比如要找名字包含“国庆”并且年龄大于25岁的用户,代码该怎么修改?
复合查询

public class Client {
     public static void main(String[] args) {
             //定义一个规格书
             IUserSpecification userSpec1 = new UserByNameLike("%国庆%");
             IUserSpecification userSpec2 = new UserByAgeThan(20);
             userList = userProvider.findUser(userSpec1);
             for(User u:userProvider.findUser(userSpec2)){
                     System.out.println(u);
             }
     }
}

能够实现,但是思考一下程序逻辑,它采用了两次过滤,也就是两次循环,如果对象数量少还好说,如果对象数量巨大,这个效率就太低了,这是其一;其二,组合方式非常多,比如“与”、“或”、“非”可以自由组合,姓名中包含“国庆”但年龄小于25的用户,姓名中不包含国庆但年龄大于25岁的用户等,我们还能如此设计吗?太多的组合方式,产生组合爆炸,这种设计就不妥了,应该有更优秀的方案。

我们换个方式思考该问题,不管是AND或者OR或者NOT操作,它们的返回结果都还是一个规格书,只是逻辑更复杂了而已,这3个操作符只是提供了对原有规格书的复合作用,换句话说,规格书对象之间可以进行与或非操作,操作的结果不变,分析到这里,我们就可以开始修改接口了,如代码清单

public interface IUserSpecification {
    /**
     * 候选者是否满足要求
     * @param user
     * @return
     */
    boolean isSatisfiedBy(User user);

    /**
     * and操作
     * @param spec
     * @return
     */
    IUserSpecification and(IUserSpecification spec);

    /**
     * or操作
     * @param spec
     * @return
     */
    IUserSpecification or(IUserSpecification spec);

    /**
     * not操作
     * @return
     */
    IUserSpecification not();
}

在规格书接口中增加了与或非的操作,接口修改了,实现类当然也要修改。先全面思考一下业务,与或非是不可扩展的操作,规格书(也就是规格对象)之间的操作只有这三种方法,是不需要扩展也不用预留扩展空间的。如此,我们就可以把与或非的实现放到基类中,那现在的问题变成了怎么在基类中实现与或非。注意看它们的返回值都需要返回规格书类型,很明显,我们在这里要用到递归调用了。可以这样理解,基类需要子类提供业务逻辑支持,因为基类是一个抽象类,不能实例化后返回,我们把简单类图画出来,如图所示。


基类对子类产生了依赖,然后进行递归计算,大家一定会发出这样的疑问:父类怎么可能依赖子类,这还是面向接口编程吗?想想看,我们提出面向接口编程的目的是什么?是为了适应变化,拥抱变化,对于不可能发生变化的部分为什么不能固化呢?与或非操作符号还会增加修改吗?规格书对象之间的操作还有其他吗?思考清楚这些问题后,答案就迎刃而解了。

详细的类图如下:


组合规格书

public abstract class CompositeSpecification implements IUserSpecification {

    @Override
    public IUserSpecification and(IUserSpecification spec) {
        return new AndSpecification(this, spec);
    }

    @Override
    public IUserSpecification or(IUserSpecification spec) {
        return new OrSpecification(this, spec);
    }

    @Override
    public IUserSpecification not() {
        return new NotSpecification(this);
    }
}

候选对象是否满足条件是由isSatisfiedBy方法决定的,它代表的是一个判断逻辑,由各个实现类实现。三个与或非操作在抽象类中实现,它是通过直接new了一个子类,如此设计非常符合单一职责原则,每个子类都有一个独立的职责,要么完成“与”操作,要么完成“或”操作,要么完成“非”操作。我们先来看“与”操作规格书,如代码清单
与规格书

public class AndSpecification extends CompositeSpecification{
    private IUserSpecification left;
    private IUserSpecification right;

    public AndSpecification(IUserSpecification left, IUserSpecification right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public boolean isSatisfiedBy(User user) {
        return left.isSatisfiedBy(user) && right.isSatisfiedBy(user);
    }
}

通过构造函数传递过来两个需要操作的规格书,然后通过isSatisfiedBy方法返回两者and操作的结果。或规格书和非规格书与此类似
或规格书

public class OrSpecification extends CompositeSpecification {
    private IUserSpecification left;
    private IUserSpecification right;

    public OrSpecification(IUserSpecification left, IUserSpecification right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public boolean isSatisfiedBy(User user) {
        return left.isSatisfiedBy(user) || right.isSatisfiedBy(user);
    }
}

非规格书

public class NotSpecification extends CompositeSpecification{
    private IUserSpecification spec;

    public NotSpecification(IUserSpecification spec) {
        this.spec = spec;
    }

    @Override
    public boolean isSatisfiedBy(User user) {
        return !spec.isSatisfiedBy(user);
    }
}

这三个规格书都是不发生变化的,只要使用该框架,三个规格书都要实现的,而且代码基本上是雷同的,所以才有了父类依赖子类的设计,否则是严禁出现父类依赖子类的情况的。大家再仔细看看这三个规格书和组合规格书,代码很简单,但也很巧妙,它跳出了我们面向对象设计的思维,不变部分使用一种固化方式实现。

原来的姓名相同规格书和年龄规格书等,只需要继承CompositeSpecification即可
再看一下新的场景类

public class Client {
    public static void main(String[] args) {
        List<User> userList = new ArrayList<>();
        userList.add(new User("苏大",3));
        userList.add(new User("牛二",8));
        userList.add(new User("张三",10));
        userList.add(new User("李四",15));
        userList.add(new User("王五",18));
        userList.add(new User("赵六",20));
        userList.add(new User("马七",25));
        userList.add(new User("杨八",30));
        userList.add(new User("侯九",35));
        userList.add(new User("布十",40));


        //打印年龄大于20岁,且名字为“布十”
        System.out.println("=====年龄大于20岁且名字为“布十”的用户=====");
        for (User u : userProvider.findUser(new UserByAgeThan(20).and(new UserByNameEqual("布十")))) {
            System.out.println(u);
        }
    }
}

规格模式

规格模式其实就是 组合模式+策略模式,没有太多的新东西
其通用类图如下:


为什么在通用类图中把方法名称都定义出来呢?是因为只要使用规格模式,方法名称都是这四个,它是把组合模式更加具体化了,放在一个更狭小的应用空间中。每个规格书都是一个策略,它完成了一系列逻辑的封装,用年龄相等的规格书替换年龄大于指定年龄的规格书上层逻辑有什么改变吗?不需要任何改变!规格模式非常重要,它巧妙地实现了对象筛选功能。我们来看其通用源码
抽象规格书

public interface ISpecification {
     //候选者是否满足要求
     public boolean isSatisfiedBy(Object candidate);
     //and操作
     public ISpecification and(ISpecification spec);
     //or操作
     public ISpecification or(ISpecification spec);
     //not操作
     public ISpecification not();
}

组合规格书实现与或非的算法
组合规格书

public abstract class CompositeSpecification implements ISpecification {
     //是否满足条件由实现类实现
     public abstract boolean isSatisfiedBy(Object candidate);
     //and操作
     public ISpecification and(ISpecification spec) {
             return new AndSpecification(this,spec);
     }
     //not操作
     public ISpecification not() {
             return new NotSpecification(this);
     }
     //or操作
     public ISpecification or(ISpecification spec) {
             return new OrSpecification(this,spec);
     }
}

与或非规格书代码分别如下
与规格书

public class AndSpecification extends CompositeSpecification {
     //传递两个规格书进行and操作
     private ISpecification left;
     private ISpecification right;
     public AndSpecification(ISpecification _left,ISpecification _right){
             this.left = _left;
             this.right = _right;
     }
     //进行and运算
     @Override
     public boolean isSatisfiedBy(Object candidate) {
             return left.isSatisfiedBy(candidate) &amp;&amp; right.isSatisfiedBy(candidate);
     }
}

或规格书

public class OrSpecification extends CompositeSpecification {
     //左右两个规格书
     private ISpecification left;
     private ISpecification right;
     public OrSpecification(ISpecification _left,ISpecification _right){
             this.left = _left;
             this.right = _right;
     }
     //or运算
     @Override
     public boolean isSatisfiedBy(Object candidate) {
             return left.isSatisfiedBy(candidate) || right.isSatisfiedBy(candidate);
     }
}

非规格书

public class NotSpecification extends CompositeSpecification {
     //传递一个规格书
     private ISpecification spec;
     public NotSpecification(ISpecification _spec){
             this.spec = _spec;
     }
     //not操作
     @Override
     public boolean isSatisfiedBy(Object candidate) {
             return !spec.isSatisfiedBy(candidate);
     }
}

以上一个接口、一个抽象类、3个实现类只要在适用规格模式的地方都完全相同,不用做任何的修改,要修改的是下面的规格书——业务规格书
业务规格书

public class BizSpecification extends CompositeSpecification {
     //基准对象
     private Object obj;
     public BizSpecification(Object _obj){
             this.obj = _obj;
     }
     @Override
     public boolean isSatisfiedBy(Object candidate) {
             //根据基准对象和候选对象,进行业务判断,返回boolean
             return false;
     }
}

规格模式已经是一个非常具体的应用框架了(相对于23个设计模式),大家遇到类似多个对象中筛选查找,或者业务规则不适于放在任何已有实体或值对象中,而且规则的变化和组合会掩盖那些领域对象的基本含义,或者是想自己编写一个类似LINQ的语言工具的时候就可以照搬这部分代码,只要实现自己的逻辑规格书即可。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容