使用 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 概念的介绍。与所有解决方案一样,没有“灵丹妙药”,因为整个过程通常不合理。
它是一个出色的工具,有助于为你和其他用户创建故障保护。
Fluent API 是由 Eric Evans 和 Martin Fowler 在 2005 年提出的,它是一种面向对象的 API,其设计广泛地依赖于 method chaining。它的目标是通过创建特定于某个领域的语言(domain-specific language, DSL)来提高代码的可读性
https://www.martinfowler.com/bliki/FluentInterface.html
Fluent API 模式
前文中,不管是传统的建造者模式,还是 Functional Options 模式,我们都没有限定属性的构建顺序,比如:
// 传统建造者模式不限定属性的构建顺序
profile := NewServiceProfileBuilder().
WithPriority(1). // 先构建Priority也完全没问题
WithId("service1").
...
// Functional Options 模式也不限定属性的构建顺序
profile := NewServiceProfile("service1", "order",
Priority(1), // 先构建Priority也完全没问题
Status(Normal),
...
但是在一些特定的场景,对象的属性是要求有一定的构建顺序的,如果违反了顺序,可能会导致一些隐藏的错误。
当然,我们可以与使用者的约定好属性构建的顺序,但这种约定是不可靠的,你很难保证使用者会一直遵守该约定。所以,更好的方法应该是通过接口的设计来解决问题, Fluent API 模式 诞生了。
下面,我们使用 Fluent API 模式进行实现:
// demo/service/registry/model/service_profile_fluent_api.go
type (
// 关键点1: 为ServiceProfile定义一个Builder对象
fluentServiceProfileBuilder struct {
// 关键点2: 将ServiceProfile作为Builder的成员属性
profile *ServiceProfile
}
// 关键点3: 定义一系列构建属性的fluent接口,通过方法的返回值控制属性的构建顺序
idBuilder interface {
WithId(id string) typeBuilder
}
typeBuilder interface {
WithType(svcType ServiceType) statusBuilder
}
statusBuilder interface {
WithStatus(status ServiceStatus) endpointBuilder
}
endpointBuilder interface {
WithEndpoint(ip string, port int) regionBuilder
}
regionBuilder interface {
WithRegion(regionId, regionName, regionCountry string) priorityBuilder
}
priorityBuilder interface {
WithPriority(priority int) loadBuilder
}
loadBuilder interface {
WithLoad(load int) endBuilder
}
// 关键点4: 定义一个fluent接口返回完成构建的ServiceProfile,在最后调用链的最后调用
endBuilder interface {
Build() *ServiceProfile
}
)
// 关键点5: 为Builder定义一系列构建方法,也即实现关键点3中定义的Fluent接口
func (f *fluentServiceProfileBuilder) WithId(id string) typeBuilder {
f.profile.Id = id
return f
}
func (f *fluentServiceProfileBuilder) WithType(svcType ServiceType) statusBuilder {
f.profile.Type = svcType
return f
}
func (f *fluentServiceProfileBuilder) WithStatus(status ServiceStatus) endpointBuilder {
f.profile.Status = status
return f
}
func (f *fluentServiceProfileBuilder) WithEndpoint(ip string, port int) regionBuilder {
f.profile.Endpoint = network.EndpointOf(ip, port)
return f
}
func (f *fluentServiceProfileBuilder) WithRegion(regionId, regionName, regionCountry string) priorityBuilder {
f.profile.Region = &Region{
Id: regionId,
Name: regionName,
Country: regionCountry,
}
return f
}
func (f *fluentServiceProfileBuilder) WithPriority(priority int) loadBuilder {
f.profile.Priority = priority
return f
}
func (f *fluentServiceProfileBuilder) WithLoad(load int) endBuilder {
f.profile.Load = load
return f
}
func (f *fluentServiceProfileBuilder) Build() *ServiceProfile {
return f.profile
}
// 关键点6: 定义一个实例化Builder对象的工厂方法
func NewFluentServiceProfileBuilder() idBuilder {
return &fluentServiceProfileBuilder{profile: &ServiceProfile{}}
}
实现 Fluent API 模式有 6 个关键点,大部分与传统的建造者模式类似:
- 为
ServiceProfile
定义一个 Builder 对象fluentServiceProfileBuilder
。 - 把需要构建的
ServiceProfile
设计为 Builder 对象fluentServiceProfileBuilder
的成员属性。 - 定义一系列构建属性的 Fluent 接口,通过方法的返回值控制属性的构建顺序,这是实现 Fluent API 的关键。比如
WithId
方法的返回值是typeBuilder
类型,表示紧随其后的就是WithType
方法。 - 定义一个 Fluent 接口(这里是
endBuilder
)返回完成构建的ServiceProfile
,在最后调用链的最后调用。 - 为 Builder 定义一系列构建方法,也即实现关键点 3 中定义的 Fluent 接口,并在构建方法中返回 Builder 对象指针本身。
- 定义一个实例化 Builder 对象的工厂方法
NewFluentServiceProfileBuilder()
,返回第一个 Fluent 接口,这里是idBuilder
,表示首先构建的是Id
属性。
Fluent API 的使用与传统的建造者实现使用类似,但是它限定了方法调用的顺序。如果顺序不对,在编译期就报错了,这样就能提前把问题暴露在编译器,减少了不必要的错误使用。
// Fluent API的使用方法
profile := NewFluentServiceProfileBuilder().
WithId("service1").
WithType("order").
WithStatus(Normal).
WithEndpoint("192.168.0.1", 8080).
WithRegion("region1", "beijing", "China").
WithPriority(1).
WithLoad(100).
Build()
// 如果方法调用不按照预定的顺序,编译器就会报错
profile := NewFluentServiceProfileBuilder().
WithType("order").
WithId("service1").
WithStatus(Normal).
WithEndpoint("192.168.0.1", 8080).
WithRegion("region1", "beijing", "China").
WithPriority(1).
WithLoad(100).
Build()
// 上述代码片段把WithType和WithId的调用顺序调换了,编译器会报如下错误
// NewFluentServiceProfileBuilder().WithType undefined (type idBuilder has no field or method WithType)
https://gitee.com/gongme/sohutool/invite_link?invite=1affb731ed04933bd8a831b394f4cc6d6b026f1487263564b7993663152babc56a3207b1d7d8dbf38489b742a2f7f3a6
本文来自博客园,作者:易先讯,转载请注明原文链接:https://www.cnblogs.com/gongxianjin/p/17596661.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具