使用 Fluent API 创建更简单、更直观的代码

凉水参合 2021-09-15 11:03:51 浏览数 (4145)
反馈

我们知道,在软件项目中,没有什么能取代好的文档。但是,也需要注意写出的代码有多直观。毕竟,代码越简单自然,用户体验就越好。

在简单的“编程规则”中,我们将忘记我们必须记住的一切,“强制”你记住的 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 概念的介绍。与所有解决方案一样,没有“灵丹妙药”,因为整个过程通常不合理。

它是一个出色的工具,有助于为你和其他用户创建故障保护。


0 人点赞