App下載

使用 Fluent API 創(chuàng)建更簡單、更直觀的代碼

涼水參合 2021-09-15 11:03:51 瀏覽數(shù) (4287)
反饋

我們知道,在軟件項(xiàng)目中,沒有什么能取代好的文檔。但是,也需要注意寫出的代碼有多直觀。畢竟,代碼越簡單自然,用戶體驗(yàn)就越好。

在簡單的“編程規(guī)則”中,我們將忘記我們必須記住的一切,“強(qiáng)制”你記住的 API 是失敗的關(guān)鍵證明。

這就是為什么在本文中,我們將介紹該主題并向你展示如何從 Fluent-API 概念創(chuàng)建流體 API。

什么是 Fluent-API?

當(dāng)我們?cè)谲浖こ痰纳舷挛闹姓務(wù)摃r(shí),fluent-API 是一種面向?qū)ο蟮?API,其設(shè)計(jì)主要基于方法鏈。

這個(gè)概念由?Eric Evans?和?Martin Fowler?于 2005 年創(chuàng)建,旨在通過創(chuàng)建特定領(lǐng)域語言 ( DSL )來提高代碼可讀性。

在實(shí)踐中,創(chuàng)建一個(gè)流暢的 API 意味著開發(fā)一個(gè) API,其中不需要記住接下來的步驟或方法,允許一個(gè)自然連續(xù)的序列,就好像它是一個(gè)選項(xiàng)菜單。

這種自然的節(jié)奏與餐廳甚至快餐連鎖店的工作方式類似,因?yàn)楫?dāng)您將一道菜放在一起時(shí),選項(xiàng)會(huì)根據(jù)你所做的選擇而有所不同。例如,如果你選擇雞肉三明治,則會(huì)根據(jù)所選菜肴等建議配菜。

Java 上下文中的 Fluent API

在 Java 世界中,我們可以想到此類實(shí)現(xiàn)的兩個(gè)著名示例。

第一個(gè)是?JOOQ?框架,這是一個(gè)由Lukas Eder領(lǐng)導(dǎo)的項(xiàng)目,它促進(jìn)了 Java 和關(guān)系數(shù)據(jù)庫之間的通信。JOOQ 最顯著的區(qū)別在于它是面向數(shù)據(jù)的,這有助于避免和/或減少與關(guān)系和面向?qū)ο笙嚓P(guān)的阻抗問題或損失。

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();

另一個(gè)例子是在企業(yè) Java 世界規(guī)范內(nèi)的非關(guān)系數(shù)據(jù)庫,即 NoSQL。其中包括Jakarta EE,它是同類中的第一個(gè)規(guī)范,并成為Eclipse Foundation旗下的Jakarta NoSQL。

本規(guī)范的目的是確保 Java 和 NoSQL 數(shù)據(jù)庫之間的順暢通信。

DocumentQuery query = select().from("Person").where(eq(Document.of("_id", id))).build();
Optional<Person> person = documentTemplate.singleResult(query);
System.out.println("Entity found: " + person);

一般來說,一個(gè) fluent API 分為三個(gè)部分:

  1. 最終的對(duì)象或結(jié)果:總的來說,fluent-API 類似于構(gòu)建器模式,但最強(qiáng)大的動(dòng)態(tài)與 DSL 相結(jié)合。在這兩種情況下,結(jié)果往往是代表流程或新實(shí)體結(jié)果的實(shí)例。
  2. 選項(xiàng):在這種情況下,是將用作“我們的交互式菜單”的接口或類的集合。從一個(gè)動(dòng)作來看,這個(gè)想法是按照直觀的順序只顯示下一步可用的選項(xiàng)。
  3. 結(jié)果:在所有這個(gè)過程之后,答案可能會(huì)或可能不會(huì)導(dǎo)致實(shí)體、策略等的實(shí)例。關(guān)鍵點(diǎn)是結(jié)果必須是有效的。

流體 API 實(shí)踐

為了演示這一概念,我們將創(chuàng)建一個(gè)三明治訂單,其中包含具有相應(yīng)購買價(jià)格的訂單的預(yù)期結(jié)果。流程如下所示。

流體 API 流程示例

當(dāng)然,有多種方法可以實(shí)現(xiàn)這種流暢的 API 功能,但我們選擇了一個(gè)簡短的版本。

正如我們已經(jīng)提到的 API 的三個(gè)部分——對(duì)象、選項(xiàng)和結(jié)果——我們將從“訂單”接口將表示的順序開始。一個(gè)亮點(diǎn)是這個(gè)界面有一些界面,它們將負(fù)責(zé)展示我們的選項(xiàng)。

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);
    }

這個(gè) API 的結(jié)果將是我們的訂單類。它將包含三明治、飲料及其各自的數(shù)量。

在我們返回教程之前的快速附加組件

我們不會(huì)在本文中關(guān)注但值得一提的一點(diǎn)與貨幣的表示有關(guān)。

當(dāng)涉及到數(shù)值運(yùn)算時(shí),最好使用 BigDecimal。那是因?yàn)?,根?jù)Java Effective書籍和博客When Make a Type 之類的參考資料,我們了解到復(fù)雜類型需要唯一的類型。這種推理,再加上“不要重復(fù)自己”的實(shí)用主義,結(jié)果就是使用了 Java 貨幣規(guī)范:?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 實(shí)現(xiàn)。它將負(fù)責(zé)代碼的“丑陋”部分,使 API 看起來很漂亮。

由于我們不使用數(shù)據(jù)庫或其他數(shù)據(jù)引用,因此價(jià)格表將直接放置在代碼中,并且我們打算使示例盡可能簡單。但值得強(qiáng)調(diào)的是,在自然環(huán)境中,這些信息會(huì)存在于數(shù)據(jù)庫或服務(wù)中。

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);
    }
}

結(jié)果是一個(gè) API,它將直接直觀地將請(qǐng)求返回給我們。

Checkout checkout = Order.bread(Bread.PLAIN)
           .size(Size.SMALL)
           .meat()
           .quantity(2)
           .softDrink(2);

Fluent API 與其他模式有何不同?

對(duì)兩種 API 標(biāo)準(zhǔn)進(jìn)行比較是很普遍的,它們是 Builder 和 Fluent-API。原因是它們?cè)趧?chuàng)建實(shí)例的過程中都按順序使用方法。

但是,F(xiàn)luent-API 是“與 DSL 相關(guān)聯(lián)的”,它強(qiáng)制采用一種簡單的方法來實(shí)現(xiàn)這一點(diǎn)。但為了使這些差異更加明顯,我們?yōu)槊總€(gè)模式分別列出了亮點(diǎn):

Builder 模式:

  • 它往往更容易實(shí)施;
  • 不清楚需要哪些施工方法;
  • 絕大多數(shù)問題都會(huì)在運(yùn)行時(shí)發(fā)生;
  • 一些工具和框架會(huì)自動(dòng)創(chuàng)建它;
  • 它需要在 build 方法中進(jìn)行更健壯的驗(yàn)證,以檢查哪些強(qiáng)制方法沒有被調(diào)用。

流利的API:

  • 重要的是,對(duì)于每個(gè)方法,都有驗(yàn)證,如果參數(shù)無效則拋出錯(cuò)誤,記住快速失敗的前提;
  • 它必須在過程結(jié)束時(shí)返回一個(gè)有效的對(duì)象。

現(xiàn)在,是否更容易理解模式之間的異同?

這就是我們對(duì) fluent-API 概念的介紹。與所有解決方案一樣,沒有“靈丹妙藥”,因?yàn)檎麄€(gè)過程通常不合理。

它是一個(gè)出色的工具,有助于為你和其他用戶創(chuàng)建故障保護(hù)。


0 人點(diǎn)贊