一、定义
规格模式(SpecificationPattern)可以认为是组合模式的一种扩展。
通常在查询、过滤等场景中,可以制定一些条件规则,这些规格也可以以 与、或、非 进行组合,从而灵活地对业务逻辑进行定制。
二、场景分析
假设有一个用户类,同时还需要提供一个操作用户的辅助类,做一些查询工作。
1. User 类
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
}
2. 用户操作类接口
import java.util.List;
public interface IUserProvider {
List<User> findUserByNameEqual(String name);
List<User> findUserByAgeThan(int age);
}
3. 用户操作的实现类
import java.util.ArrayList;
import java.util.List;
public class UserProvider implements IUserProvider {
private List<User> mUserList;
public UserProvider(List<User> userList) {
this.mUserList = userList;
}
@Override
public List<User> findUserByNameEqual(String name) {
List<User> result = new ArrayList<User>();
for (User user : mUserList) {
if (user.getName().equals(name)) {
result.add(user);
}
}
return result;
}
@Override
public List<User> findUserByAgeThan(int age) {
List<User> result = new ArrayList<User>();
for (User user : mUserList) {
if (user.getAge() > age) {
result.add(user);
}
}
return result;
}
}
4. 场景模拟
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<User> userList = new ArrayList<User>();
userList.add(new User("大毛", 24));
userList.add(new User("二毛", 23));
userList.add(new User("三毛", 22));
userList.add(new User("四毛", 21));
userList.add(new User("五毛", 20));
userList.add(new User("小明", 10));
IUserProvider userProvider = new UserProvider(userList);
System.out.println("查询出所有年龄大于22的用户");
List<User> result = userProvider.findUserByAgeThan(22);
for (User user : result) {
System.out.println(user.toString());
}
}
}
5. 继续扩展
以上结果虽然可以正常运行,但是实际场景会这么简单吗?明天我想查找年龄小于20岁的用户,后天我又想查找名字中带有“毛”的用户……难道查询接口要一直修改下去?
所以我们要想办法封装变化,抽取共同点。变化就是 if
后面的判断条件不同,共同点就是都是遍历数组,然后根据指定的条件进行过滤。
三、初步封装
既然发现了问题,我们就要想办法解决,比如将可变的因素抽离出来,拟定成规格,如下是规格接口的示例。
1. 规格接口
public interface IUserSpecification {
// 候选者是否满足要求
boolean isSatisfiedBy(User user);
}
之前已经分析到,可变的部分只有 if
后面的判断条件,因此我们将在这个可变的判断条件抽离出来制定成接口。
2. 具体规格
public class UserByNameEqual implements IUserSpecification {
private String name;
public UserByNameEqual(String name) {
this.name = name;
}
@Override
public boolean isSatisfiedBy(User user) {
return user.getName().equals(name);
}
}
public class UserByAgeThan implements IUserSpecification {
private int age;
public UserByAgeThan(int age) {
this.age = age;
}
@Override
public boolean isSatisfiedBy(User user) {
return user.getAge() > age;
}
}
3. 重新修改后的用户操作接口
import java.util.List;
public interface IUserProvider {
List<User> findUser(IUserSpecification userSpecification);
}
4. 重新修改后的用户操作实现类
import java.util.ArrayList;
import java.util.List;
public class UserProvider implements IUserProvider {
private List<User> mUserList;
public UserProvider(List<User> userList) {
this.mUserList = userList;
}
@Override
public List<User> findUser(IUserSpecification userSpecification) {
List<User> result = new ArrayList<User>();
for (User user : mUserList) {
if (userSpecification.isSatisfiedBy(user)) {
result.add(user);
}
}
return result;
}
}
5. 重新修改后的场景类
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<User> userList = new ArrayList<User>();
userList.add(new User("大毛", 24));
userList.add(new User("二毛", 23));
userList.add(new User("三毛", 22));
userList.add(new User("四毛", 21));
userList.add(new User("五毛", 20));
userList.add(new User("小明", 10));
IUserProvider userProvider = new UserProvider(userList);
System.out.println("查询出所有年龄大于22的用户");
List<User> result = userProvider.findUser(new UserByAgeThan(22));
for (User user : result) {
System.out.println(user.toString());
}
}
}
6. 继续扩展
这个时候,需求变更时,我们只需要增加一个对应需求的具体规则类即可,而不再需要修改别的接口了。但是,实际情况下我们的过滤条件通常并不止一个,例如我们想查找名字中包含“毛”并且年龄大于22的用户,那怎么办?查询两遍么?查询两遍的确可以解决,但是要进行两次循环,如果对象数量巨大,那么效率就大大降低了,且逻辑组合方式众多,比如 与、或、非 等自由组合,一味地增加子类并不是好的解决方法。
那么,我们的设计还需要改,换个角度想,不管是 AND
或者 OR
还是 NOT
操作,它们返回的结果都还是一个规则,这三个操作只是对原有规则的一个复合操作,操作的结果还是一个 boolean
。
四、最后形状
1. 修改规格接口,新增与或非方法
public interface IUserSpecification {
// 候选者是否满足要求
boolean isSatisfiedBy(User user);
// AND 操作
public IUserSpecification and(IUserSpecification spec);
// OR 操作
public IUserSpecification or(IUserSpecification spec);
// NOT 操作
public IUserSpecification not();
}
2. 新增一个抽象类,实现与或非操作
public abstract class CompositeSpecification implements IUserSpecification {
// 是否满足要求由子类去实现
public abstract boolean isSatisfiedBy(User user);
@Override
public IUserSpecification and(IUserSpecification spec) {
return new AndSepecification(this, spec);
}
@Override
public IUserSpecification or(IUserSpecification spec) {
return new OrSepecification(this, spec);
}
@Override
public IUserSpecification not() {
return new NotSepecification(this);
}
}
- 注意:
这里的抽象父类依赖了其具体子类(与或非的三个实现类),这种父类依赖子类的情景只有在非常明确不会发生变化的时候才使用,它不具备扩展性。
父类依赖子类不是面向接口编程的思想,但是面向接口编程的目的是什么呢?就是为了适应变化和拥抱变化,那么对于这种不可能发生变化的部分我们是可以选择固化的。
3. 与或非的实现类
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);
}
}
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 specification) {
this.spec = specification;
}
@Override
public boolean isSatisfiedBy(User user) {
return !spec.isSatisfiedBy(user);
}
}
这三个规则类都是不会发生变化的,因此有了固化在父类中的现象,否则禁止在父类中依赖子类。
而其他规格类,都只需要继承 CompositeSpecification
父类即可。
4. 场景
IUserSpecification spec = new UserByNameEqual("小明").and(new UserByAgeThan(20));
List<User> result = userProvider.findUser(spec);
于是我们就可以灵活的使用“与或非”的条件进行组合了。
五、类图
各个角色在上面已经介绍过了,以上一个接口、一个抽象类、三个“与或非”的实现类,基本在适用的规则模式中都相同,不用做任何修改,唯一需要修改和扩张的只有业务规则书(BizSpecification)。
我们用全局的观点思考一下,基类代表的是一个整体的规格书,其下的 AND
、OR
、NOT
等规格书都是一个个具体的实现,也就是一个局部,局部和整体的关系,是什么模式呢?对,组合模式,它就是组合模式的一个特殊应用。其次,每一个规则书都是一个策略,它完成了一系列逻辑的封装,这里面也有策略模式的影子。
六、应用场景
实现对象的查找、过滤的场景。
查看更多:设计模式分类以及六大设计原则