背景
Fluent Api
最早是由Martin Fowler1提出的一种简洁的编程风格, 其每一步的执行都返回一个对象并可用于进一步的方法调用. 这样的编程风格可以很好的简化某些领域的开发, 并显著地提高代码的可读性和可维护性. 无论是在Java
的流式api中, 还是众多DLS中都有它的身影. 原因主要有以下几点:
- 代码简化: 流式编程提供了一种链式调用的方法, 可以更加灵活地将多个操作组合在一起, 减少代码量
- 代码封装: 流式编程可以将某些复杂操作封装起来, 屏蔽其实现细节, 进而带来良好的可维护性
- 延迟计算: 流式编程中往往只有到达了某个状态才开始计算, 其过程可以对执行链路进行优化, 进而带来更好的系统性能
功能实现
流式编程在java中的示例: Stream
在java中, Stream
是流式编程的一个非常典型的例子, 甚至"Stream"同"Fluent"一样都可以翻译成"流". 以下面的代码为例:
public static void main(String[] args) {int[] array = new Random().ints().limit(100).filter(x -> x > 10).sorted().toArray();System.out.println(Arrays.toString(array));}
上述例子可以非常容易地过滤掉100个小于10的随机数, 如果我们想要添加排序功能, 可以通过简单添加parallel
一个关键字实现:
public static void main(String[] args) {int[] array = new Random().ints().parallel().limit(100).filter(x -> x > 10).sorted().toArray();System.out.println(Arrays.toString(array));}
可见流式编程能够非常有效地简化代码, 进而带来非常好的可维护性.
一个订单的例子2
流式编程的本质其实是状态的转换[^3], 以餐馆销售三明治为例, 实现通过流式api购买三明治.2
餐馆的构成很简单, 可以购买三明治和饮料, 三明治和饮料又分别有不同的配料和种类; 在选择这些种类的时候应支持Fluent api
风格. 主要需要实现的有以下几点:
- 餐馆可以购买三明治, 三明治可以选择大小, 面包和馅料, 餐馆根据三明治的配料决定价格
- 餐馆可以购买饮料, 饮料可以选择种类和大小, 餐馆可以根据饮料的种类决定价格
- 客户可以购买无限多的三明治和饮料, 所有的三明治和饮料必须由上述三个部分组成
- 餐馆可以打印账单
据此可以画出状态机图:
状态机实现
根据上面状态机图, 编写相应的接口
package com.passnight.javanote.design.fluent.sandwich;public interface Order {interface SandwichOrder {interface BreadOrder {SizeOrder bread(BreadType bread);}interface SizeOrder {StyleOrder size(Size size);}interface StyleOrder {Order vegan();Order meat();}}interface DrinkOrder {interface DrinksStyleOrder {SizeOrder softDrink();SizeOrder cocktail();}interface SizeOrder {Order size(Size size);}}Order.SandwichOrder.BreadOrder sandwich();Order.DrinkOrder.DrinksStyleOrder drink();Checkout checkout();
}
状态转换的实现
编写OrderFluent
, DrinkOrderFluent
, SandwichOrderFluent
类实现上面的接口, 这里注意 DrinkOrderFluent
和SandwichOrderFluent
与OrderFluent
之间是组合的关系, 因为内部类的生命周期依赖于外部类而存在
package com.passnight.javanote.design.fluent.sandwich;import java.util.ArrayList;
import java.util.List;class OrderFluent implements Order {List<Sandwich> sandwiches = new ArrayList<>();List<Drink> drinks = new ArrayList<>();@Overridepublic SandwichOrder.BreadOrder sandwich() {return new SandwichOrderFluent();}@Overridepublic DrinkOrder.DrinksStyleOrder drink() {return new DrinkOrderFluent();}@Overridepublic Checkout checkout() {return new Checkout(sandwiches, drinks);}class DrinkOrderFluent implements Order.DrinkOrder.DrinksStyleOrder,Order.DrinkOrder.SizeOrder {private Drink drink;@Overridepublic Order.DrinkOrder.SizeOrder softDrink() {drink = new Drink();drink.setType(DrinkType.SOFT_DRINK);return this;}@Overridepublic Order.DrinkOrder.SizeOrder cocktail() {drink = new Drink();drink.setType(DrinkType.COCKTAIL);return this;}@Overridepublic Order size(Size size) {drink.setSize(size);OrderFluent.this.drinks.add(drink);return OrderFluent.this;}}class SandwichOrderFluent implements Order.SandwichOrder.BreadOrder,Order.SandwichOrder.SizeOrder,Order.SandwichOrder.StyleOrder {private Sandwich sandwich;@Overridepublic Order.SandwichOrder.SizeOrder bread(BreadType bread) {sandwich = new Sandwich();sandwich.setBread(bread);return this;}@Overridepublic Order.SandwichOrder.StyleOrder size(Size size) {sandwich.setSize(size);return this;}@Overridepublic Order vegan() {sandwich.setStyle(SandwichStyle.VEGAN);OrderFluent.this.sandwiches.add(sandwich);return OrderFluent.this;}@Overridepublic Order meat() {sandwich.setStyle(SandwichStyle.MEAT);OrderFluent.this.sandwiches.add(sandwich);return OrderFluent.this;}}
}
餐馆的实现
在定义了状态转换之后, 需要定义三明治以及饮料的种类
public enum BreadType {ITALIAN, PLAIN, GLUTEN_FREE
}public enum SandwichStyle {VEGAN, MEAT
}public enum Size {SMALL, MEDIUM, LARGE
}public enum DrinkType {SOFT_DRINK, COCKTAIL
}
之后是三明治和饮料的定义
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Sandwich {SandwichStyle style;BreadType bread;Size size;
}@Data
@AllArgsConstructor
@NoArgsConstructor
public class Drink {DrinkType type;Size size;
}
有了三明治和饮料之后, 餐厅需要有菜单标注其价格, 这里菜单使用enum
的单例模式, java虚拟机自动帮助我们完成单例对象的线程安全的懒加载
enum Menu {INSTANCE;private final Map<Size, Double> sizePrice;private final Map<SandwichStyle, Double> stylePrice;private final Map<DrinkType, Double> drinkPrice;private final Map<BreadType, Double> breadPrice;Menu() {this.sizePrice = new EnumMap<>(Size.class);this.sizePrice.put(Size.SMALL, 1d);this.sizePrice.put(Size.MEDIUM, 5d);this.sizePrice.put(Size.LARGE, 10d);this.stylePrice = new EnumMap<>(SandwichStyle.class);this.stylePrice.put(SandwichStyle.MEAT, 10d);this.stylePrice.put(SandwichStyle.VEGAN, 12d);this.drinkPrice = new EnumMap<>(DrinkType.class);this.drinkPrice.put(DrinkType.SOFT_DRINK, 1d);this.drinkPrice.put(DrinkType.COCKTAIL, 6d);this.breadPrice = new EnumMap<>(BreadType.class);this.breadPrice.put(BreadType.PLAIN, 1d);this.breadPrice.put(BreadType.ITALIAN, 2d);this.breadPrice.put(BreadType.GLUTEN_FREE, 3d);}double getPrice(DrinkType type) {return ofNullable(this.drinkPrice.get(type)).orElseThrow(() -> new IllegalArgumentException("There is not price to the drink " + type));}double getPrice(BreadType bread) {return ofNullable(this.breadPrice.get(bread)).orElseThrow(() -> new IllegalArgumentException("There is not price to the bread " + bread));}double getPrice(SandwichStyle style) {return ofNullable(this.stylePrice.get(style)).orElseThrow(() -> new IllegalArgumentException("There is not price to the sandwich style " + style));}double getPrice(Size size) {return ofNullable(this.sizePrice.get(size)).orElseThrow(() -> new IllegalArgumentException("There is not price to the size " + size));}
}
最后是订单, 可以打印最终的价格已经购买的菜品
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Checkout {private final static Menu menu = Menu.INSTANCE;List<Sandwich> sandwiches = new ArrayList<>();List<Drink> drinks = new ArrayList<>();public double checkout() {return sandwiches.stream().mapToDouble(sandwich -> menu.getPrice(sandwich.getBread()) + menu.getPrice(sandwich.getStyle()) + menu.getPrice(sandwich.getSize())).sum()+drinks.stream().mapToDouble(drink -> menu.getPrice(drink.getType()) + menu.getPrice(drink.getSize())).sum();}public String receipt() {return "sandwiches\n"+ sandwiches.stream().map(sandwich -> sandwich.toString() + ": " + (menu.getPrice(sandwich.getBread()) + menu.getPrice(sandwich.getStyle()) + menu.getPrice(sandwich.getSize()))).collect(Collectors.joining("\n"))+ "\ndrinks\n"+ drinks.stream().map(drink -> drink.toString() + ": " + (menu.getPrice(drink.getType()) + menu.getPrice(drink.getSize()))).collect(Collectors.joining("\n"));}
}
以下是一个测试类
public class Restaurant {public static void main(String[] args) {Checkout checkout = new OrderFluent().drink().cocktail().size(Size.LARGE).drink().cocktail().size(Size.MEDIUM).drink().softDrink().size(Size.SMALL).sandwich().bread(BreadType.ITALIAN).size(Size.LARGE).vegan().sandwich().bread(BreadType.GLUTEN_FREE).size(Size.LARGE).meat().sandwich().bread(BreadType.PLAIN).size(Size.LARGE).meat().checkout();System.out.println(checkout.receipt());System.out.println(checkout.checkout());}
}
运行结果如下
sandwiches
Sandwich(style=VEGAN, bread=ITALIAN, size=LARGE): 24.0
Sandwich(style=MEAT, bread=GLUTEN_FREE, size=LARGE): 23.0
Sandwich(style=MEAT, bread=PLAIN, size=LARGE): 21.0
drinks
Drink(type=COCKTAIL, size=LARGE): 16.0
Drink(type=COCKTAIL, size=MEDIUM): 11.0
Drink(type=SOFT_DRINK, size=SMALL): 2.0
97.0
引用
引用
Fluent interface - Wikipedia ↩ ↩︎
Fluent-API: Creating Easier, More Intuitive Code with a Fluent API | by Otavio Santana | xgeeks | Medium
[^ 3]: https://blog.csdn.net/significantfrank/article/details/104996419 ↩︎ ↩︎