使用 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 分为三个部分:

  1. 最终的对象或结果:总的来说,fluent-API 类似于构建器模式,但最强大的动态与 DSL 相结合。在这两种情况下,结果往往是代表流程或新实体结果的实例。
  2. 选项:在这种情况下,是将用作“我们的交互式菜单”的接口或类的集合。从一个动作来看,这个想法是按照直观的顺序只显示下一步可用的选项。
  3. 结果:在所有这个过程之后,答案可能会或可能不会导致实体、策略等的实例。关键点是结果必须是有效的。

流体 API 实践

为了演示这一概念,我们将创建一个三明治订单,其中包含具有相应购买价格的订单的预期结果。流程如下所示。

流体 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 个关键点,大部分与传统的建造者模式类似:

  1. 为 ServiceProfile 定义一个 Builder 对象 fluentServiceProfileBuilder
  2. 把需要构建的 ServiceProfile 设计为 Builder 对象 fluentServiceProfileBuilder 的成员属性。
  3. 定义一系列构建属性的 Fluent 接口,通过方法的返回值控制属性的构建顺序,这是实现 Fluent API 的关键。比如 WithId 方法的返回值是 typeBuilder 类型,表示紧随其后的就是 WithType 方法。
  4. 定义一个 Fluent 接口(这里是 endBuilder)返回完成构建的 ServiceProfile,在最后调用链的最后调用。
  5. 为 Builder 定义一系列构建方法,也即实现关键点 3 中定义的 Fluent 接口,并在构建方法中返回 Builder 对象指针本身。
  6. 定义一个实例化 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

 

 

  

posted @   易先讯  阅读(626)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示