代码简化利器-行为参数化

简介: 在软件工程中,一个众所周知的问题就是,不管做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”又过了两天,农民又跑回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”要如何应对这样不断变化的需求?理想的状态下,应该把工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。

1、 前言


在软件工程中,一个众所周知的问题就是,不管做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”又过了两天,农民又跑回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”要如何应对这样不断变化的需求?理想的状态下,应该把工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。


2、应对不断变化的需求


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


2.1 小试牛刀,筛选苹果


假设有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。可能想要选出所 有的绿苹果,并返回一个列表。通常我们用筛选(filter)一词来表达这个概念,可能会写这样一个方法filterGreenApples:


public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if ("green".equals(apple.getColor())) {
            result.add(apple);
        }
    }
    return (result);
}


但是现在农民改主意了,他还想要筛选红苹果。该怎么做呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果。然而,要是农民想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。


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


一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:

public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
    List<Apple> result = new ArrayList<Apple>();
    for (Apple apple : inventory) {
        if (apple.getColor().equals(color)) {
            result.add(apple);
        }
    }
    return result;
}
复制代码


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


List<Apple> greenApples = filterApplesByColor(inventory, "green");
List<Apple> redApples = filterApplesByColor(inventory, "red");
...
复制代码

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


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


public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
    List<Apple> result = new ArrayList<Apple>();
    For(Apple apple:inventory){
        if (apple.getWeight() > weight) {
            result.add(apple);
        }
    }
    return result;
}
复制代码

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


可以将颜色和重量结合为一个方法,称为filter。不过就算这样,还是需要一种方式来区分想要筛选哪个属性。可以加上一个标志来区分对颜色和重量的查询。


3、对能想到的每个属性做筛选


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

public static List<Apple> filterApples(List<Apple> inventory, String color, int weight, boolean flag) {
    List<Apple> result = new ArrayList<Apple>();
    for (Apple apple : inventory) {
        if ((flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight)) {
            result.add(apple);
        }
    }
    return result;
}
复制代码


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


List<Apple> greenApples = filterApples(inventory, "green", 0, true);、
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
复制代码


4、行为参数化


行为参数化就是可以帮助处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被程序的其他部分调用,这意味着可以推迟这块代码的执行例如,可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。


需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对的选择标准建模:考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选择标准建模:


public interface ApplePredicate{
  boolean test (Apple apple);
}
复制代码

现在就可以用ApplePredicate的多个实现代表不同的选择标准了

public class AppleHeavyWeightPredicate implements ApplePredicate{
  public boolean test(Apple apple){
    return apple.getWeight() > 150;
  }
}
public class AppleGreenColorPredicate implements ApplePredicate{
  public boolean test(Apple apple){
    return "green".equals(apple.getColor());
  }
}
复制代码


可以把这些标准看作filter方法的不同行为。刚做的这些和“策略设计模式”相关,它定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。


但是,该怎么利用ApplePredicate的不同实现呢?需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。


要在我们的例子中实现这一点,要给filterApples方法添加一个参数,让它接受ApplePredicate对象。这在软件工程上有很大好处:现在把filterApples方法迭代集合的逻辑与要应用到集合中每个元素的行为(这里是一个谓词)区分开了。


5、根据抽象条件筛选


利用ApplePredicate改过之后,filter方法看起来是这样的:

public static List<Apple> filterApples(List<Apple> inventory,ApplePredicate p){
  List<Apple> result = new ArrayList<>();
  for(Apple apple: inventory){
    if(p.test(apple)){
      result.add(apple);
    }
  }
  return result;
}
复制代码


这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在可以创建不同ApplePredicate对象,并将它们传递给filterApples方法。比如,如果农民让找出所有重量超过150克的红苹果,只需要创建一个类来实现ApplePredicate就行了。的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了:


public class AppleRedAndHeavyPredicate implements ApplePredicate{
  public boolean test(Apple apple){
    return "red".equals(apple.getColor())&& apple.getWeight() > 150;
  }
}
List<Apple> redAndHeavyApples =filterApples(inventory, new AppleRedAndHeavyPredicate());
复制代码

filterApples方法的行为取决于通过ApplePredicate对象传递的代码。也即是把filterApples方法的行为参数化了!


在例子中唯一重要的代码是test方法的实现,正是它定义了filterApples方法的新行为。但令人遗憾的是,由于该filterApples方法只能接受对象,所以必须把代码包裹在ApplePredicate对象里。的做法就类似于在内联“传递代码”,因为是通过一个实现了test方法的对象来传递布尔表达式的。


6、对付啰嗦


人们都不愿意用那些很麻烦的功能或概念。目前,当要把新的行为传递给filterApples方法的时候,不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次ApplePredicate对象。这真是很啰嗦,很费时间!

public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}
public class FilteringApples {
    public static void main(String... args) {
        List<Apple> inventory = Arrays.asList(new Apple(80, "green"),new Apple(155, "green"), new Apple(120, "red"));
        List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
        List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());
    }
    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
        List<Apple> result = new ArrayList<>();
        for (Apple apple : inventory) {
            if (p.test(apple)) {
                result.add(apple);
            }
        }
        return result;
    }
}
复制代码

费这么大劲儿真没必要,能不能做得更好呢?Java有一个机制称为匿名类,它可以同时声明和实例化一个类。它可以帮助进一步改善代码,让它变得更简洁。但这也不完全令人满意。


6.1 匿名类


匿名类和熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许同时声明并实例化一个类。换句话说,它允许随用随建。


6.2 使用匿名类


下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
  public boolean test(Apple apple){
    return "red".equals(apple.getColor());
  }
});
复制代码

但匿名类还是不够好。它往往很笨重,因为它占用了很多空间,很多模板式代码

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
  public boolean test(Apple a){
    return "red".equals(a.getColor());
  }
});
复制代码


6.3、使用Lambda 表达式


上面的代码在Java8里可以用Lambda表达式重写为下面的样子:

List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
复制代码

不得不承认这代码看上去比先前干净很多,因为它看起来更像问题陈述本身了,从下图可以看到几种方法的对比:

image.png


7、将List 类型抽象化


在通往抽象的路上,我们还可以更进一步。目前,filterApples方法还只适用于Apple。还可以将List类型抽象化,从而超越眼前要处理的问题:

public interface Predicate<T>{
  boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
  List<T> result = new ArrayList<>();
  for(T e: list){
    if(p.test(e)){
      result.add(e);
    }
  }
  return result;
}
复制代码

现在可以把filter方法用在香蕉、桔子、Integer或是String的列表上了。这里有一个使用Lambda表达式的例子:


List<Apple> redApples =filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));
List<Integer> evenNumbers =filter(numbers, (Integer i) -> i % 2 == 0);
复制代码

现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!


8、总结


跟之前的版本相比较,Java 8的新特性也可以帮助提升代码的可读性:


  • 使用Java 8,可以减少冗长的代码,让代码更易于理解
  • 通过方法引用和Stream API,代码会变得更直观


利用Lambda表达式、方法引用以及Stream改善程序代码的可读性:


  • 重构代码,用Lambda表达式取代匿名类
  • 用方法引用重构Lambda表达式
  • 用Stream API重构命令式的数据处理



目录
相关文章
|
自然语言处理 测试技术 C#
NSubstitute:一个简单易用、灵活多变的.NET单元测试模拟框架
NSubstitute是一个开源的.NET单元测试模拟类库,该框架设计的主要目标是提供一个简单明了、易用性强、贴近自然语言的模拟测试框架。它使得开发者能够专注于测试工作,而不是纠结于测试替代实例的创建和配置。
110 5
|
6月前
|
存储 算法 C语言
【编程陷阱】编写出色C++代码:遵循的注意事项和最佳实践
【编程陷阱】编写出色C++代码:遵循的注意事项和最佳实践
59 0
|
Java 编译器 应用服务中间件
代码开发优化细节
带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是final的。为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,具体参见Java运行期优化。此举能够使性能平均提高50% 。
209 2
代码开发优化细节
一个 C#例子,代码简化的过程
一个 C#例子,代码简化的过程
67 0
|
程序员 测试技术
《重构2》第十章-简化条件逻辑
《重构2》第十章-简化条件逻辑
340 0
|
安全 Java 开发者
案例之简化模式测试|学习笔记
快速学习案例之简化模式测试
案例之简化模式测试|学习笔记
|
JSON 前端开发 决策智能
优化封装方案测试| 学习笔记
快速学习优化封装方案测试。
优化封装方案测试| 学习笔记
|
新零售 存储 JavaScript
智能合约的核心思想、语法重点、编程模式、示例、规范及架构
目录 智能合约简介 智能合约例子 合约编程模式COP 合约语法 重难点 限制和规范 合约架构 什么是智能合约 一个智能合约是一套以数字形式定义的承诺(promises) ,包括合约参与方可以在上面执行这些承诺的协议。
1623 0
|
存储 SQL 测试技术
软件测试面试题:接口自动化当中,参数化(数据驱动)是如何实现?
软件测试面试题:接口自动化当中,参数化(数据驱动)是如何实现?
216 0
|
测试技术
Pytest单元测试框架如何实现参数化
Pytest单元测试框架如何实现参数化
99 0