Docs

Documentation versions (currently viewingVaadin 24)
Documentation translations (currently viewingChinese)

本页面是从官方文档(http://vaadin.com/docs)机器翻译而来的。它可能包含错误、不准确或误导性的陈述。Vaadin不对翻译的准确性、可靠性或时效性作出任何保证或表示。

领域原语

了解什么是领域原语以及如何在应用程序中使用它们。

在编程中,原始数据类型 是构建所有其他数据类型的基础。在Java中,原始数据类型包括整数类型(即`byte`,shortintlong`和`char);浮点数类型(即`float`和`double`);以及`boolean`。Java还提供了其他一些有用的数据类型,例如`String`、`BigDecimal`和日期时间类型。

原始数据类型对可以存储的数据具有一定约束。例如,一个`int`可以包含-2147483648到2147483647之间的整数。当它们被用于领域模型中的属性时,会增加更多的约束。例如,一个商店的订单中“商品数量”属性可能是一个整数,但并非所有整数都是有效的数量。不应允许客户订购负数个商品。此外可能还有最高订单数量限制,超过该限制客户必须联系销售部门。

约束强制执行问题

传统上,这些领域约束以各种方式得到强制执行。例如,通过创建验证工具类:

Source code
Java
public final class QuantityUtils {

    public static final int MIN_QTY = 1;
    public static final int MAX_QTY = 100;

    public static boolean isValid(int quantity) {
        return (quantity >= MIN_QTY) && (quantity <= MAX_QTY);
    }

    public static int validate(int quantity) {
        if (!isValid(quantity)) {
            throw new IllegalArgumentException("Invalid quantity");
        }
        return quantity;
    }
}

接下来,您可能会看到在方法或构造函数中调用验证工具:

Source code
Java
public class OrderItem {

    private int quantity;
    ...

    public OrderItem(int quantity) {
        this.quantity = QuantityUtils.validate(quantity);
    }
}

您还可能见到类似于Jakarta Bean验证注解:

Source code
Java
public class OrderItem {

    @Min(QuantityUtils.MIN_QTY)
    @Max(QuantityUtils.MAX_QTY)
    private int quantity;
    ...

    public OrderItem(int quantity) {
        this.quantity = quantity;
    }
}

在这种情况下,必须能够记住在某个时候调用`Validator`。

以上两种方法均可以实现,但它们存在相同的问题:属性本身并未携带任何领域含义。一个字符串可能表示一个人的名字,也可能是个SQL查询。一个整数可能表示已订购商品数量,也可能表示数据库中记录的主键。

你必须在使用属性值的每个地方验证它们。如果没有这样做,可能会导致运行时产生意想不到的错误。例如,试图在数据库中长度为100个字符的`VARCHAR`列中存储101个字符的字符串将导致异常发生。更严重的是,数据库可能存储错误数据(如截断的数据)。

数据完整性问题可能蔓延到系统其他部分并产生意料之外的结果。例如,如果客户能输入负数的商品数量,他们可自行操作获利:添加需要的商品,再通过输入负数商品数量使得订单净额为零或负值。如果系统在检测到负净额订单时发放退款,则用户甚至可以从下订单中得到报酬。

字符串因能够包含代码而被广泛用作攻击向量。未进行验证或转义的字符串输入是所有注入攻击的根本原因。注入攻击在网络应用的关键安全风险http://owasp.org/www-project-top-ten/[OWASP Top Ten 2021]排名第3。

引入领域原语

领域模型中的所有数据结构都具有领域含义。例如,`Customer`类对应现实中的真实客户。然而,领域含义不应只局限于顶层类型,您还应给每个单独的属性赋予领域含义。例如,不应使用字符串表示用户名、整数表示数量,而应创建`Username`类和`Quantity`类。当您需要用户名或数量时,可以直接使用这些_领域原语_。[1]中。]

领域原语是具备特定性质的Java类。首先,领域原语是_值对象_,意味着其是不可变的;值相同的两个对象可互换。

其次,它封装一个或多个其他类型对象。例如,`Quantity`领域原语可以封装整数,`Username`领域原语封装字符串。领域原语也可以封装多个对象或其他领域原语。要表示货币金额,需要货币和数字金额,因此你可能会有一个`CurrencyUnit`领域原语和一个封装`BigDecimal`和`CurrencyUnit`的`MonetaryAmount`领域原语。

第三,领域原语在构造函数中验证所有输入数据。由于其不可变,因此确保数据始终有效。有关验证更多信息,请查阅Validation文档页面。

例如,以上面的数量领域原语为例:

Source code
Java
public final class Quantity {
    public static final int MIN_QTY = 1;
    public static final int MAX_QTY = 100;

    private final int value; 1

    public Quantity(int value) {
        if (value < MIN_QTY || value > MAX_QTY) { 2
            throw new IllegalArgumentException("Invalid quantity");
        }
        this.value = value;
    }

    public int value() { 3
        return value;
    }

    @Override
    public String toString() { 4
        return Integer.toString(value);
    }

    @Override
    public boolean equals(Object o) { 5
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Quantity that = (Quantity) o;
        return value == that.value;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }
}
  1. 因`Quantity`不可变,所以封装的整数变量应声明为`final`。

  2. 在构造函数中验证封装的整数。

  3. 提供访问方法获取封装的整数。

  4. 重写`toString()`以调试记录。

  5. 因为`Quantity`是值对象,所以重写`equals()和`hashCode()

使用领域原语后的`OrderItem`:

Source code
Java
public class OrderItem {

    private Quantity quantity;
    ...

    public OrderItem(Quantity quantity) {
        this.quantity = quantity;
    }
}

避免混淆

使用领域原语还有助于减少语义混淆的风险,例如将不同意义但类型相同的属性误用。

例如,定义以下对象:

Source code
Java
public record StreetAddress(
    StreetNumber number,
    StreetName streetName
) {}

须使用明确类型,防止参数混淆错误。

行为

领域原语不仅仅是封装和验证数据,也可包含计算、转换方法甚至业务逻辑。

例如,金额(MonetaryAmount)类可封装`add`和`subtract`方法,确保货币一致性,例如:

Source code
Java
public MonetaryAmount add(MonetaryAmount amount) {
    requireSameCurrency(amount);
    return new MonetaryAmount(currency, value.add(amount.value));
}

在 Flow 和 Hilla 中的使用

要在Vaadin Flow界面中使用单值领域原语,请创建一个自定义的`Converter`。多值领域原语可通过`CustomField`实现。

在Hilla中,单值领域原语使用Jackson注解`@JsonValue`与`@JsonCreator`处理JSON序列化和反序列化;多值领域原语转为TypeScript类型并通过端点反序列化。

详细引导可参考Flow和Hilla的相应文档章节。


1. 领域原语 的概念首次出现在http://www.manning.com/books/secure-by-design[Secure by Design