《Java8实战》读书笔记07:Lambda 重构测试和调试(设计模式实现)

Posted 笑虾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java8实战》读书笔记07:Lambda 重构测试和调试(设计模式实现)相关的知识,希望对你有一定的参考价值。

《Java8实战》读书笔记07:Lambda 重构、测试和调试

第8章 重构、测试和调试

本章内容
如何使用Lambda表达式重构代码
Lambda表达式对面向对象的设计模式的影响
Lambda表达式的测试
 如何调试使用Lambda表达式和Stream API的代码

8.1 为改善可读性和灵活性重构代码

8.1.1 改善代码的可读性

为了确保你的代码能被其他人理解,有几个步骤可以尝试,比如确保你的代码附有良好的文档,并严格遵守编程规范。

  • 跟之前的版本相比较,Java 8的新特性也可以帮助提升代码的可读性:
  1. 使用Java 8,你可以减少冗长的代码,让代码更易于理解
  2. 通过方法引用Stream API,你的代码会变得更直观。这里我们会介绍三种简单的重构,利用Lambda表达式方法引用以及Stream改善程序代码的。
  • 可读性:
  1. 重构代码,用Lambda表达式取代匿名类
  2. 方法引用重构Lambda表达式
  3. Stream API重构命令式的数据处理

8.1.2 从匿名类到 Lambda 表达式的转换

注意:我们重构的目的是让代码更简洁直观,这也是我们要不要把匿名类换成Lambda的依据。(如果重构后,导致逻辑更绕,阅读成本增加就没必要了。)
Lambda可用来简化匿名类的废话代码。(比如:核心代码就一句话,为了这一句,以前非得让我实例化一个匿名类再重写方法,太多无用功。)

  • 匿名类
		Runnable r1 = new Runnable()
		    public void run()
		        System.out.println("Hello 匿名类");
		    
		;
		new Thread(r1).start();
  • Lambda
		Runnable r2 = () -> System.out.println("Hello Lambda");
		new Thread(r2).start();
  1. Lambda表达式匿名类。对此保持清醒的认识,关于作用域this的问题就不言自明了。
  2. Lambda类型根据上下文决定。如果出现重载,可以显示转换类型。例子:
    (IntelliJ支持这种重构,能自动帮你检查)
interface Task 
 public void execute(); 
 
public static void doSomething(Runnable r) r.run();  
public static void doSomething(Task a) a.execute(); 

// 这样 doSomething 就知道自己是谁了
doSomething((Task)() -> System.out.println("Danger danger!!"));

8.1.3 从 Lambda 表达式到方法引用的转换

这是一段Lambda实现的分组逻辑。将groupingBy内的业务代码提取出来改为方法引用可以让代码简洁意图清晰

        Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream()
                .collect(
                        groupingBy(dish -> 
                            if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                            else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                            else return CaloricLevel.FAT;
                        )
                );

Dish类中添加一个方法getCaloricLevel来实现groupingBy中的逻辑。

public class Dish
    // …
    public CaloricLevel getCaloricLevel()
        if (this.getCalories() <= 400) return CaloricLevel.DIET;
        else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
        else return CaloricLevel.FAT;
    

有了Dish::getCaloricLevel原来的代码就可以简化为如下效果:(取菜单流》按热量级别分组》收集到Map。代码简洁意图清晰,简直像读小说)

        Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream()
                .collect(groupingBy(Dish::getCaloricLevel));

JDK提供了大量现成工具:java.util.stream.Collectors 收集器、java.util.Comparator 比较器。《Java8实战》读书笔记04《Java8实战》读书笔记05 有相关介绍。

8.1.4 从命令式的数据处理切换到 Stream

命令式(传统)迭代方式:

        List<String> dishNames = new ArrayList<>();
        for(Dish dish: menu)
            if(dish.getCalories() > 300)
                dishNames.add(dish.getName());
            
        

Stream API隐式迭代:

        menu.parallelStream()
                .filter(d -> d.getCalories() > 300)
                .map(Dish::getName)
                .collect(toList());

哪个好阅读一目了然。但目前还有个问题就是不支持:break、continue、return 书上给了个连接说是辅助工具,但是无法访问 http://refactoring.info/tools/LambdaFicator/
个人认为换个思路:break、continue 可以通过 filter、limit 等配合实现。return 我只想到能抛异常模拟。

8.1.5 增加代码的灵活性

本节介绍了具体的方法:

1. 采用函数接口

将参数类型声明为函数接口,这样就能能给它传Lambda了。然而何时需要这样做呢?引出了两个概念:条件的延迟执行环绕执行。(本质就是TemplateMethod模板方法的思想)

2. 条件的延迟执行(别被名字唬了)

客户代码原本是每次输出日志,先判断,再打印。

if (logger.isLoggable(Log.FINER)) 
 logger.finer("Problem: " + generateDiagnostic()); 

提取为一个log函数后,每次调用,传入条件 level行为 msgSupplier。条件满足,就执行。

public void log(Level level, Supplier<String> msgSupplier) 
	if(logger.isLoggable(level)) 
		log(level, msgSupplier.get()); 
	 

logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());

3. 环绕执行(别被名字唬了)

processFile可以接收一个Lambda,并在创建BufferedReader后用这个Lambda来处理它。

    public static String processFile(BufferedReaderProcessor p) throws IOException 
        try(BufferedReader br = new BufferedReader(new FileReader("src\\\\main\\\\resources\\\\test.txt")))
            return p.process(br);
        
    

    public interface BufferedReaderProcessor
        String process(BufferedReader b) throws IOException;
    

    public static void main(String[] args) throws IOException 
        String oneLine = processFile((BufferedReader b) -> b.readLine());
        System.out.println("oneLine: " + oneLine);
        String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());
        System.out.println("twoLines: " + twoLines);
    

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

8.2.1 策略模式

准备工作

准备:

  1. 策略接口 ValidationStrategy
  2. 应用类 Validator
// 定义策略接口(只有一个方法,所以它也是一个函数式接口)
public interface ValidationStrategy 
    boolean execute(String s);


// 构造时指定策略,validate 执行策略返回结果。
public class Validator
    private final ValidationStrategy strategy;
    public Validator(ValidationStrategy v)
        this.strategy = v;
    
    public boolean validate(String s)
        return strategy.execute(s);
    

传统写法

// 实现策略 一
public class IsAllLowerCase implements ValidationStrategy 
    public boolean execute(String s)
        return s.matches("[a-z]+");
    

// 实现策略 二
public class IsNumeric implements ValidationStrategy 
    public boolean execute(String s)
        return s.matches("\\\\d+");
    

// 客户端代码
public class LambdaDemo 
    public static void main(String[] args) 
        Validator numericValidator = new Validator(new IsNumeric());
        boolean b1 = numericValidator.validate("aaaa");
        Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
        boolean b2 = lowerCaseValidator.validate("bbbb");
    
 

Lambda 写法

省了策略实现类,直接 new Validator 策略传Lambda就OK了。

public static void main(String[] args) 
	Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
	boolean b1 = numericValidator.validate("aaaa");
	Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\\\d+"));
	boolean b2 = lowerCaseValidator.validate("bbbb");

8.2.2 模板方法

如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,
那么采用模板方法设计模式是比较通用的方案。

准备工作

  1. 消费者 Customer
@Data
@AllArgsConstructor
public class Customer 
    private int id;
    private String name;

传统写法

public abstract class OnlineBanking 
    public void processCustomer(int id)
        Customer c = Database.getCustomerWithId(id); // 从数据库取
        makeCustomerHappy(c);
    
    abstract void makeCustomerHappy(Customer c);


public class BankA extends  OnlineBanking 
    @Override
    void makeCustomerHappy(Customer c) 
        System.out.println(String.format("ID: %d - %s  你好!",  c.getId(),  c.getName()));
    

客户端代码

public class LambdaDemo 
    public static void main(String[] args) 
        BankA bankA = new BankA();
        bankA.processCustomer(666);
    

// ID: 666 - 笨笨  你好!

Lambda 写法

改造processCustomer加一个参数,类型为函数式接口Consumer<T>

public class OnlineBankingLambda 
    public void processCustomer(int id, Consumer<Customer> makeCustomerHappy)
        Customer c = Database.getCustomerWithId(id); // 从数据库取
        makeCustomerHappy.accept(c);
    

使用时,第二个参数传Lambda

public class LambdaDemo 
    public static void main(String[] args) 
        OnlineBankingLambda onlineBankingLambda = new OnlineBankingLambda();
        onlineBankingLambda.processCustomer(666,  c -> 
            System.out.println(String.format("ID: %d - %s  你好!",  c.getId(),  c.getName()));
        );
    

8.2.3 观察者模式

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

书上写有的点多,我再简化一下。

准备工作

  1. 观察者接口 Observer
  2. 主题接口 Subject
  3. 主题实现 Feed
// 观察者接口
interface Observer 
    void notify(String tweet);

// 主题接口
interface Subject
    void registerObserver(Observer o); // 注册观察者
    void notifyObservers(String tweet); // 广播通知所有观察者

// 主题实现
class Feed implements Subject
    private final List<Observer> observers = new ArrayList<>();
    public void registerObserver(Observer o) 
        this.observers.add(o);
    
    public void notifyObservers(String tweet) 
        observers.forEach(o -> o.notify(tweet));
    

传统写法

实现 N 个具体的观察者。(书上还对消息内容做了判断,满足条件的消息,才处理。我这里简化了)

// 观察者 A
class ObserverA implements Observer
    public void notify(String tweet) 
        System.out.println("观察者A 收到通知:" + tweet);
    

// 观察者 B
class ObserverB implements Observer
    public void notify(String tweet) 
        System.out.println("观察者B 收到通知:" + tweet);
    

// 观察者 C
class ObserverC implements Observer
    public void notify(String tweet) 
        System.out.println("观察者C 收到通知:" + tweet);
    

客户端代码

    public static void main(String[] args) 
        Feed f = new Feed();
        f.registerObserver(new ObserverA());
        f.registerObserver(new ObserverB());
        f.registerObserver(new ObserverC());
        f.notifyObservers("大家好,我是笨笨,笨笨的笨,笨笨的笨,谢谢!");
    

Lambda 写法

无需实现几个类来用。直接传Lambda

    public static void main(String[] args) 
        Feed f = new Feed();
        f.registerObserver(tweet -> System.out.println("观察者A 收到通知:" + tweet));
        f.registerObserver(tweet -> System.out.println("观察者A 收到通知:" + tweet));
        f.registerObserver(tweet -> System.out.println("观察者A 收到通知:" + tweet));
        f.notifyObservers("大家好,我是笨笨,笨笨的笨,笨笨的笨,谢谢!");
    

8.2.4 责任链模式

责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要
在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。
通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字
段来记录后续对象。一旦对象完成它的工

以上是关于《Java8实战》读书笔记07:Lambda 重构测试和调试(设计模式实现)的主要内容,如果未能解决你的问题,请参考以下文章

《Java8实战》 - 读书笔记 - Lambda 表达式的组合用法

《Java8实战》 - 8.2.2 使用 Lambda 重构面向对象的设计模式 之 模板模式

读Java8函数式编程笔记08_测试调试和重构

《Java8实战》读书笔记13:Java8 与 Scala

《Java8实战》读书笔记13:Java8 与 Scala

《Java8实战》读书笔记11:Java8中新的日期时间API