重构、测试和调试-Java 8

概述

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

概述代码的可读性

  • 使用Java 8,你可以减少冗长的代码,让代码更易于理解。
  • 通过方法引用和Stream API,你的代码会变的更直观。
  • 重构代码,用Lambda表达式取代匿名类。
  • 用方法引用重构Lambda表达式。
  • 用Stream API重构命令式的数据处理。

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

在某些情况下,将匿名类转换为Lambda表达式可能是一个比较复杂的过程。首先,匿名类和Lambda表达式中的this和super含义是不一样的。在匿名类中,this代表的是类自身,但在Lambda表达式中,它代表的是包装类。其次,匿名类可以屏蔽包含类的变量,而Lambda表达式不能(它们会导致编译错误)。

增加代码的灵活性

我们曾经介绍过Lambda表达式有利于行为参数化。你可以使用不同的Lambda表示不同的行为,并将他们作为参数传递给函数去处理执行。这种方式可以帮助我们淡定从容地面对需求变化。

采用函数接口

首先,你必须意思到,没有函数接口,你就无法使用Lambda表达式。因此,你需要在代码中引入函数接口。

有条件的延迟执行

把判断语句封装在方法中,不要混杂在业务逻辑代码中,看起来比较乱,也不清晰。
比如:

jim.study();
String girlName = "aliy";
if(age > 18) {
	log.info("can marry, girl name is {}", girlName);
}
jim.work();

更好的方案是,不要保留年龄判断条件。

jim.study();
jim.marry("aliy");
jim.work();

这样写还是有一个问题,就是如果jim没有到18岁,它就不能和aliy结婚,无需创建aliy的名称;我应该秉承用到的使用才创建的原则,减少无用的对象创建,提高性能,可以通过lambda来延迟创建。

jim.study();
jim.marry(() -> "aliy");
jim.work();

private void marry(Supplier<String> supplier) {
	if(age > 18) {
		log.info("can marry, girl name is {}", supplier.get());
	}
}

从这里我们可以看到,如果你发现需要频繁地从客户端代码去查询一个对象的状态,只是为了传递参数、调用该对象的一个方法,那么可以考虑实现一个新的方法,以Lambda或者方法表达式作为参数,新方法在检测完该对象的状态之后才调用用来的方法。你的代码会因此变得更易读(结构更清晰),封装性更好(对象的状态也不会暴露给客户端了)。

环绕执行

我们介绍一种值得考虑的模式,那就是环绕执行。如果你发现虽然你的业务代码千差万别,但是它们用于相同的准备和清理阶段,这时,你完全可以将这部分代码用Lambda实现。这种方式的好处是可以重用准备和清理阶段的逻辑,减少重复冗余的代码。下面的这段代码,它在打开和关闭文件时使用了同样的逻辑,但在处理文件时可以使用不同的Lambda进行参数化。

String oneLine = processFile((BufferedReader b) -> b.readLine());
String towLines = processFile(b -> b.readLine() + b.readLine());
public static String processFile(BufferedReaderProcessor p) throws IOException {
	try (BufferedReader br = new BufferedReader(new File("java8inaction/chap8/data.txt"))) {
		return p.process(br);
	}
}
public interface BufferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}

这一优化是凭借函数式接口BufferedReaderProcessor达成的,通过这个接口,你可以传递各种Lambda表达式对BufferReader对象进行处理。

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

新的语言特性常常让现存的编程模式或设计黯然失色。比如,Java 5中引入了for-each循环,由于它的稳健性和简洁性,已经替代了很多显示使用迭代的情形。Java 7中推出的菱形操作符(<>)让大家在创建实例时无需显示使用泛型,一定程度上推动了java 程序员采用类型接口(type interface)进行程序设计。
对设计经验的归纳总结被称为设计模式。设计软件时,如果你愿意,可以复用这些方法来解决一些常见问题。这看起来像传统建筑工程师的工作方式,对典型的场景(比如悬挂桥、拱桥等)都定义有可重用的解决方案。例如,访问者模式常用语分离程序的算法和它的操作对象。
Lambda表达式为程序员的工具箱又新添了一件利器。它们为解决传统设计模式所面对的问题提供了新的解决方案,不但如此,采用这些方案往往更高效、更简单。使用Lambda表达式后,很多现存的略显臃肿的面向对象设计模式能够用更精简的方式实现了。这一节,我们会针对五个设计模式展开讨论,它们分别是:

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

策略模式

策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。
策略模式包含三部分内容:如下图所示:
- 一个代表某个算法的接口(它是策略模式的接口)。
- 一个或多个该接口的具体实现,它们代表了算法的多种实现。
- 一个或多个使用策略对象的客户。
在这里插入图片描述
使用Lambda表达式

public interface ValidationStrategy {
	boolean execute(String s);
}
public class Validator{
	private final ValidationStrategy strategy;
	public Validator(ValidationStrategy v){
		this.strategy = v;
	}
	public boolean validate(String s){
		return strategy.execute(s);
	}
}
Validator numericValidator = new Validator((s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");

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

模板方法

如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板设计模式时比较通用的方案。换句话说,模板方法模式是在你“希望使用这个算法,但是需要对其中的某些进行改进,才能达到希望的效果”是是非常有用的。就是算法的结构固定,需要灵活调整部分内容,类似于生命周期。
假如你需要编写一个简单的在线银行应用。通常,用户需要输入一个用户账户,之后应用才能从银行的数据库汇总得到用户的详细信息,最终完成一些让用户满意的操作。不同分行的在线银行应用让客户满意的方式可能略有不同,比如给客户的账户发放红利,或者仅仅是少发送一些推广文件。你可能通过下面的抽象类方式来实现在线银行应用:

abstract class OnlineBanking {
	public void processCustomer(int id) {
		Customer c = Database.getCustomerWithId(id);
		makeCustomerHappy(c);
	}
	abstract void makeCustomerHappy(Consumer c);
}

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

使用Lambda表达式

使用你偏爱的Lambda表达式同样可以解决这些问题。你想要插入的不同算法组件通过Lambda表达式或者方法引用的方式实现。
这里我们向processCustomer方法引入了第二个参数,它是一个Consumer类型的参数,与前文定义的makeCustomerHappy的特征保持一致:

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

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

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

观察者模式

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

在这里插入图片描述
让我们写点儿代码来看看观察者模式在实际中的都没有用。
首先,你需要一个观察者接口,它将不同的观察者聚合在一起。它仅有一个名为notify的方法,一旦接收到一条新的新闻,该方法就会被调用:

interface Observer {
	void notify(String tweet);
}
现在,你可以声明不同的观察者(比如,这里是三家不同的报纸机构),依据新闻中不同的关键字分别定义不同的行为:
class NYTimes implements Observer {
	public void notify(String tweet) {
		if(tweet != null && tweet.contains("money")) {
			System.out.println("Breaking news in NY! " + tweet);
		}
	}
}
class Guardian implements Observer{
	public void notify(String tweet) {
		if(tweet != null && tweet.contains("queen")){
			System.out.println("Yet another news in London... " + tweet);
		}
	}
}
class LeMonde implements Observer{
	public void notify(String tweet) {
		if(tweet != null && tweet.contains("wine")){
			System.out.println("Today cheese, wine and news! " + tweet);
		}
	}
}

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

interface Subject {
	void registerObserver(Observer o);
	void notifyObservers(String tweet);
}

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

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

这是一个非常直观的实现: Feed 类在内部维护了一个观察者列表,一条新?到达时,它就
进行通知。

Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favourite book is Java 8 in Action!");

使用Lambda表达式

Lambda表达式的设计初衷就是要消除僵化的代码。使用Lambda表达式后,你无需显示的实例化三个观察者对象,之间传递Lambda表达式表示需要执行的行为即可:
Feed f = new Feed();
f.registerObserver((String tweet) -> {
	if(tweet != null && tweet.contains("money")) {
		System.out.println("Breaking news in NY! " + tweet);
	}
});
f.registerObserver((String tweet) -> {
	if(tweet != null && tweet.contains("queen")){
		System.out.println("Yet another news in London... " + tweet);
	}
});
那么,是否我们随时随地都可以使用Lambda表达式呢?答案是否定的!我们前文介绍的例子中,Lambda适配的很好,那是因为需要执行的动作都很简单,因此才能很方便地消除僵化代码。但是,观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义了多个方法,诸如此类。在这些情形下,你还是应该继续使用类的方式。

责任链模式

责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要在完成一些工作后,将结果传递给另一对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。
通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交到它的后继。代码中,这段逻辑看起来是下面这样:
public abstract class ProcessingObject<T> {
	protected ProcessingObject<T> successor;
	public void setSuccessor(ProcessingObject<T> successor) {
		this.successor = successor;
	}
	public T handler(T input) {
		T r = handlerWork(input);
		if(successor != null) {
			return successor.handler(r);
		}
		return r;
	}
	
	abstract protected T handleWork(T input);
}

在这里插入图片描述
可能你已经注意到,这就是之前介绍的模板方法设计模式。handle方法提供了如何进行工作处理的框架。不同的处理对象可能通过继承ProcessingObject类,提供handleWork方法来进行创建。
示例如下:
在这里插入图片描述

使用Lambda表达式

稍等!这个模式看起来像是在链接(也即是构造)函数。我们可以将处理对象作为一个函数实例,或者更确切地说作为UnaryOperator<String>的一个实例。为了链接这些函数,你需要使用andThen方法对其进行构造。
UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan:" + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
// 将两个方法结合起来,结果就是一个操作链
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!!");

工厂模式

使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。比如,我们假定你为一家银行工作,它们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。
通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法,如下所示:
public class ProductFactory {
	public static Product createProduct(String name) {
		switch(name) {
			case "loan": return new Loan();
			case "stock": return new Stock();
			case "bond": return new Bound();
			default: throw new RuntimeException("No such product " + name);
		}
	}
}

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

Product p = ProductFactory.createProduct("loan");

使用Lambda表达式

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

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

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

final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
	map.put("loan", Load::new);
	map.put("stock", Stock::new);
	map.put("bond", Bond::new);
}

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

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

这是个全新的??,它使用Java 8中的新特性达到了传统工厂模式同样的效果。但是,如果
工厂方法 createProduct 需要接收多个传递给产?构造方法的参数,这种方式的扩展性不是很
好。你不得不提供不同的函数接口,无法采用之前统一使用一个简单接口的方式。
比如,我们假设你希望保存具有三个参数(两个参数为 Integer 类型,一个参数为 String
类型)的构造函数;为了完成这个任务,你需要创建一个特?的函数接口 TriFunction 。最终
的结果是 Map 变得更加复杂。

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

小结

  • Lambda表达式能提升代码的可读性和灵活性。
  • 如果你的代码汇总使用了匿名类,尽量使用lambda表达式替换它们,但是需要注意二者间语义的微妙差别,比如关键字this,以及变量隐藏。
  • 跟Lambda表达式比起来,方法引用的可读性更好。
  • 尽量使用Stream API替换迭代式的集合处理。
  • Lambda表达式有助于避免使用面向对象设计模式时容易出现的僵化的模板代码,典型的比如策略模式、模板方法、观察者模式、责任链模式,以及工厂模式。
  • 即使采用了Lambda表达式,也同样可以进行单元测试,但是通常你应该关注使用了Lambda表达式的方法的行为。
  • 尽量将复杂的Lambda表达式抽象到普通方法中。
  • Lambda表达式会让跟踪的分析变得更为复杂。
  • 流提供的peek方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

融极

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值