简化 Java 代码 ——(一)使用 PropertyMapper

1 前言

在日常的开发中,我们需要使用到各种非空,非 Null 等条件判定以保证程序不出错,因此避免不了写出臃肿的代码。尽管 JDK 8 提供了强大的 Stream 流,但它并不总是能满足各种需求。

网络上对于 PropertyMapper 类的研究甚少,写这篇文章也是为了记下所学知识,同时也希望给大家提供一个另类的思路,以简化日常开发。

2 Spring 与 PropertyMapper

PropertyMapper 是 Spring 框架中的工具类,广泛应用于框架的各处,作为一个程序猿,我们不应该局限于使用框架本身,更应从里边学到各种程序设计。

3 使用 PropertyMapper

让我们从规定一个实体类 Container 开始:

 1 /**
 2  * 容器类
 3  *
 4  * @author pancc
 5  * @version 1.0
 6  */
 7 @Data
 8 @Accessors(chain = true)
 9 public class Container {
10     /**
11      * 容器的位置,可以为  null 或者空字符串
12      */
13     private String location;
14     /**
15      * 容器中的所有数值,可以为 null 或者空
16      */
17     private Collection<Integer> integers;
18 }

对于这个实体类的字段,我们需要打印 location 去空之后的结果,容器内数值的二进制值。并且这个容器在方法参数中可能为空,对于一般的写法:

 1     private void normal(@Nullable Container container) {
 2         String local = "";
 3         List<String> binaries = new ArrayList<>();
 4 
 5         if (container != null) {
 6             if (container.getLocation() != null) {
 7                 local = container.getLocation().trim();
 8             }
 9             if (container.getIntegers() != null) {
10                 container.getIntegers().stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries));
11             }
12         }
13 
14         System.out.println("local = " + local);
15         System.out.println("binaries = " + binaries);
16     }

上边的尽管结合了 Stream ,但如果处理参数变得复杂,程序将会变得冗长不可读。使用 PropertyMapper 可以极大地改善:

 1     private void mapper(@Nullable Container container) {
 2         StringBuilder local = new StringBuilder();
 3         List<String> binaries = new ArrayList<>();
 4 
 5         PropertyMapper mapper = PropertyMapper.get();
 6         mapper.from(container).whenNonNull().as(Container::getLocation).whenNonNull().as(String::trim).to(local::append);
 7         mapper.from(container).whenNonNull().as(Container::getIntegers).whenNonNull().to(is -> is.stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries)));
 8 
 9         System.out.println("local = " + local.toString());
10         System.out.println("binaries = " + binaries);
11     }

4 PropertyMapper 的核心解读

4.1 核心

PropertyMapper 类主要有四大核心:

  1. Source<T> :一个存储提供值的 Supplier<T> 与调用方法时对 Supplier 进行合法测试的 Predicate<T>;
  2. SourceOperator:对 Source<T> 实例进行处理的 lambda 接口,与 JDK 8 的 Function 接口不同的是它的返回值总是与入参相同
  3. NullPointerExceptionSafeSupplier<T>:特殊的 Supplier,供 whenNonNull 方法调用以避免空指针(返回 null 值)
  4. CachingSupplier<T>:缓存 PropertyMapper 实例中的 Supplier<T> 的返回值 result (保存在上次调用的父实例中)

4.2 细节解读

4.2.1 静态入口方法 PropertyMapper::get

PropertyMapper 类内部维护一个静态实例,我们一开始只能通过获取它得到 PropertyMapper 实例。从类的构造方法来看,父实例与 SourceOperator  都不存在。

4.2.2 实例方法 PropertyMapper::from

向 PropertyMapper 传递初始值的方法有两种,from(T value) from(Supplier<T> supplier) ,前者实际上是后者的包装,所以我们只看后者的实现。让我们看一下内部的流程:

from(Supplier<T> supplier) 方法首先调用 getSource 获得  Source<T> 实例。另,见 5.1 中的 sourceOperator 操作(需完成第 4 点的阅读)。

getSource 首先会检查父 PropertyMapper 是否存在,如果存在则继承父的 Source<T> 实例(对应上边 3 点中代码第二次调用 mapper.from(container) 方法),否则新建一个 CachingSupplier<T> 接收 from 方法传进来的 Supplier<T> 。可以看到此时于 Source<T>  的第二个构造参数 Predicate<T> 总是返回 true.

  

4.2.3 实例方法 Source::to

方法逻辑很简单,首先从由 PropertyMapper 传递进来的 CachingSupplier<T> 中获得值,然后使用内部的 Predicate<T> 对该值进行检查,如果检测通过则执行 Comsumer<T> 方法。

4.2.4 实例方法 Source::toInstance

与实例方法 to(Consumer<T> consumer) 相似但在判断有所不同,实例方法 toInstance(Function<T, R> factory) 首先检测值的合法与否,合法则进行转换并返回转换结果,否则抛出错误。实际开发中用到这个方法的场景可以说是没有。

4.2.5 实例方法 Source::toCall

toCall(Runnable runnable) 方法的代码不贴出,细节很简单,当值合法,则执行参数 Runnable 

4.2.6 实例方法 Source::as

as 方法首先检查值是否合法,如果合法则对值进行转换,非法则将值设为 null,Predicate<T> 则继续传递。

4.2.7 实例方法 Source::asInt

asInt 是对 as 方法的双重调用,参数 Function<T, R> 需要负责映射当前值 T 到 Number

4.2.8 实例方法 Source::whenNonNull

whenNonNull 方法继承了原有实例的 Supplier<T> 字段,覆盖了原有的 Predicate<T> 字段以供后续值判断或者条件附加使用。

4.2.10 实例方法 Source::when

when 方法是最灵活的,它接收一个 Predicate<T> 参数,用来组合自身持有的 Predicate<T> 字段(如果有的话)。以 when  方法为基础的 whenEqualTo,whenFalse,whenHasText,whenNot,whenTrue 都是对它的进一步包装,因此不展开。

4.2.11 实例方法 Source::whenInstanceOf

这个方法实际上组合了 when 方法 与 as 方法,因此具有判断与转换的双重方法,需要格外注意。

5 额外的补充

5.1 实例方法 PropertyMapper::alwaysApplying

alwaysApplying 用于向 PropertyMapper 添加一个 when 条件判定,这个判定在次 from 方法中都被调用(见 4.2.2 )。alwaysApplyingWhenNonNull 则是对这个方法的包装。

6 PropertyMapper 的副作用

  1. 内存/时间开销:由于每次调用方法都返回一个新的 PropertyMapper 实例,在压测中有明显的性能影响 (对于附录的代码,常规与简化方式的执行时间对比是 7ms:48ms)。
  2. 同一个对象不可复用:从上边的例子中可以看到,对于常规的 if 判断,后边都可以享受到判断结果,但是在 PropertyMapper 中一般则不可,套用则有可能影响阅读(可看后边的代码)。

  

7 附 代码

 1 /**
 2  * @author pancc
 3  * @version 1.0
 4  */
 5 public class PropertyTest {
 6 
 7 
 8     private static Container allNullContainer;
 9     private static Container container;
10 
11     @BeforeAll
12     static void initContainer() {
13         allNullContainer = new Container().setLocation(null).setIntegers(null);
14         container = new Container().setLocation(" xs").setIntegers(IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList()));
15     }
16 
17     @Test
18     public void testNormal() {
19         normal(allNullContainer);
20         normal(container);
21     }
22 
23     @Test
24     void testMapper() {
25         mapper(allNullContainer);
26         mapper(container);
27     }
28 
29     private void normal(@Nullable Container container) {
30         String local = "";
31         List<String> binaries = new ArrayList<>();
32 
33         if (container != null) {
34             if (container.getLocation() != null) {
35                 local = container.getLocation().trim();
36             }
37             if (container.getIntegers() != null) {
38                 container.getIntegers().stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries));
39             }
40         }
41 
42         System.out.println("local = " + local);
43         System.out.println("binaries = " + binaries);
44     }
45 
46     private void mapper(@Nullable Container container) {
47         StringBuilder local = new StringBuilder();
48         List<String> binaries = new ArrayList<>();
49 
50         PropertyMapper mapper = PropertyMapper.get();
51 
52 /*        mapper.from(container).whenNonNull().to(c -> {
53             mapper.from(c.getLocation()).whenNonNull().as(String::trim).to(local::append);
54             mapper.from(c.getIntegers()).whenNonNull().to(is -> is.stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries)));
55         });*/
56         mapper.from(container).whenNonNull().as(Container::getLocation).whenNonNull().as(String::trim).to(local::append);
57         mapper.from(container).whenNonNull().as(Container::getIntegers).whenNonNull().to(is -> is.stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries)));
58 
59         System.out.println("local = " + local.toString());
60         System.out.println("binaries = " + binaries);
61     }
62 }

CachingSupplier<T>

原文地址:https://www.cnblogs.com/siweipancc/p/simplify_java_code_one_using_PropertyMapper.html