Java8实战-总结35

Java8实战-总结35

  • 重构、测试和调试
    • [使用 Lambda 重构面向对象的设计模式](#使用 Lambda 重构面向对象的设计模式)
    • [测试 Lambda 表达式](#测试 Lambda 表达式)
      • [测试可见 Lambda 函数的行为](#测试可见 Lambda 函数的行为)
      • [测试使用 Lambda 的方法的行为](#测试使用 Lambda 的方法的行为)
      • [将复杂的 Lambda 表达式分到不同的方法](#将复杂的 Lambda 表达式分到不同的方法)
      • 高阶函数的测试

重构、测试和调试

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

工厂模式

使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。比如,假定你为一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法,如下所示:

java 复制代码
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 Bond(); 
 			default: throw new RuntimeException("No such product " + name); 
		} 
	} 
} 

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

java 复制代码
Product p = ProductFactory.createProduct("loan"); 

使用Lambda表达式

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

java 复制代码
Supplier<Product> loanSupplier = Loan::new; 
Loan loan = loanSupplier.get(); 

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

java 复制代码
final static Map<String, Supplier<Product>> map = new HashMap<>(); 
	static { 
		map.put("loan", Loan::new); 
		map.put("stock", Stock::new); 
		map.put("bond", Bond::new); 
	} 

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

java 复制代码
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变得更加复杂。

java 复制代码
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 表达式

现在代码中已经充溢着Lambda表达式,看起来不错,也很简洁。但是,大多数时候,程序开发工作的要求并不是编写优美的代码,而是编写正确的代码。

通常而言,好的软件工程实践一定少不了单元测试,借此保证程序的行为与预期一致。编写测试用例,通过这些测试用例确保你代码中的每个组成部分都实现预期的结果。比如,图形应用的一个简单的Point类,可以定义如下:

java 复制代码
	public class Point { 
		private final int x; 
		private final int y; 
		private Point(int x, int y) { 
		this.x = x; 
		this.y = y; 
	} 
	
		public int getX() { return x; }
		public int getY() { return y; } 
		public Point moveRightBy(int x) { 
			return new Point(this.x + x, this.y); 
		} 
	} 

下面的单元测试会检查moveRightBy方法的行为是否与预期一致:

java 复制代码
@Test 
	public void testMoveRightBy() throws Exception { 
		Point p1 = new Point(5, 5); 
		Point p2 = p1.moveRightBy(10); 
		assertEquals(15, p2.getX()); 
		assertEquals(5, p2.getY()); 
	} 

测试可见 Lambda 函数的行为

由于moveRightBy方法声明为public,测试工作变得相对容易。可以在用例内部完成测试。但是Lambda并无函数名(毕竟它们都是匿名函数),因此要对代码中的Lambda函数进行测试实际上比较困难,因为无法通过函数名的方式调用它们。

有些时候,可以借助某个字段访问Lambda函数,这种情况,可以利用这些字段,通过它们对封装在Lambda函数内的逻辑进行测试。比如,假设在Point类中添加了静态字段compareByXAndThenY,通过该字段,使用方法引用可以访问Comparator对象:

java 复制代码
public class Point { 
	public final static Comparator<Point> compareByXAndThenY = comparing(Point::getX).thenComparing(Point::getY); 
}

Lambda表达式会生成函数接口的一个实例。由此,可以测试该实例的行为。这个例子中,可以使用不同的参数,对Comparator对象类型实例compareByXAndThenYcompare方法进行调用,验证它们的行为是否符合预期:

java 复制代码
@Test 
public void testComparingTwoPoints() throws Exception { 
	Point p1 = new Point(10, 15); 
	Point p2 = new Point(10, 20); 
	int result = Point.compareByXAndThenY.compare(p1 , p2); 
	assertEquals(-1, result); 
} 

测试使用 Lambda 的方法的行为

但是Lambda的初衷是将一部分逻辑封装起来给另一个方法使用。从这个角度出发,不应该将Lambda表达式声明为public,它们仅是具体的实现细节。相反,需要对使用Lambda表达式的方法进行测试。比如下面这个方法moveAllPointsRightBy

java 复制代码
	public static List<Point> moveAllPointsRightBy(List<Point> points, int x) { 
		return points.stream()
			.map(p -> new Point(p.getX() + x, p.getY()))
			.collect(toList()); 
	} 

没必要对Lambda表达式p -> new Point(p.getX() + x,p.getY())进行测试,它只是moveAllPointsRightBy内部的实现细节。更应该关注的是方法moveAllPointsRightBy的行为:

java 复制代码
@Test 
public void testMoveAllPointsRightBy() throws Exception { 
	List<Point> points = Arrays.asList(new Point(5, 5), new Point(10, 5)); 
	List<Point> expectedPoints = Arrays.asList(new Point(15, 5), new Point(20, 5)); 
	List<Point> newPoints = Point.moveAllPointsRightBy(points, 10); 
	assertEquals(expectedPoints, newPoints); 
} 

上面的单元测试中,Point类恰当地实现equals方法非常重要,否则该测试的结果就取决于Object类的默认实现

将复杂的 Lambda 表达式分到不同的方法

可能会碰到非常复杂的Lambda表达式,包含大量的业务逻辑,比如需要处理复杂情况的定价算法。无法在测试程序中引用Lambda表达式,这种情况该如何处理呢?一种策略是将Lambda表达式转换为方法引用(这时往往需要声明一个新的常规方法)。

高阶函数的测试

接受函数作为参数的方法或者返回一个函数的方法("高阶函数",higher-order function)更难测试。如果一个方法接受Lambda表达式作为参数,

可以采用的一个方案是使用不同的Lambda表达式对它进行测试。比如,可以使用不同的谓词对filter方法进行测试。

java 复制代码
@Test 
public void testFilter() throws Exception { 
	List<Integer> numbers = Arrays.asList(1, 2, 3, 4); 
	List<Integer> even = filter(numbers, i -> i % 2 == 0); 
	List<Integer> smallerThanThree = filter(numbers, i -> i < 3); 
	assertEquals(Arrays.asList(2, 4), even); 
	assertEquals(Arrays.asList(1, 2), smallerThanThree); 
}

如果被测试方法的返回值是另一个方法,该如何处理呢?可以仿照之前处理Comparator的方法,把它当成一个函数接口,对它的功能进行测试。

相关推荐
亚马逊云开发者2 小时前
RAG 向量存储月费 800 刀?S3 Vectors 直接砍到 100 出头
java
2401_895521342 小时前
springboot集成onlyoffice(部署+开发)
java·spring boot·后端
zlpzlpzyd2 小时前
groovy学习
java·jvm·学习
程序员小假2 小时前
你分得清 Prompt、Agent、Function Call、Skill、MCP 吗?
java·后端
xuboyok22 小时前
【Spring Boot】统一数据返回
java·spring boot·后端
亚马逊云开发者2 小时前
你的 AI Agent 只有鱼的记忆?聊聊 Agent 记忆管理的正确姿势
java
燕山罗成3 小时前
JAVA多线程基础
java·开发语言
予枫的编程笔记3 小时前
【面试专栏|Java并发编程】拆解Java线程生命周期:从新建到终止,再讲清进程与线程的核心差异
java·多线程·java基础·java面试·进程与线程·面试干货·java线程生命周期
Yvonne爱编码3 小时前
JAVA数据结构 DAY7-二叉树
java·开发语言·数据结构
程序媛徐师姐3 小时前
Java基于微信小程序的球馆预约系统,附源码+文档说明
java·微信小程序·球馆预约系统小程序·jav球馆预约系统小程序·java球馆预约微信小程序·球馆预约微信小程序·java球馆预约系统