Value Object - Sofka-XT/ddd-generic-java GitHub Wiki
Uno de los conceptos más importantes del DDD táctico es el objeto de valor. Este es también el bloque de construcción de DDD que más utilizo en proyectos que no son de DDD, y espero que después de leer esto, tú también lo hagas.
Un objeto de valor es un objeto cuyo valor es importante. Esto significa que dos objetos de valor con el mismo valor exacto pueden considerarse el mismo objeto de valor y, por lo tanto, son intercambiables. Por esta razón, los objetos de valor siempre deben hacerse inmutables. En lugar de cambiar el estado del objeto de valor, lo reemplaza con una nueva instancia. Para objetos de valor complejo, considere usar el patrón de construcción o esencia.
Los objetos de valor no son solo contenedores de datos, también pueden contener lógica empresarial. El hecho de que los objetos de valor también sean inmutables hace que las operaciones comerciales sean seguras para subprocesos y libres de efectos secundarios. Esta es una de las razones por las que me gustan tanto los objetos de valor y por qué debería intentar modelar tantos conceptos de su dominio como sea posible como objetos de valor. Además, trate de hacer que los objetos de valor sean lo más pequeños y coherentes posible, esto los hace más fáciles de mantener y reutilizar.
Un buen punto de partida para crear objetos de valor es tomar todas las propiedades de valor único que tienen un significado comercial y envolverlas como objetos de valor. Por ejemplo:
-
En lugar de usar un BigDecimal para valores monetarios, use un objeto de valor Money que envuelva un BigDecimal. Si está tratando con más de una moneda, es posible que desee crear también un objeto de valor Moneda y hacer que su objeto Moneda envuelva un par BigDecimal-Moneda.
-
En lugar de utilizar cadenas para números de teléfono y direcciones de correo electrónico, utilice los objetos de valor PhoneNumber y EmailAddress que envuelven cadenas.
El uso de objetos de valor como este tiene varias ventajas. En primer lugar, aportan contexto al valor. No necesita saber si una cadena específica contiene un número de teléfono, una dirección de correo electrónico, un nombre o un código postal, ni necesita saber si un BigDecimal es un valor monetario, un porcentaje o algo completamente diferente. El tipo en sí le dirá inmediatamente a qué se enfrenta.
En segundo lugar, puede agregar todas las operaciones comerciales que se pueden realizar en valores de un tipo particular al objeto de valor en sí. Por ejemplo, un objeto Money puede contener operaciones para sumar y restar sumas de dinero o calcular porcentajes, al tiempo que garantiza que la precisión del BigDecimal subyacente sea siempre correcta y que todos los objetos Money involucrados en la operación tengan la misma moneda.
En tercer lugar, puede estar seguro de que el objeto de valor siempre contiene un valor válido. Por ejemplo, puede validar la cadena de entrada de la dirección de correo electrónico en el constructor de su objeto de valor EmailAddress.
Un objeto de valor de Money en Java podría verse así (el código no está probado y algunas implementaciones de métodos se han omitido para mayor claridad):
public class Money implements ValueObject<Money.Value>, Serializable, Comparable<Money> {
private final BigDecimal amount;
private final Currency currency; // Currency is an enum or another value object
public Money(BigDecimal amount, Currency currency) {
this.currency = Objects.requireNonNull(currency);
this.amount = Objects.requireNonNull(amount).setScale(
currency.getScale(), currency.getRoundingMode()
);
}
public Money add(Money other) {
assertSameCurrency(other);
return new Money(amount.add(other.amount), currency);
}
public Money subtract(Money other) {
assertSameCurrency(other);
return new Money(amount.subtract(other.amount), currency);
}
private void assertSameCurrency(Money other) {
if (!other.currency.equals(this.currency)) {
throw new IllegalArgumentException("Money objects must have the same currency");
}
}
public boolean equals(Object o) {
// Check that the currency and amount are the same
}
public int hashCode() {
// Calculate hash code based on currency and amount
}
public int compareTo(Money other) {
// Compare based on currency and amount
}
@Override
public Value value() {
return new Value() {
@Override
public BigDecimal amount() {
return amount;
}
@Override
public Currency currency() {
return currency;
}
};
}
interface Value {
BigDecimal amount();
Currency currency();
}
}
Un objeto de valor StreetAddress y el constructor correspondiente en Java podrían verse así (el código no está probado y algunas implementaciones de métodos se han omitido para mayor claridad):
public class StreetAddress implements ValueObject<StreetAddress.Value>, Serializable, Comparable<StreetAddress> {
private final String streetAddress;
private final PostalCode postalCode; // PostalCode is another value object
private final String city;
private final Country country; // Country is an enum
private StreetAddress(String streetAddress, PostalCode postalCode, String city, Country country) {
// Verify that required parameters are not null
// Assign the parameter values to their corresponding fields
}
// Getters and possible business logic methods omitted
public boolean equals(Object o) {
// Check that the fields are equal
}
public int hashCode() {
// Calculate hash code based on all fields
}
public int compareTo(StreetAddress other) {
// Compare however you want
}
@Override
public Value value() {
return new Value() {
@Override
public String streetAddress() {
return streetAddress;
}
@Override
public PostalCode postalCode() {
return postalCode;
}
@Override
public String city() {
return city;
}
@Override
public Country country() {
return country;
}
};
}
public static class Builder {
private String streetAddress;
private PostalCode postalCode;
private String city;
private Country country;
public Builder() { // For creating new StreetAddresses
}
public Builder(StreetAddress original) { // For "modifying" existing StreetAddresses
streetAddress = original.streetAddress;
postalCode = original.postalCode;
city = original.city;
country = original.country;
}
public Builder withStreetAddress(String streetAddress) {
this.streetAddress = streetAddress;
return this;
}
// The rest of the 'with...' methods omitted
public StreetAddress build() {
return new StreetAddress(streetAddress, postalCode, city, country);
}
}
interface Value {
String streetAddress();
PostalCode postalCode(); // PostalCode is another value object
String city();
Country country(); // Country is an enum
}
}