Java 设计模式最佳实践:1~5(2)https://developer.aliyun.com/article/1426754
适配器
最好的例子是使用map
函数,它执行从旧接口到新接口的自适应。我们将重用第 4 章中的示例“结构模式”,稍加改动;映射模拟适配器代码:
jshell> class PS2Device {}; | created class PS2Device jshell> class USBDevice {}; | created class USBDevice jshell> Optional.of(new PS2Device()).stream().map(x -> new USBDevice()).findFirst().get() $39 ==> USBDevice@15bb6bea
装饰器
装饰器可以通过利用函数组合来实现。例如,如前所示,可以使用stream.peek
方法将日志添加到现有函数调用,并从提供给peek
的Consumer
将日志记录到控制台。
我们的第 4 章“结构模式”,装饰器示例可以用函数式重写;注意装饰器用于使用与初始装饰器消费者相同的输入:
jshell> Consumer<String> toASCII = x -> System.out.println("Print ASCII: " + x); toASCII ==> $Lambda$159/1690859824@400cff1a jshell> Function<String, String> toHex = x -> x.chars().boxed().map(y -> "0x" + Integer.toHexString(y)).collect(Collectors.joining(" ")); toHex ==> $Lambda$158/1860250540@55040f2f jshell> Consumer<String> decorateToHex = x -> System.out.println("Print HEX: " + toHex.apply(x)) decorateToHex ==> $Lambda$160/1381965390@75f9eccc jshell> toASCII.andThen(decorateToHex).accept("text") Print ASCII: text Print HEX: 0x74 0x65 0x78 0x74
责任链
责任链可以实现为处理器(函数)的列表,每个处理器执行一个特定的操作。下面的示例代码使用闭包和一系列函数,这些函数一个接一个地应用于给定的文本:
jshell> String text = "Text"; text ==> "Text" jshell> Stream.<Function<String, String>>of(String::toLowerCase, x -> LocalDateTime.now().toString() + " " + x).map(f -> f.apply(text)).collect(Collectors.toList()) $55 ==> [text, 2017-08-10T08:41:28.243310800 Text]
命令
其目的是将一个方法转换成一个对象来存储它并在以后调用它,能够跟踪它的调用、记录和撤消。这是Consumer
类的基本用法。
在下面的代码中,我们将创建一个命令列表并逐个执行它们:
jshell> List<Consumer<String>> tasks = List.of(System.out::println, x -> System.out.println(LocalDateTime.now().toString() + " " + x)) tasks ==> [$Lambda$192/728258269@6107227e, $Lambda$193/1572098393@7c417213] jshell> tasks.forEach(x -> x.accept(text)) Text 2017-08-10T08:47:31.673812300 Text
解释器
解释器的语法可以存储为关键字映射,相应的操作存储为值。在第二章“创建模式”中,我们使用了一个数学表达式求值器,将结果累加成一个栈。这可以通过将表达式存储在映射中来实现,并使用reduce
来累加结果:
jshell> Map<String, IntBinaryOperator> operands = Map.of("+", (x, y) -> x + y, "-", (x, y) -> x - y) operands ==> {-=$Lambda$208/1259652483@65466a6a, +=$Lambda$207/1552978964@4ddced80} jshell> Arrays.asList("4 5 + 6 -".split(" ")).stream().reduce("0 ",(acc, x) -> { ...> if (operands.containsKey(x)) { ...> String[] split = acc.split(" "); ...> System.out.println(acc); ...> acc = split[0] + " " + operands.get(x).applyAsInt(Integer.valueOf(split[1]), Integer.valueOf(split[2])) + " "; ...> } else { acc = acc + x + " ";} ...> return acc; }).split(" ")[1] 0 4 5 0 9 6 $76 ==> "3"
迭代器
迭代器部分是通过使用流提供的序列来实现的。Java8 添加了forEach
方法,该方法接收消费者作为参数,其行为与前面的循环实现类似,如下面的示例代码所示:
jshell> List.of(1, 4).forEach(System.out::println) jshell> for(Integer i: List.of(1, 4)) System.out.println(i);
如预期的那样,每个示例的输出是 1 和 4。
观察者
在 Java8 中,观察者模式被 Lambda 表达式取代。最明显的例子是ActionListener
替换。使用匿名类监听器的旧代码被替换为一个简单的函数调用:
JButton button = new Jbutton("Click Here"); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Handled by the old listener"); } });
新代码只有一行:
button.addActionListener(e -> System.out.println("Handled by lambda"));
策略
这个策略可以被一个函数代替。在下面的代码示例中,我们对所有价格应用 10% 的折扣策略:
jshell> Function<Double, Double> tenPercentDiscount = x -> x * 0.9; tenPercentDiscount ==> $Lambda$217/1990160809@4c9f8c13 jshell> List.<Double>of(5.4, 6.27, 3.29).stream().map(tenPercentDiscount).collect(Collectors.toList()) $98 ==> [4.86, 5.643, 2.9610000000000003]
模板方法
当模板提供调用顺序时,可以实现模板方法以允许注入特定的方法调用。在下面的示例中,我们将添加特定的调用并从外部设置它们的内容。它们可能已经插入了特定的内容。通过使用接收所有可运行项的单个方法,可以简化代码:
jshell> class TemplateMethod { ...> private Runnable call1 = () -> {}; ...> private Runnable call2 = () -> System.out.println("Call2"); ...> private Runnable call3 = () -> {}; ...> public void setCall1(Runnable call1) { this.call1 = call1;} ...> public void setCall2(Runnable call2) { this.call2 = call2; } ...> public void setCall3(Runnable call3) { this.call3 = call3; } ...> public void run() { ...> call1.run(); ...> call2.run(); ...> call3.run(); ...> } ...> } | created class TemplateMethod jshell> TemplateMethod t = new TemplateMethod(); t ==> TemplateMethod@70e8f8e jshell> t.setCall1(() -> System.out.println("Call1")); jshell> t.setCall3(() -> System.out.println("Call3")); jshell> t.run(); Call1 Call2 Call3
函数式设计模式
在本节中,我们将学习以下函数式设计模式:
- 映射和归约
- 借贷模式
- 尾部调用优化
- 回忆录
- 环绕执行方法
映射和归约
MapReduce 是 Google 开发的一种用于大规模并行编程的技术,由于易于表达,它以函数设计模式出现。在函数式编程中,它是单子的一种形式。
意图
其目的是将现有任务分解为多个较小的任务,并行运行它们,并聚合结果(reduce
)。它有望提高大数据的性能。
示例
我们将通过基于给定的 Sleuth 跨度解析和聚合来自多个 Web 服务的日志并计算每个命中端点的总持续时间来演示 MapReduce 模式的用法。日志取自这个页面并拆分成相应的服务日志文件。下面的代码并行读取所有日志、映射、排序和过滤相关日志条目,收集并减少(聚合)结果。如果有结果,它将被打印到控制台。导入的日期/时间类用于排序比较。flatMap
代码需要处理Exception
,如下代码所示:
jshell> import java.time.* jshell> import java.time.format.* jshell> DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") dtf ==> Value(YearOfEra,4,19,EXCEEDS_PAD)'-'Value(MonthOf ... Fraction(NanoOfSecond,3,3) jshell> try (Stream<Path> files = Files.find(Paths.get("d:/"), 1, (path, attr) -> String.valueOf(path).endsWith(".log"))) { ...> files.parallel(). ...> flatMap(x -> { try { return Files.lines(x); } catch (IOException e) {} return null;}). ...> filter(x -> x.contains("2485ec27856c56f4")). ...> map(x -> x.substring(0, 23) + " " + x.split(":")[3]). ...> sorted((x, y) -> LocalDateTime.parse(x.substring(0, 23), dtf).compareTo(LocalDateTime.parse(y.substring(0, 23), dtf))). ...> collect(Collectors.toList()).stream().sequential(). ...> reduce((acc, x) -> { ...> if (acc.length() > 0) { ...> Long duration = Long.valueOf(Duration.between(LocalDateTime.parse(acc.substring(0, 23), dtf), LocalDateTime.parse(x.substring(0, 23), dtf)).t oMillis()); ...> acc += "n After " + duration.toString() + "ms " + x.substring(24); ...> } else { ...> acc = x; ...> } ...> return acc;}).ifPresent(System.out::println); ...> } 2016-02-26 11:15:47.561 Hello from service1\. Calling service2 After 149ms Hello from service2\. Calling service3 and then service4 After 334ms Hello from service3 After 363ms Got response from service3 [Hello from service3] After 573ms Hello from service4 After 595ms Got response from service4 [Hello from service4] After 621ms Got response from service2 [Hello from service2, response from service3 [Hello from service3] and from service4 [Hello from service4]]
借贷模式
借贷模式确保资源一旦超出范围就被决定性地处置。资源可以是数据库连接、文件、套接字或任何处理本机资源的对象(内存、系统句柄、任何类型的连接)之一。这与 MSDN 上描述的 Dispose 模式的意图类似。
意图
这样做的目的是让用户在未使用的资源被使用后,从释放这些资源的负担中解脱出来。用户可能忘记调用资源的release
方法,从而导致泄漏。
示例
在处理数据库事务时,最常用的模板之一是获取事务、进行适当的调用、确保在异常时提交或回滚并关闭事务。这可以实现为借贷模式,其中移动部分是事务中的调用。以下代码显示了如何实现这一点:
jshell> class Connection { ...> public void commit() {}; public void rollback() {}; public void close() {}; public void setAutoCommit(boolean autoCommit) {}; ...> public static void runWithinTransaction(Consumer<Connection> c) { ...> Connection t = null; ...> try { t = new Connection(); t.setAutoCommit(false); ...> c.accept(t); ...> t.commit(); ...> } catch(Exception e) { t.rollback(); } finally { t.close(); } } } | created class Connection jshell> Connection.runWithinTransaction(x -> System.out.println("Execute statement...")); Execute statement...
Java 设计模式最佳实践:1~5(4)https://developer.aliyun.com/article/1426756