Java8实战-总结35

Java8实战-总结35

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

重构、测试和调试

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

工厂模式

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

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方法可以通过附加的逻辑来设置每个创建的产品。但是带来的好处也显而易见,在创建对象时不用再担心会将构造函数或者配置暴露给客户,这使得客户创建产品时更加简单:

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", Loan::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 表达式

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

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

	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方法的行为是否与预期一致:

@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对象:

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

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

@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

	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的行为:

@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方法进行测试。

@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的方法,把它当成一个函数接口,对它的功能进行测试。

你可能感兴趣的:(java,开发语言)