通过行为参数化传递代码(Java)

行为参数化就是可以帮你处理频繁变更的需求的一种软件开发模式。

接下来,让我们来看看如何应对不断变化的需求。

1.初试牛刀:筛选绿苹果
一种常见的实现方式可能是这样:

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;
}

2.再展身手:把颜色作为参数
如果想要筛选红苹果,你该怎么做呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果。
然而,如果想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。

一个良好的原则是在编写类似的代码之后,尝试将其抽象化。

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

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

3.新需求:按重量筛选苹果
一种可能的实现方式,是另写一个按重筛选苹果方法:

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

优点
解决思路不错,不与筛选颜色的方法耦合,做到
一个方法只做一件事情,简单明了。

缺点
复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。
这有点儿令人失望,因为它
打破了DRY(Don't Repeat Yourself,不要重复自己)的软件工程原则

如果你想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。

4.糟糕的实现方式:一个方法筛选多个属性
如果筛选的属性较多,由于前一种实现方式会产生大量的重复代码,于是另一种可能的实现方式如下:

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

通过一个方法来实现对多个属性的筛选,看似简化了代码,防止出现大量重复性代码。
但却存在以下缺点

  1. 代码复杂度明显增加,可阅读性也变差了。如果筛选的属性越多,这个缺点会越明显。

  2. 从使用者的角度来看,接口参数变得难以理解了。比如flag代表什么意思,什么时候传color或weight,这无形增加了使用者对接口理解的成本。

  3. 如果还有其他属性需要筛选,比如大小、形状等,那接口将会变得异常复杂。

  4. 对于这样复杂的一个接口,使用时可能还会传错参数

5.行为参数化:多种行为,一个参数
一种更高层次的方式,是将筛选的行为抽象化,将筛选的逻辑作为参数来传递。这样一方面可以复用遍历逻辑,另一方面可以应对多种多样的筛选需求

抽象出一个通用的筛选接口:
public interface ApplePredicate {
    boolean test(Apple apple);
}

按重量筛选:
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());
    }
}

利用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,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。

行为参数化的好处
可以
把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的。(将不变与变化部分进行分离

6.匿名类
通过行为参数化,我们似乎已经找到了较为理想的实现方式了。
但是,
行为参数化仍然存在一个问题,就是对于每个行为的传递,必须通过实现一个封装该行为的类,最后实例化后再进行传递。

可不可以再简单一点呢?对于Java 8之前,答案是匿名类
对于匿名类,我们或多或少都会在项目中使用,想必大家应该都不会陌生了。例如GUI事件处理、Comparator排序实现、Runnable执行代码块等。

GUI事件处理
Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
    public void handle(ActionEvent event) {
        label.setText("Sent!!");
    }
});

Comparator排序
inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

Runnable执行代码块
Thread t = new Thread(new Runnable() {
    public void run{
        System.out.println("Hello world");
    }
});

通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:

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

7.使用Lambda表达式
接下来,我们再来看看Java 8引入Lambda表达式的实现方式:

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

不错,干净漂亮。一方面更好的陈述了问题本身,另一方面解决了代码的啰嗦问题

8.持续改进:将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;
}

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

怎么样,既灵活又简洁,还不赶紧使用Java 8亲测一把^_^。

小结

  • 行为参数化(类、匿名类、Lambda):灵活,值参数化:死板。

  • 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。

  • 将不变与变化部分解耦,既复用了代码,又保证了灵活性。

  • 没有最好只有更好,持续改进代码。

以上内容整理自《Java 8 实战》第2章,排版及内容略做调整,更易于渐进理解。

转载请注明来源:http://zhanjia.iteye.com/blog/2426409

个人公众号

二进制之路

猜你喜欢

转载自zhanjia.iteye.com/blog/2426409