使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。比如,假定你为一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法,如下所示:
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
表达式,看起来不错,也很简洁。但是,大多数时候,程序开发工作的要求并不是编写优美的代码,而是编写正确的代码。
通常而言,好的软件工程实践一定少不了单元测试,借此保证程序的行为与预期一致。编写测试用例,通过这些测试用例确保你代码中的每个组成部分都实现预期的结果。比如,图形应用的一个简单的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());
}
由于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
对象类型实例compareByXAndThenY
的compare
方法进行调用,验证它们的行为是否符合预期:
@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表达式声明为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
表达式转换为方法引用(这时往往需要声明一个新的常规方法)。
接受函数作为参数的方法或者返回一个函数的方法(“高阶函数”,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
的方法,把它当成一个函数接口,对它的功能进行测试。