废话少说,从实例开始,Show me the code。我们的实例就是从一堆苹果中选出符合某种条件的苹果。
我们知道,苹果有很多属性,都可以用来作为筛选的标准。简单起见,我们只选择三个属性:颜色,大小、产地。属性的取值范围是:
- 颜色:红色,绿色
- 大小:大,小,中等
- 产地:陕西,甘肃,山东,进口
苹果Apple类定义如下:
public class Apple {
private String id = UUID.randomUUID().toString();
private Color color;
private double weight;
private Area area;
public Apple(Color color, double weight, Area area) {
this.color = color;
this.weight = weight;
this.area = area;
}
public Color getColor() {
return color;
}
public double getWeight() {
return weight;
}
public Area getArea() {
return area;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Apple)) {
return false;
}
Apple apple = (Apple) o;
return Objects.equals(id, apple.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
颜色Color是个枚举:
public enum Color {
GREEN,
RED
}
产地Area是另一个枚举:
public enum Area {
SHAN_XI,
SHAN_DONG,
GAN_SU,
ABROAD
}
无参数化
我们可以根据每个属性的每个取值分别编写一个筛选苹果的方法,例如:
(1)选择红苹果
public class AppleSelectorNoneParameterized {
private Set<Apple> apples = new HashSet<Apple>();
public void load(Apple... apples) {
this.apples = new HashSet<Apple>(Arrays.asList(apples));
}
public Set<Apple> selectRed() {
Set<Apple> results = new HashSet<>();
for (Apple apple : apples) {
if (apple.getColor() == Color.RED) {
results.add(apple);
}
}
return results;
}
}
(2)选择进口苹果
public Set<Apple> selectAbroad() {
Set<Apple> results = new HashSet<>();
for (Apple apple : apples) {
if (apple.getArea() == Area.ABROAD) {
results.add(apple);
}
}
return results;
}
还有各种组合选择,例如:
(3)选择进口的红苹果:
public Set<Apple> selectRedAndAbroad() {
Set<Apple> results = new HashSet<>();
for (Apple apple : apples) {
if (apple.getArea() == Area.ABROAD && apple.getArea() == Area.ABROAD) {
results.add(apple);
}
}
return results;
}
(4)选择进口的、大个的红苹果(我们假设重100克以上的是大苹果):
public Set<Apple> selectRedAndHeavyAndAbroad() {
Set<Apple> results = new HashSet<>();
for (Apple apple : apples) {
if (apple.getArea() == Area.ABROAD
&& apple.getArea() == Area.ABROAD
&& apple.getWeight() >= 100) {
results.add(apple);
}
}
return results;
}
我们面临的问题是什么?
-
组合爆炸
我们只选择了三个属性维度作为筛选条件,只用And作为组合方式,也需要定义很多个筛选方法:
- 单条件筛选方法:2 + 3 + 4 = 9个
- 两条件组合筛选方法:2 × 3 + 2 × 4 + 3 × 4 = 26个
- 三条件组合筛选方法:2 × 3 × 4 = 24个
- 合计:7 + 26 + 24 = 57个
如果增加更多的可筛选属性(例如品种、是否有机),或者更多的属性值(例如红绿之外还有黄色),或者加入Or和Not组合方式,后果不堪设想。
-
代码重复
上面的设计中,代码重复随处可见。每个筛选方法除了if部分外,其余部分都完全相同。我们知道在软件设计中,“重复是万恶之源。”
-
对变化没有抵抗力
当增加更多的可筛选属性(例如品种、是否有机),或者更多的可选属性值(例如红绿之外还有黄色)时,我们别无选择,只能修改AppleSelector,加上这些选择方法,这样做严重违反“开放封闭原则OCP”。OCP原则要求:当需要扩展系统的能力时,永远不要修改现有的类,而是通过添加更多的类(通常是现有类的子类或现有接口的实现类)来实现,也就是说:只添加,不修改。
数据参数化
当然,我们是专业的程序猿,对付上述问题,我们已经拥有一个趁手的武器,叫做“数据参数化”。直接看例子:
(1)根据颜色筛选苹果:
public Set<Apple> selectByColor(Color color) {
Set<Apple> results = new HashSet<>();
for (Apple apple : apples) {
if (apple.getColor() == color) {
results.add(apple);
}
}
return results;
}
(2)根据颜色和重量筛选苹果:
public Set<Apple> selectByColorAndWeightMoreThan(Color color, int threshold) {
Set<Apple> results = new HashSet<>();
for (Apple apple : apples) {
if (apple.getColor() == color && apple.getWeight() > threshold) {
results.add(apple);
}
}
return results;
}
数据参数化就是:根据外界传入的不同的数据值,调整算法的输出。在数据参数化中,不是在当前代码中列举每一个可能的属性值,而是只针对属性(或者“维度”,如果你像我一样喜欢用数学的隐喻)编写程序,属性值由代码的使用者在使用时传入。因此我们不再有selectRed()和selectGreen()方法,而是用一个统一的selectByColor()方法取而代之,用Color作为方法的参数。如果用户传入的Color是Red,就给他选出红苹果;如果用户传入的是Green,就给他选出绿苹果。color就是selectByColor()方法的数据参数。
通过对筛选条件作数据参数化,我们大大缓解了非参数化的方式面临问题:
-
降低组合爆炸
仍然是选择了三个属性维度作为筛选条件,只用And作为组合方式:
- 单条件筛选方法:3个
- 两条件组合筛选方法:3个
- 三条件组合筛选方法:1个
- 合计:7个
-
减少代码重复
由于减少了方法的数量,代码重复大大降低了,但重复代码仍然遍布在剩下的7个筛选方法上。那段For each...循环代码让人如鲠在喉,如刺在背。
-
部分适应变化
由于方法签名和实现中排除了属性值(红色,进口……),设计做到了对属性值的变化封闭,在未来需要添加更多的属性值时不需要修改现有代码。但是方法签名中仍然包含属性(颜色、产地……),因此没能做到对属性的变化封闭,当由更多的可筛选属性(例如品种)时,仍然需要修改现有代码。
没达到尽善尽美之前睡不着觉是优秀程序员的必备素质。我们还有没有更进一步的可能,在山穷水复疑无路之际,发现柳暗花明又一村?
还真的有这样的方法,隆重推出:行为参数化
行为参数化
我们的问题是思考深度不够,所以提供的解决方案都不够超然,而是太过“滞于物”了。在非参数化的形式中,我们的注意力放在属性值(红、绿、轻、重、山东、陕西……)一级;在数据参数化形式中,我们的注意力放在属性(颜色、重量、产地)一级。我们不知道用户会按什么条件筛选苹果,因此列出所有的属性的所有的组合;但是未来属性值和属性都有可能增加,因此我们的“所有”很快变成了“部分”,又需要修改和增补原来的设计。目前的两种设计,既过剩又短缺。“过剩”是因为我们提供了太多的筛选方法,其中绝大部分也许用户将来根本用不上;“短缺”是因为我们只能穷举目前已知的筛选条件,无法应对未来的扩展。
让我们回到原始的需求,看看能否找出根本的解决之道:
从一堆苹果中选出符合条件的苹果
谁来定义怎样叫做“符合条件”?
我们实际上陷入了“职责错配”的困境。我们假设我们(代码库的实现者)需要/能够知道所有筛选条件,并针对这些筛选条件给出了所有的筛选实现。我们做了下面这样的不可靠的假设:
- 我们只会根据根据苹果本身的属性筛选苹果(实际上,我们可能在夏天选择红苹果,冬天选择绿苹果,筛选条件“时间”并不是苹果本身的属性);
- 苹果的属性只有三种:颜色、大小、产地(实际上,还有很多其他的属性可能成为筛选条件,例如品种,是否有机);
- 每种属性的取值范围都是已知的,覆盖了所有可能的值,例如产地只有陕西、山东、甘肃和进口(实际上,别的省份也种苹果,也许“进口”太笼统,需要具体化为每个国家)。
因此:
为了得到一个通用的设计,我们不应该对筛选条件做任何的假设和限定,应该由代码的使用者来定义筛选条件:代码的使用者把筛选方法交给我,我据此给他选出符合条件的苹果。
“把筛选方法交给我,我使用它来执行筛选”的方式,就是“行为参数化”,即:根据外界传入的不同行为,调整算法的输出。代码的使用者将一种行为方式以参数的形式注入我们的代码,我们的代码内部执行这些行为,返回代码使用者期待的结果。
如果你学习过设计模式,你就会想到,“策略模式”是实现行为参数化的绝佳方法,其实现方式可归纳为:
- 定义一个策略接口,代表筛选的方法;
- 筛选苹果的代码通过参数接收这个策略接口的实现。在筛选时,将每一个苹果提交给它去判断是否满足条件;
- 代码的使用者负责提供策略接口的实现类,代表自己的筛选方法。
定义策略接口
我们首先针对筛选方法定义一个策略接口AppleSelectMethod:
public interface AppleSelectMethod {
boolean isSatisfiedBy(Apple apple);
}
方法isSatisfiedBy用于判断一个苹果是否满足筛选条件。
实现筛选算法
然后我们的苹果选择器的筛选方法就可以合并为一个了:
public class AppleSelectorBehaviorParameterized {
private Set<Apple> apples = new HashSet<Apple>();
public void load(Apple... apples) {
this.apples = new HashSet<Apple>(Arrays.asList(apples));
}
public Set<Apple> selectApple(AppleSelectMethod method) {
Set<Apple> results = new HashSet<>();
for (Apple apple : apples) {
if (method.isSatisfiedBy(apple)) {
results.add(apple);
}
}
return results;
}
}
数据参数化传入一个“名词”,行为参数化传入一个“动词”。
可以看出“行为参数化”有着非常巨大的优势:
-
消除组合爆炸
由于我们不预先定义筛选方式,将筛选方式转交给代码库的使用者按需定义,因此完全没有组合爆炸的问题。
-
消除重复
我们将所有的组合方法合并为一个,完全消除了代码重复的问题。
-
适应变化
不管是增加了多少的筛选方式,现有算法都可以支持,而且不需要做任何修改。
至此,我们作为代码库的提供者的责任已经完成。下面转到代码库的使用者的角色。库使用者的责任是定义具体的筛选行为,也就是提供策略接口AppleSelectMethod的实现类。
提供策略接口实现有几种不同的方式。
顶层实现类方式
最常用的方式就是由代码库使用者按需定义自己的策略接口实现类。例如根据颜色和重量筛选:
public class AppleSelectByColorAndWeight implements AppleSelectMethod {
private Color color;
private double weightThreshold;
public AppleSelectByColorAndWeight(Color color, double weightThreshold) {
this.color = color;
this.weightThreshold = weightThreshold;
}
@Override
public boolean isSatisfiedBy(Apple apple) {
return apple.getColor() == color && apple.getWeight() >= weightThreshold;
}
}
我们可以通过单元测试验证筛选方法的正确性:
public class BehaviorParameterizedByClassTest {
private Apple redAndHeavy = new Apple(Color.RED, 100, Area.ABROAD);
private Apple redAndLight = new Apple(Color.RED, 30, Area.GAN_SU);
private Apple greenAndHeavy = new Apple(Color.GREEN, 80, Area.SHAN_DONG);
private Apple greenAndLight = new Apple(Color.GREEN, 40, Area.GAN_SU);
private AppleSelectorBehaviorParameterized appleSelector;
@Before
public void setUp() {
appleSelector = new AppleSelectorBehaviorParameterized();
appleSelector.load(redAndHeavy, redAndLight, greenAndLight, greenAndHeavy);
}
@Test
public void selectRedAndHeavyApple() {
Set<Apple> selected = appleSelector.selectApple(new AppleSelectByColorAndWeight(Color.RED, 50));
assertThat(selected, hasItems(redAndHeavy));
assertThat(selected, not(hasItems(redAndLight, greenAndHeavy, greenAndLight)));
}
}
顶层实现类的方式有个缺点:系统中充斥着大量的策略接口的实现类。这些类往往只使用一次,却永久存在于系统中,增加了代码阅读者的认知负担。
匿名内类
因为代表筛选条件的类通常都只是一次性使用(在代码基中只有一个地方使用到),因此不值得把它们定义为顶层类,只需要在使用到策略接口的地方即时提供一个匿名内类就足够了,范例代码如下:
public class BehaviorParameterizedByInnerClassTest {
private Apple redAndHeavy = new Apple(Color.RED, 100, Area.ABROAD);
private Apple redAndLight = new Apple(Color.RED, 30, Area.GAN_SU);
private Apple greenAndHeavy = new Apple(Color.GREEN, 80, Area.SHAN_DONG);
private Apple greenAndLight = new Apple(Color.GREEN, 40, Area.GAN_SU);
private AppleSelectorBehaviorParameterized appleSelector;
@Before
public void setUp() {
appleSelector = new AppleSelectorBehaviorParameterized();
appleSelector.load(redAndHeavy, redAndLight, greenAndLight, greenAndHeavy);
}
@Test
public void selectRedApple() {
Set<Apple> selected = appleSelector.selectApple(new AppleSelectMethod() {
@Override
public boolean isSatisfiedBy(Apple apple) {
return apple.getColor() == Color.RED;
}
});
assertThat(selected, hasItems(redAndHeavy, redAndLight));
assertThat(selected, not(hasItems(greenAndHeavy, greenAndLight)));
}
@Test
public void selectRedAndHeavyApple() {
Set<Apple> selected = appleSelector.selectApple(new AppleSelectMethod() {
@Override
public boolean isSatisfiedBy(Apple apple) {
return apple.getColor() == Color.RED && apple.getWeight() > 50;
}
});
assertThat(selected, hasItems(redAndHeavy));
assertThat(selected, not(hasItems(redAndLight, greenAndHeavy, greenAndLight)));
}
}
匿名内类的方式也有缺陷:我们只需要一个筛选方法,它却传给我一个完整的类实现。大量的样板代码污染了我们的代码基,分散了我们的注意力。它是一种“非本质复杂性”(并非来源于问题域的本质复杂性,而是源于编程语言的技术缺陷),应该彻底消除。
Java 8的lambda表达式,让我们可以消除这种非本质复杂性。
Lambda表达式
当筛选苹果的时候,我需要的是一个筛选方法(一段代码),而不是一个完整的类。在Java 8之前,我们无法做到直接将代码传递给方法,只能将代码封装为一个类中的一个方法,然后传递这个类。Java 8改变了一切,支持函数式编程,从此,代码(函数)成为和数据(数值、字符串、对象)一样的一等公民,可以传递给方法作为参数,可以赋值给变量和字段,甚至可以作为方法的返回值返回。下面是采用Lambda表达式筛选苹果的例子:
public class BehaviorParameterizedByLambdaTest {
private Apple redAndHeavy = new Apple(Color.RED, 100, Area.ABROAD);
private Apple redAndLight = new Apple(Color.RED, 30, Area.GAN_SU);
private Apple greenAndHeavy = new Apple(Color.GREEN, 80, Area.SHAN_DONG);
private Apple greenAndLight = new Apple(Color.GREEN, 40, Area.GAN_SU);
private AppleSelectorBehaviorParameterized appleSelector;
@Before
public void setUp() {
appleSelector = new AppleSelectorBehaviorParameterized();
appleSelector.load(redAndHeavy, redAndLight, greenAndLight, greenAndHeavy);
}
@Test
public void selectRedApple() {
Set<Apple> selected = appleSelector.selectApple(
apple -> apple.getColor() == Color.RED);
assertThat(selected, hasItems(redAndHeavy, redAndLight));
assertThat(selected, not(hasItems(greenAndHeavy, greenAndLight)));
}
@Test
public void selectRedAndHeavyApple() {
Set<Apple> selected = appleSelector.selectApple(
apple -> apple.getColor() == Color.RED && apple.getWeight() > 50);
assertThat(selected, hasItems(redAndHeavy));
assertThat(selected, not(hasItems(redAndLight, greenAndHeavy, greenAndLight)));
}
}
本质上,上面的例子中,lambda表达式apple -> apple.getColor() == Color.RED和apple -> apple.getColor() == Color.RED && apple.getWeight() > 50都是函数式接口(只有一个抽象方法的接口)AppleSelectMethod的即时实现。
采用Lambda表达式语法之后,代码重新变得简洁和优美,消除了不必要的技术代码行,直接用面向业务的语言编写代码。
通过上面的例子,我们可以看到,行为参数化的本质就是将行为(如同数据一样)传递给实现主算法的方法,影响主算法的输出结果。在Java 8中,Lambda表达式实际上是个匿名函数(函数类似于方法,但不与一个具体对象关联),我喜欢这样描述它:
传递时是名词
执行时是动词
就像一台发动机,在被作为货物运输时是名词——被处理的被动对象,而在加电运转时是动词——一个能动的机械。运输时我们关注的是它的属性(重量、体积),运行时我们关注的是它的行为。
各种参数化方式的比较
我们可以从僵化性/灵活性与繁杂性/简洁性两个维度比较各种参数化方式的优劣:
- 在僵化性/灵活性的维度上,数据参数化是相当僵化的(相前面所说的,既过剩又短缺),行为参数化则灵活的多(按需即时实现)。
- 在繁杂性/简洁性的维度上,数据参数化、实现类和匿名内类形式的行为参数化都比较繁杂(充满重复代码或样板代码),而lambda则非常简洁(直接用领域词汇表达业务意图)。
进一步泛化
目前为止,一切都很优雅:通过行为参数化,我们得到了简单性和灵活性的高度统一。我们通过逐步提高抽象的层级,使得筛选苹果的方法越来越通用:
- 一开始,我们直接关注属性值(数学隐喻是坐标值,例如红,绿,轻,重),针对属性值进行筛选;
- 通过数据参数化,我们将抽象层级提高到属性(数学隐喻是维度,例如颜色、重量)一级,针对属性进行筛选,将提供属性值的责任转移给代码库的使用者;
- 通过行为参数化,我们完全抛弃“只能根据苹果自身的属性筛选苹果”这样一个不合理的假设,将定义筛选条件的责任完全转移给代码库的使用者。
通过“抽象”和“职责分离”,我们得到了一个通用的解决方案。
但是,还不够通用!
我们的库代码,还被绑定在“苹果”这个特定领域的概念上。
我们的AppleSelector类是Apple相关的:
public class AppleSelectorBehaviorParameterized {
private Set<Apple> apples = new HashSet<Apple>();
public void load(Apple... apples) {
this.apples = new HashSet<Apple>(Arrays.asList(apples));
}
public Set<Apple> selectApple(AppleSelectMethod method) {
Set<Apple> results = new HashSet<>();
for (Apple apple : apples) {
if (method.isSatisfiedBy(apple)) {
results.add(apple);
}
}
return results;
}
}
我们的策略接口AppleSelectMethod也是Apple相关的:
public interface AppleSelectMethod {
boolean isSatisfiedBy(Apple apple);
}
这意味着当我们要选择桔子的时候,需要另外编写一套相似的代码。
我们完全应该而且可以将我们的设计方案泛化到领域无关的、最通用的层次,不再限定到苹果、桔子或其他任何具体事物上。
我们将AppleSelectorBehaviorParameterized泛化为ItemSelector类:
public class ItemSelector<T> {
private Set<T> items = new HashSet<>();
public void load(T... items) {
this.items = new HashSet<T>(Arrays.asList(items));
}
public Set<T> select(ItemSelectCriteria<T> method) {
Set<T> results = new HashSet<>();
for (T item : items) {
if (method.isSatisfiedBy(item)) {
results.add(item);
}
}
return results;
}
}
将AppleSelectMethod策略接口泛化为ItemSelectCriteria接口:
public interface ItemSelectCriteria<T> {
boolean isSatisfiedBy(T item);
}
下面是测试方法:
public class ItemSelectorTest {
private Apple redAndHeavy = new Apple(Color.RED, 100, Area.ABROAD);
private Apple redAndLight = new Apple(Color.RED, 30, Area.GAN_SU);
private Apple greenAndHeavy = new Apple(Color.GREEN, 80, Area.SHAN_DONG);
private Apple greenAndLight = new Apple(Color.GREEN, 40, Area.GAN_SU);
private ItemSelector<Apple> selector;
@Before
public void setUp() {
selector = new ItemSelector();
selector.load(redAndHeavy, redAndLight, greenAndLight, greenAndHeavy);
}
@Test
public void selectRedApple() {
Set<Apple> selected = selector.select(
apple -> apple.getColor() == Color.RED);
assertThat(selected, hasItems(redAndHeavy, redAndLight));
assertThat(selected, not(hasItems(greenAndHeavy, greenAndLight)));
}
@Test
public void selectRedAndHeavyApple() {
Set<Apple> selected = selector.select(
apple -> apple.getColor() == Color.RED && apple.getWeight() > 50);
assertThat(selected, hasItems(redAndHeavy));
assertThat(selected, not(hasItems(redAndLight, greenAndHeavy, greenAndLight)));
}
}
总结一下,我们通过逐级提高抽象层次,得到了一个越来越通用的解决方案:
具体事物的属性值 -> 具体事物的属性 -> 具体事物 -> 一般事物
我们从
从一堆苹果中筛选出红色的大苹果
这样一个非常领域特定的问题得出了
从一个集合中根据某种条件筛选出一个子集
这样一个通用的解决方案。
至此,这项解决方案可以脱离具体项目,提升为企业级通用类库的一部分,在多个项目间重用。
Java 8, Lambda和Stream
事实上,由于从一个集合中根据某种条件筛选出一个子集是如此通用的一个功能,Java 8直接在JDK的层面上实现了,成为世界级通用方法。
JDK中通过流Stream(类似于集合Collection的概念),可以实现根据用户给出的筛选函数获取满足指定条件的元素的功能:
public class StreamTest {
private Apple redAndHeavy = new Apple(Color.RED, 100, Area.ABROAD);
private Apple redAndLight = new Apple(Color.RED, 30, Area.GAN_SU);
private Apple greenAndHeavy = new Apple(Color.GREEN, 80, Area.SHAN_DONG);
private Apple greenAndLight = new Apple(Color.GREEN, 40, Area.GAN_SU);
private List<Apple> apples;
@Before
public void setUp() {
apples = Arrays.asList(redAndHeavy, redAndLight, greenAndLight, greenAndHeavy);
}
@Test
public void selectRedApple() {
Set<Apple> selected = apples
.stream()
.filter(apple -> apple.getColor() == Color.RED)
.collect(Collectors.toSet());
assertThat(selected, hasItems(redAndHeavy, redAndLight));
assertThat(selected, not(hasItems(greenAndHeavy, greenAndLight)));
}
@Test
public void selectRedAndHeavyApple() {
Set<Apple> selected = apples
.stream()
.filter(apple -> apple.getColor() == Color.RED)
.filter(apple -> apple.getWeight() > 50)
.collect(Collectors.toSet());
assertThat(selected, hasItems(redAndHeavy));
assertThat(selected, not(hasItems(redAndLight, greenAndHeavy, greenAndLight)));
}
}
具体做法是:
- 对原有集合调用stream()方法,转化成Stream;
- 对stream调用filter()方法,传入一个代表筛选条件的函数,得到一个由符合筛选条件的元素组成的新的stream;
- 对新stream调用collect()方法,将流中的元素收集到结果集合中。
stream的filter()接收一个Predicate函数式接口的实现作为参数。Predicate含有一个抽象方法
boolean test(T t);
用于判断一个元素t是否满足条件。我们的lambda表达式
apple -> apple.getColor() == Color.RED
实际上是Predicate的test()方法的即时实现,作为stream的filter()方法的行为参数。->符号左边的部分apple是test()方法的参数,右边部分是test()的方法体。
结语
以一句正确的废话作总结吧:
磨练你的抽象能力,尽可能达成最泛化的设计。
范例项目可以从范例项目地址下载。