9.2 使用Lambda重构面向对象的设计模式

新的语言特性常常让现存的编程模式或设计黯然失色。比如,Java 5引入了for-each循环,由于它的稳健性和简洁性,已经替代了很多显式使用迭代器的情形。Java 7推出的菱形操作符(<>)帮助大家在创建实例时无须显式使用泛型,一定程度上推动了Java程序员们采用类型接口(type interface)进行程序设计。

对设计经验的归纳总结被称为设计模式2。设计模式是一种可重用的蓝图,设计软件时,如果你愿意,可以复用这些方式或方法来解决一些常见问题。这看起来很像传统建筑工程师的工作方式,对典型的场景(比如悬挂桥、拱桥等)都定义有可重用的解决方案。例如,访问者模式常用于分离程序的算法和它的操作对象。单例模式一般用于限制类的实例化,仅生成一份对象。

2如果你希望更进一步了解设计模式,请参阅由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides编写的《设计模式:可复用面向对象软件的基础》。

Lambda表达式为程序员的工具箱又新添了一件利器。它们为解决传统设计模式所面对的问题提供了新的解决方案,不但如此,采用这些方案往往更高效、更简单。使用Lambda表达式后,很多现存的略显臃肿的面向对象设计模式能够用更精简的方式实现了。这一节会针对五个设计模式展开讨论,它们分别是:

  • 策略模式;
  • 模板方法;
  • 观察者模式;
  • 责任链模式;
  • 工厂模式。

我们会展示Lambda表达式是如何另辟蹊径解决设计模式原来试图解决的问题的。

9.2.1 策略模式

策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。在第2章中你已经简略地了解过这种模式了,当时我们介绍了如何使用不同的条件(比如苹果的重量,或者颜色)来筛选库存中的苹果。你可以将这一模式应用到更广泛的领域,比如使用不同的标准来验证输入的有效性,使用不同的方式来分析或者格式化输入。

策略模式包含三部分内容,如图9-1所示。

  • 一个代表某个算法的接口(Strategy接口)。
  • 一个或多个该接口的具体实现,它们代表了算法的多种实现(比如,实体类ConcreteStrategyA或者ConcreteStrategyB)。
  • 一个或多个使用策略对象的客户。

9.2 使用Lambda重构面向对象的设计模式 - 图1

图 9-1 策略模式

假设你希望验证输入的内容是否根据标准进行了恰当的格式化(比如只包含小写字母或数字)。你可以从定义一个验证文本(以String的形式表示)的接口入手:

  1. public interface ValidationStrategy {
  2. boolean execute(String s);
  3. }

其次,你定义了该接口的一个或多个具体实现:

  1. public class IsAllLowerCase implements ValidationStrategy {
  2. public boolean execute(String s){
  3. return s.matches("[a-z]+");
  4. }
  5. }
  6. public class IsNumeric implements ValidationStrategy {
  7. public boolean execute(String s){
  8. return s.matches("\\d+");
  9. }
  10. }

之后,你就可以在你的程序中使用这些略有差异的验证策略了:

  1. public class Validator{
  2. private final ValidationStrategy strategy;
  3. public Validator(ValidationStrategy v){
  4. this.strategy = v;
  5. }
  6. public boolean validate(String s){
  7. return strategy.execute(s);
  8. }
  9. }
  10. Validator numericValidator = new Validator(new IsNumeric());
  11. boolean b1 = numericValidator.validate("aaaa"); ←---- 返回false
  12. Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
  13. boolean b2 = lowerCaseValidator.validate("bbbb"); ←---- 返回true
使用Lambda表达式

到现在为止,你应该已经意识到ValidationStrategy是一个函数接口了。除此之外,它还与Predicate具有同样的函数描述。这意味着我们不需要声明新的类来实现不同的策略,通过直接传递Lambda表达式就能达到同样的目的,并且还更简洁:

  1. Validator numericValidator =
  2. new Validator((String s) -> s.matches("[a-z]+")); (以下4行)直接传递Lambda表达式
  3. boolean b1 = numericValidator.validate("aaaa");
  4. Validator lowerCaseValidator =
  5. new Validator((String s) -> s.matches("\\d+"));
  6. boolean b2 = lowerCaseValidator.validate("bbbb");

正如你看到的,Lambda表达式避免了采用策略设计模式时僵化的模板代码。如果你仔细分析一下个中缘由,可能会发现,Lambda表达式实际已经对部分代码(或策略)进行了封装,而这就是创建策略设计模式的初衷。因此,强烈建议对类似的问题,你应该尽量使用Lambda表达式来解决。

9.2.2 模板方法

如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。好吧,这样讲听起来有些抽象。换句话说,模板方法模式在你“希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果”时是非常有用的。

让我们从一个例子着手,看看这个模式是如何工作的。假设你需要编写一个简单的在线银行应用。通常,用户需要输入一个用户账户,之后应用才能从银行的数据库中得到用户的详细信息,最终完成一些让用户满意的操作。不同分行的在线银行应用让客户满意的方式可能略有不同,比如给客户的账户发放红利,或者仅仅是少发送一些推广文件。你可能通过下面的抽象类方式来实现在线银行应用:

  1. abstract class OnlineBanking {
  2. public void processCustomer(int id){
  3. Customer c = Database.getCustomerWithId(id);
  4. makeCustomerHappy(c);
  5. }
  6. abstract void makeCustomerHappy(Customer c);
  7. }

processCustomer方法搭建了在线银行算法的框架:获取客户提供的ID,然后提供服务让用户满意。不同的支行可以通过继承OnlineBanking类,对makeCustomerHappy方法提供差异化的实现。

使用Lambda表达式

使用你偏爱的Lambda表达式同样可以解决这些问题(创建算法框架,让具体的实现插入某些部分)。你想要插入的不同算法组件可以通过Lambda表达式或者方法引用的方式实现。

这里我们向processCustomer方法引入了第二个参数,它是一个Consumer类型的参数,与前文定义的makeCustomerHappy的特征保持一致:

  1. public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
  2. Customer c = Database.getCustomerWithId(id);
  3. makeCustomerHappy.accept(c);
  4. }

现在,你可以很方便地通过传递Lambda表达式,直接插入不同的行为,不再需要继承OnlineBanking类了:

  1. new OnlineBankingLambda().processCustomer(1337, (Customer c) ->
  2. System.out.println("Hello " + c.getName());

这是又一个例子,佐证了Lamba表达式能帮助你解决设计模式与生俱来的设计僵化问题。

9.2.3 观察者模式

观察者模式是一种比较常见的方案,某些事件发生时(比如状态转变),如果一个对象(通常称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。创建图形用户界面(GUI)程序时,你经常会使用该设计模式。这种情况下,你会在图形用户界面组件(比如按钮)上注册一系列的观察者。如果点击按钮,观察者就会收到通知,并随即执行某个特定的行为。但是观察者模式并不局限于图形用户界面。比如,观察者设计模式也适用于股票交易的情形,多个券商(观察者)可能都希望对某一支股票价格(主题)的变动做出响应。图9-2通过UML图解释了观察者模式。

9.2 使用Lambda重构面向对象的设计模式 - 图2

图 9-2 观察者模式

让我们写点儿代码来看看观察者模式在实际中多么有用。你需要为Twitter这样的应用设计并实现一个定制化的通知系统。想法很简单:好几家报纸机构,比如美国《纽约时报》、英国《卫报》以及法国《世界报》都订阅了新闻推文,他们希望当接收的新闻中包含他们感兴趣的关键字时,能得到特别通知。

首先,你需要一个Observer接口,它将不同的观察者聚合在一起。它仅有一个名为notify的方法,一旦接收到一条新的新闻,该方法就会被调用:

  1. interface Observer {
  2. void notify(String tweet);
  3. }

现在,你可以声明不同的观察者(比如,这里是三家不同的报纸机构),依据新闻中不同的关键字分别定义不同的行为:

  1. class NYTimes implements Observer{
  2. public void notify(String tweet) {
  3. if(tweet != null && tweet.contains("money")){
  4. System.out.println("Breaking news in NY! " + tweet);
  5. }
  6. }
  7. }
  8. class Guardian implements Observer{
  9. public void notify(String tweet) {
  10. if(tweet != null && tweet.contains("queen")){
  11. System.out.println("Yet more news from London... " + tweet);
  12. }
  13. }
  14. }
  15. class LeMonde implements Observer{
  16. public void notify(String tweet) {
  17. if(tweet != null && tweet.contains("wine")){
  18. System.out.println("Today cheese, wine and news! " + tweet);
  19. }
  20. }
  21. }

你还遗漏了最重要的部分:Subject!让我们为它定义一个接口:

  1. interface Subject{
  2. void registerObserver(Observer o);
  3. void notifyObservers(String tweet);
  4. }

Subject使用registerObserver方法可以注册一个新的观察者,使用notifyObservers方法通知它的观察者一个新闻的到来。让我们更进一步,实现Feed类:

  1. class Feed implements Subject{
  2. private final List<Observer> observers = new ArrayList<>();
  3. public void registerObserver(Observer o) {
  4. this.observers.add(o);
  5. }
  6. public void notifyObservers(String tweet) {
  7. observers.forEach(o -> o.notify(tweet));
  8. }
  9. }

这是一个非常直观的实现:Feed类在内部维护了一个观察者列表,一条新闻到达时,它就进行通知。你可以创建一个实例应用,对新闻主题和观察者进行封装,如下所示:

  1. Feed f = new Feed();
  2. f.registerObserver(new NYTimes());
  3. f.registerObserver(new Guardian());
  4. f.registerObserver(new LeMonde());
  5. f.notifyObservers("The queen said her favourite book is Modern Java in Action!");

毫不意外,《卫报》会特别关注这条新闻!

使用Lambda表达式

你可能会疑惑Lambda表达式在观察者设计模式中如何发挥它的作用。不知道你有没有注意到,Observer接口的所有实现类都提供了一个方法:notify。新闻到达时,它们都只是对同一段代码封装执行。Lambda表达式的设计初衷就是要消除这样的僵化代码。使用Lambda表达式后,你无须显式地实例化三个观察者对象,直接传递Lambda表达式表示需要执行的行为即可:

  1. f.registerObserver((String tweet) -> {
  2. if(tweet != null && tweet.contains("money")){
  3. System.out.println("Breaking news in NY! " + tweet);
  4. }
  5. });
  6. f.registerObserver((String tweet) -> {
  7. if(tweet != null && tweet.contains("queen")){
  8. System.out.println("Yet more news from London... " + tweet);
  9. }
  10. });

那么,是否随时随地都可以使用Lambda表达式呢?答案是否定的!前文介绍的例子中,Lambda适配得很好,那是因为需要执行的动作都很简单,因此才能很方便地消除僵化代码。但是,观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义了多个方法,诸如此类。在这些情形下,你还是应该继续使用类的方式。

9.2.4 责任链模式

责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。

通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继。代码中,这段逻辑看起来是下面这样:

  1. public abstract class ProcessingObject<T> {
  2. protected ProcessingObject<T> successor;
  3. public void setSuccessor(ProcessingObject<T> successor){
  4. this.successor = successor;
  5. }
  6. public T handle(T input){
  7. T r = handleWork(input);
  8. if(successor != null){
  9. return successor.handle(r);
  10. }
  11. return r;
  12. }
  13. abstract protected T handleWork(T input);
  14. }

图9-3以UML的方式阐释了责任链模式。

9.2 使用Lambda重构面向对象的设计模式 - 图3

图 9-3 责任链模式

可能你已经注意到,这就是9.2.2节介绍的模板方法设计模式。handle方法提供了如何进行工作处理的框架。不同的处理对象可以通过继承ProcessingObject类,提供handleWork方法来进行创建。

下面来看看如何使用该设计模式。你可以创建两个处理对象,它们的功能是进行一些文本处理工作。

  1. public class HeaderTextProcessing extends ProcessingObject<String> {
  2. public String handleWork(String text){
  3. return "From Raoul, Mario and Alan: " + text;
  4. }
  5. }
  6. public class SpellCheckerProcessing extends ProcessingObject<String> {
  7. public String handleWork(String text){
  8. return text.replaceAll("labda", "lambda"); ←---- 糟糕,我们漏掉了Lambda中的m字符
  9. }
  10. }

现在你可以将这两个处理对象结合起来,构造一个操作序列:

  1. ProcessingObject<String> p1 = new HeaderTextProcessing();
  2. ProcessingObject<String> p2 = new SpellCheckerProcessing();
  3. p1.setSuccessor(p2); ←---- 将两个处理对象链接起来
  4. String result = p1.handle("Aren't labdas really sexy?!!");
  5. System.out.println(result); ←---- 打印输出“From Raoul, Mario and Alan: Arent lambdas really sexy?!!”
使用Lambda表达式

稍等!这个模式看起来像是在链接(也就是构造) 函数。第3章探讨过如何构造Lambda表达式。你可以将处理对象作为Function的一个实例,或者更确切地说作为UnaryOperator的一个实例。为了链接这些函数,你需要使用andThen方法对其进行构造。

  1. UnaryOperator<String> headerProcessing =
  2. (String text) -> "From Raoul, Mario and Alan: " + text; ←---- 第一个处理对象
  3. UnaryOperator<String> spellCheckerProcessing =
  4. (String text) -> text.replaceAll("labda", "lambda"); ←---- 第二个处理对象
  5. Function<String, String> pipeline =
  6. headerProcessing.andThen(spellCheckerProcessing); ←---- 将两个方法结合起来,结果就是一个操作链
  7. String result = pipeline.apply("Aren't labdas really sexy?!!");

9.2.5 工厂模式

使用工厂模式,你无须向客户暴露实例化的逻辑就能完成对象的创建。假定你为一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。

通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法,如下所示:

  1. public class ProductFactory {
  2. public static Product createProduct(String name){
  3. switch(name){
  4. case "loan": return new Loan();
  5. case "stock": return new Stock();
  6. case "bond": return new Bond();
  7. default: throw new RuntimeException("No such product " + name);
  8. }
  9. }
  10. }

这里贷款(Loan)、股票(Stock)和债券(Bond)都是产品(Product)的子类。createProduct方法可以通过附加的逻辑来设置每个创建的产品。但是带来的好处也显而易见,你在创建对象时不用再担心会将构造函数或者配置暴露给客户,这使得客户创建产品时更加简单:

  1. Product p = ProductFactory.createProduct("loan");
使用Lambda表达式

第3章中,我们已经知道可以像引用方法一样引用构造函数。比如,下面就是一个引用贷款(Loan)构造函数的示例:

  1. Supplier<Product> loanSupplier = Loan::new;
  2. Loan loan = loanSupplier.get();

通过这种方式,你可以重构之前的代码,创建一个Map,将产品名映射到对应的构造函数:

  1. final static Map<String, Supplier<Product>> map = new HashMap<>();
  2. static {
  3. map.put("loan", Loan::new);
  4. map.put("stock", Stock::new);
  5. map.put("bond", Bond::new);
  6. }

现在,你可以像之前使用工厂设计模式那样,利用这个Map来实例化不同的产品。

  1. public static Product createProduct(String name){
  2. Supplier<Product> p = map.get(name);
  3. if(p != null) return p.get();
  4. throw new IllegalArgumentException("No such product " + name);
  5. }

这是个全新的尝试,它使用Java 8中的新特性达到了传统工厂模式同样的效果。但是,如果工厂方法createProduct需要接受多个传递给产品构造方法的参数,那这种方式的扩展性不是很好。所以除了简单的Supplier接口外,你还必须提供一个函数接口。

假设你希望保存具有三个参数(两个参数为Integer类型,一个参数为String类型)的构造函数。为了完成这个任务,你需要创建一个特殊的函数接口TriFunction。最终的结果是Map变得更加复杂。

  1. public interface TriFunction<T, U, V, R>{
  2. R apply(T t, U u, V v);
  3. }
  4. Map<String, TriFunction<Integer, Integer, String, Product>> map
  5. = new HashMap<>();

你已经了解了如何使用Lambda表达式编写和重构代码。接下来,我们会介绍如何确保新编写代码的正确性。