2.1 应对不断变化的需求

编写能够应对变化的需求的代码并不容易。下面来看一个例子,我们会逐步改进这个例子,以展示一些让代码更灵活的最佳做法。就农场库存程序而言,你必须实现一个从列表中筛选绿苹果的功能。听起来很简单吧?

2.1.1 初试牛刀:筛选绿苹果

我们在第1章中假设你使用一个枚举变量Color来表示苹果的各种颜色:

  1. enum Color { RED, GREEN }

第一个解决方案可能是下面这样的:

  1. public static List<Apple> filterGreenApples(List<Apple> inventory) {
  2. List<Apple> result = new ArrayList<>(); ←---- 累积苹果的列表
  3. for(Apple apple: inventory){
  4. if( GREEN.equals(apple.getColor() ) { ←---- 仅仅选出绿苹果
  5. result.add(apple);
  6. }
  7. }
  8. return result;
  9. }

突出显示的行就是筛选绿苹果所需的条件。你可以假设枚举变量Color是一个由颜色组成的集合,譬如GREEN。但是现在农民突然改主意了,他还想要筛选出红色的苹果。你该怎么做呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果。然而,要是农民想要筛选多种颜色,这种方法就应付不了了。一个好的原则是编写类似的代码之后,尽量对其进行抽象化。

2.1.2 再展身手:把颜色作为参数

为了创建filterRedApples,我们重复了filterGreenApples中的大部分代码,怎样才能避免这种问题发生呢?一种做法是给方法添加一个参数,把颜色变成参数,这样就能灵活地适应变化了:

  1. public static List<Apple> filterApplesByColor(List<Apple> inventory,
  2. Color color) {
  3. List<Apple> result = new ArrayList<>();
  4. for (Apple apple: inventory) {
  5. if ( apple.getColor().equals(color) ) {
  6. result.add(apple);
  7. }
  8. }
  9. return result;
  10. }

现在,只要像下面这样调用方法,农民朋友就会满意了:

  1. List<Apple> greenApples = filterApplesByColor(inventory, GREEN);
  2. List<Apple> redApples = filterApplesByColor(inventory, RED);
  3. ...

太简单了,对吧?让我们把例子再弄得复杂一点儿。这位农民又跑回来和你说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”

作为软件工程师,你早就想到农民可能会要改变重量,于是你写了下面的方法,用另一个参数来应对不同的重量:

  1. public static List<Apple> filterApplesByWeight(List<Apple> inventory,
  2. int weight) {
  3. List<Apple> result = new ArrayList<>();
  4. For (Apple apple: inventory){
  5. if ( apple.getWeight() > weight ) {
  6. result.add(apple);
  7. }
  8. }
  9. return result;
  10. }

解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。这有点儿令人失望,因为它打破了DRY(Don't Repeat Yourself,不要重复自己)的软件工程原则。如果你想要改变筛选遍历方式以提升性能,该怎么办?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。

你可以将颜色和重量结合为一个方法,称为filter。不过就算这样,你还是需要一种方式来区分想要筛选哪个属性。你可以加上一个标志来区分对颜色和重量的查询(但绝不要这样做!我们很快会解释为什么)。

2.1.3 第三次尝试:对你能想到的每个属性做筛选

一种把所有属性结合起来的笨拙尝试如下所示:

  1. public static List<Apple> filterApples(List<Apple> inventory, Color color,
  2. int weight, boolean flag) {
  3. List<Apple> result = new ArrayList<>();
  4. for (Apple apple: inventory) {
  5. if ( (flag && apple.getColor().equals(color)) ||
  6. (!flag && apple.getWeight() > weight) ){ ←---- 十分笨拙的选择颜色或重量的方式
  7. result.add(apple);
  8. }
  9. }
  10. return result;
  11. }

你可以这么用(但真的很笨拙):

  1. List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
  2. List<Apple> heavyApples = filterApples(inventory, null, 150, false);
  3. ...

这个解决方案再差不过了。首先,客户端代码看上去糟透了。truefalse是什么意思?此外,这个解决方案还是不能很好地应对变化的需求。如果这位农民要求你对苹果的不同属性做筛选,比如大小、形状、产地等,该怎么办?而且,如果农民要求你组合属性,做更复杂的查询,比如绿色的重苹果,又该怎么办?你会有好多个重复的filter方法,或一个巨大的非常复杂的方法。到目前为止,你已经给filterApples方法加上了值(比如StringIntegerboolean)的参数。这对于某些确定性问题可能还不错。但如今这种情况下,你需要一种更好的方式,来把苹果的选择标准告诉你的filterApples方法。下一节会介绍如何利用行为参数化实现这种灵活性。