背景
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<>();
@Override
public SandwichOrder.BreadOrder sandwich() {
return new SandwichOrderFluent();
}
@Override
public DrinkOrder.DrinksStyleOrder drink() {
return new DrinkOrderFluent();
}
@Override
public Checkout checkout() {
return new Checkout(sandwiches, drinks);
}
class DrinkOrderFluent implements Order.DrinkOrder.DrinksStyleOrder,
Order.DrinkOrder.SizeOrder {
private Drink drink;
@Override
public Order.DrinkOrder.SizeOrder softDrink() {
drink = new Drink();
drink.setType(DrinkType.SOFT_DRINK);
return this;
}
@Override
public Order.DrinkOrder.SizeOrder cocktail() {
drink = new Drink();
drink.setType(DrinkType.COCKTAIL);
return this;
}
@Override
public 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;
@Override
public Order.SandwichOrder.SizeOrder bread(BreadType bread) {
sandwich = new Sandwich();
sandwich.setBread(bread);
return this;
}
@Override
public Order.SandwichOrder.StyleOrder size(Size size) {
sandwich.setSize(size);
return this;
}
@Override
public Order vegan() {
sandwich.setStyle(SandwichStyle.VEGAN);
OrderFluent.this.sandwiches.add(sandwich);
return OrderFluent.this;
}
@Override
public 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 ↩︎ ↩︎