使用 Fluent API 创建更简单、更直观的代码
我们知道,在软件项目中,没有什么能取代好的文档。但是,也需要注意写出的代码有多直观。毕竟,代码越简单自然,用户体验就越好。
在简单的“编程规则”中,我们将忘记我们必须记住的一切,“强制”你记住的 API 是失败的关键证明。
这就是为什么在本文中,我们将介绍该主题并向你展示如何从 Fluent-API 概念创建流体 API。
什么是 Fluent-API?
当我们在软件工程的上下文中谈论时,fluent-API 是一种面向对象的 API,其设计主要基于方法链。
这个概念由Eric Evans和Martin Fowler于 2005 年创建,旨在通过创建特定领域语言 ( DSL )来提高代码可读性。
在实践中,创建一个流畅的 API 意味着开发一个 API,其中不需要记住接下来的步骤或方法,允许一个自然连续的序列,就好像它是一个选项菜单。
这种自然的节奏与餐厅甚至快餐连锁店的工作方式类似,因为当您将一道菜放在一起时,选项会根据你所做的选择而有所不同。例如,如果你选择鸡肉三明治,则会根据所选菜肴等建议配菜。
Java 上下文中的 Fluent API
在 Java 世界中,我们可以想到此类实现的两个著名示例。
第一个是JOOQ
框架,这是一个由Lukas Eder领导的项目,它促进了 Java 和关系数据库之间的通信。JOOQ 最显着的区别在于它是面向数据的,这有助于避免和/或减少与关系和面向对象相关的阻抗问题或损失。
Query query = create.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
.from(BOOK)
.join(AUTHOR)
.on(BOOK.AUTHOR_ID.eq(AUTHOR.ID))
.where(BOOK.PUBLISHED_IN.eq(1948));
String sql = query.getSQL();
List<Object> bindValues = query.getBindValues();
另一个例子是在企业 Java 世界规范内的非关系数据库,即 NoSQL。其中包括Jakarta EE,它是同类中的第一个规范,并成为Eclipse Foundation旗下的Jakarta NoSQL。
本规范的目的是确保 Java 和 NoSQL 数据库之间的顺畅通信。
DocumentQuery query = select().from("Person").where(eq(Document.of("_id", id))).build();
Optional<Person> person = documentTemplate.singleResult(query);
System.out.println("Entity found: " + person);
一般来说,一个 fluent API 分为三个部分:
- 最终的对象或结果:总的来说,fluent-API 类似于构建器模式,但最强大的动态与 DSL 相结合。在这两种情况下,结果往往是代表流程或新实体结果的实例。
- 选项:在这种情况下,是将用作“我们的交互式菜单”的接口或类的集合。从一个动作来看,这个想法是按照直观的顺序只显示下一步可用的选项。
- 结果:在所有这个过程之后,答案可能会或可能不会导致实体、策略等的实例。关键点是结果必须是有效的。
流体 API 实践
为了演示这一概念,我们将创建一个三明治订单,其中包含具有相应购买价格的订单的预期结果。流程如下所示。
当然,有多种方法可以实现这种流畅的 API 功能,但我们选择了一个简短的版本。
正如我们已经提到的 API 的三个部分——对象、选项和结果——我们将从“订单”接口将表示的顺序开始。一个亮点是这个界面有一些界面,它们将负责展示我们的选项。
public interface Order {
interface SizeOrder {
StyleOrder size(Size size);
}
interface StyleOrder {
StyleQuantityOrder vegan();
StyleQuantityOrder meat();
}
interface StyleQuantityOrder extends DrinksOrder {
DrinksOrder quantity(int quantity);
}
interface DrinksOrder {
Checkout softDrink(int quantity);
Checkout cocktail(int quantity);
Checkout softDrink();
Checkout cocktail();
Checkout noBeveragesThanks();
}
static SizeOrder bread(Bread bread) {
Objects.requireNonNull(bread, "Bread is required o the order");
return new OrderFluent(bread);
}
这个 API 的结果将是我们的订单类。它将包含三明治、饮料及其各自的数量。
在我们返回教程之前的快速附加组件
我们不会在本文中关注但值得一提的一点与货币的表示有关。
当涉及到数值运算时,最好使用 BigDecimal。那是因为,根据Java Effective书籍和博客When Make a Type 之类的参考资料,我们了解到复杂类型需要唯一的类型。这种推理,再加上“不要重复自己”的实用主义,结果就是使用了 Java 货币规范:The Money API
。
import javax.money.MonetaryAmount;
import java.util.Optional;
public class Checkout {
private final Sandwich sandwich;
private final int quantity;
private final Drink drink;
private final int drinkQuantity;
private final MonetaryAmount total;
//...
}
旅程的最后一步是 API 实现。它将负责代码的“丑陋”部分,使 API 看起来很漂亮。
由于我们不使用数据库或其他数据引用,因此价格表将直接放置在代码中,并且我们打算使示例尽可能简单。但值得强调的是,在自然环境中,这些信息会存在于数据库或服务中。
import javax.money.MonetaryAmount;
import java.util.Objects;
class OrderFluent implements Order.SizeOrder, Order.StyleOrder, Order.StyleQuantityOrder, Order.DrinksOrder {
private final PricingTables pricingTables = PricingTables.INSTANCE;
private final Bread bread;
private Size size;
private Sandwich sandwich;
private int quantity;
private Drink drink;
private int drinkQuantity;
OrderFluent(Bread bread) {
this.bread = bread;
}
@Override
public Order.StyleOrder size(Size size) {
Objects.requireNonNull(size, "Size is required");
this.size = size;
return this;
}
@Override
public Order.StyleQuantityOrder vegan() {
createSandwich(SandwichStyle.VEGAN);
return this;
}
@Override
public Order.StyleQuantityOrder meat() {
createSandwich(SandwichStyle.MEAT);
return this;
}
@Override
public Order.DrinksOrder quantity(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("You must request at least one sandwich");
}
this.quantity = quantity;
return this;
}
@Override
public Checkout softDrink(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("You must request at least one sandwich");
}
this.drinkQuantity = quantity;
this.drink = new Drink(DrinkType.SOFT_DRINK, pricingTables.getPrice(DrinkType.SOFT_DRINK));
return checkout();
}
@Override
public Checkout cocktail(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("You must request at least one sandwich");
}
this.drinkQuantity = quantity;
this.drink = new Drink(DrinkType.COCKTAIL, pricingTables.getPrice(DrinkType.COCKTAIL));
return checkout();
}
@Override
public Checkout softDrink() {
return softDrink(1);
}
@Override
public Checkout cocktail() {
return cocktail(1);
}
@Override
public Checkout noBeveragesThanks() {
return checkout();
}
private Checkout checkout() {
MonetaryAmount total = sandwich.getPrice().multiply(quantity);
if (drink != null) {
MonetaryAmount drinkTotal = drink.getPrice().multiply(drinkQuantity);
total = total.add(drinkTotal);
}
return new Checkout(sandwich, quantity, drink, drinkQuantity, total);
}
private void createSandwich(SandwichStyle style) {
MonetaryAmount breadPrice = pricingTables.getPrice(this.bread);
MonetaryAmount sizePrice = pricingTables.getPrice(this.size);
MonetaryAmount stylePrice = pricingTables.getPrice(SandwichStyle.VEGAN);
MonetaryAmount total = breadPrice.add(sizePrice).add(stylePrice);
this.sandwich = new Sandwich(style, this.bread, this.size, total);
}
}
结果是一个 API,它将直接直观地将请求返回给我们。
Checkout checkout = Order.bread(Bread.PLAIN)
.size(Size.SMALL)
.meat()
.quantity(2)
.softDrink(2);
Fluent API 与其他模式有何不同?
对两种 API 标准进行比较是很普遍的,它们是 Builder 和 Fluent-API。原因是它们在创建实例的过程中都按顺序使用方法。
但是,Fluent-API 是“与 DSL 相关联的”,它强制采用一种简单的方法来实现这一点。但为了使这些差异更加明显,我们为每个模式分别列出了亮点:
Builder 模式:
- 它往往更容易实施;
- 不清楚需要哪些施工方法;
- 绝大多数问题都会在运行时发生;
- 一些工具和框架会自动创建它;
- 它需要在 build 方法中进行更健壮的验证,以检查哪些强制方法没有被调用。
流利的API:
- 重要的是,对于每个方法,都有验证,如果参数无效则抛出错误,记住快速失败的前提;
- 它必须在过程结束时返回一个有效的对象。
现在,是否更容易理解模式之间的异同?
这就是我们对 fluent-API 概念的介绍。与所有解决方案一样,没有“灵丹妙药”,因为整个过程通常不合理。
它是一个出色的工具,有助于为你和其他用户创建故障保护。