El 'Value object' es la pieza más pequeña del 'Domain'.
Especificaciones
Suele tener una complejidad baja salvo excepciones muy puntuales, ya que sólo valida que su información es consistente y alguna operación que lo modifique. Pero como actúan como imanes de lógica de negocio, le subimos un punto la complejidad, con lo que le damos una complejidad de 2.
Su principal habilidad es contener pequeñas unidades de información y que cumplan sus reglas.
Los Value objects son fáciles de diferenciar de los Aggregates porque no tienen un identificador que los haga únicos. Aparte contienen uno o más primitivos.
Como característica remarcable, cabe decir que son inmutables. Eso quiere decir que si se intenta alterar el contenido del Value object, nos devolverá un Value object nuevo. Esto se hace para evitar los llamados “side effects”, porque podemos enviar un valor a través de las funciones u otros objetos y podría ser alterado sin nuestro conocimiento al ser pasado por referencia.
Si tenemos que comparar si dos Value objects son iguales, lo haremos sobreescribiendo el método “equals” o implementando uno para que compare si sus valores coinciden entre los dos.
Se relaciona con los que lo contienen, que pueden ser Aggregate root, Aggregate y otros Value objects.
¿Qué valor me aporta implementar un Value object?
Al ser la pieza más pequeña del Domain, también es el responsable de definir las pequeñas reglas que existen en dicho Domain.
Por un lado, todas esas pequeñas normas y validaciones que podrían parecer un grano de arena, en un desarrollo iterativo e incremental, a medida que el sistema se vuelve más complejo o maduro, se vuelven una montaña.
Por otro lado, si se define una regla de Domain nueva que afecta a un Value object, ya tenemos el objeto candidato a centralizar dicha verificación.
También mantiene a raya el uso de librerías en nuestro Domain. Si necesitamos una librería que nos gestione temas complejos, como validar un NIF, nuestro Value object actuará de wrapper para envolver dicha dependencia, y si en un futuro queremos cambiar de librería o implementar directamente la validación, sólo tendremos que cambiar una sola clase en toda la aplicación.
¿Un Value object puede contener otros Value objects?
Pues la verdad es que si. La idea es que siempre vayamos pasando valores con un tipado fuerte para evitar bailes de parámetros en funciones y tener las pequeñas validaciones ubicadas en su lugar correcto, que son los Value objects.
¿Cómo se expresa esta carta en el mundo real?
Como indica el icono de arriba a la izquierda, corresponde a una clase.
Lo ideal es que sea una clase sin herencia, ya que cada Value object, aunque contenga primitivos, tendrá sus propias normas y operaciones. Aunque hay maneras de asegurar que podamos heredar sin las consecuencias más perjudiciales derivadas de ello.
readonlyclassMoney{privatefunction__construct(privatestring$amount,privateCurrency$currency,){if(!is_numeric($amount)){thrownewInvalidAmountException::notNumeric($amount);}}publicstaticfunctioneur(string$amount):self{returnnewstatic($amount,Currency::eur());}publicfunctionamount():string{return$this->amount;}publicfunctioncurrency():Currency{return$this->currency;}publicfunctionadd(Money$other):Money{if(get_class($this)!=get_class($other)){thrownewInvalidArgumentException("Expected ".get_class($this)." value object, not ".get_class($other));}if(!$this->currency->equals($other->currency())){thrownewInvalidCurrencyException::notEquals($this->currency(),$other->currency(),);}$sum=bcadd($this->amount(),$other->amount(),2);returnnewself($sum,$this->currency);}publicfunctionequals(Money$other):bool{if(get_class($this)!=get_class($other)){thrownewInvalidArgumentException("Expected ".get_class($this)." value object, not ".get_class($other));}returnbccomp($this->amount(),$other->amount(),2)===0&&$this->currency->equals($other->currency());}}
Explicación
Nos aseguramos que la clase sea de solo lectura después de crearla.
En el caso que dejemos que haya herencia, nos aseguramos que aunque se creen clases que extienden de Money, solo puedan compararse entre sus mismas clases finales.
Agregamos un constructor semántico para aportar contexto. En este caso creamos el Value object con Currency Euro.
classMoney(privatevalamount:Float,privatevalcurrency:String){funamount():Float=amountfuncurrency():String=currencyoverridefunequals(other:Any?):Boolean{if(this===other)returntrueif(other==null||javaClass!=other.javaClass)throwInvalidClassException("Expected $javaClass value object.")valotherMoney=otherasMoneyif(amount!=otherMoney.amount)returnfalseif(currency!=otherMoney.currency)returnfalsereturntrue}overridefunhashCode():Int{returnamount.hashCode()+currency.hashCode()}}
Solución
NO lo es.
No valida el currency.
Aspectos interesantes
Kotlin gestiona la posibilidad de herencia en el equals de otra manera, ya que te fuerza a implementar su equals con Any? como tipo de argumento.
classMoney(privatevaramount:Float,privatevarcurrency:String){init{require(currency.uppercase()inlistOf("EUR","USD")){throwInvalidCurrencyException.notInTheAcceptedCurrencies(currency)}}funamount():Float=amountfuncurrency():String=currencyfunadd(amount:Float){this.amount+=amount}overridefunequals(other:Any?):Boolean{if(this===other)returntrueif(other==null||javaClass!=other.javaClass)throwInvalidClassException("Expected $javaClass value object.")valotherMoney=otherasMoneyif(amount!=otherMoney.amount)returnfalseif(currency!=otherMoney.currency)returnfalsereturntrue}overridefunhashCode():Int{returnamount.hashCode()+currency.hashCode()}}
Solución
NO lo es.
Es mutable, con lo que puede crear “side effects” inesperados.
El parámetro que acepta la función add debería ser del mismo tipo que la clase que lo recibe. O sea, Money.
classMoney(privatevalamount:Float,privatevalcurrency:String){init{require(currency.uppercase()inlistOf("EUR","USD")){throwInvalidCurrencyException.notInTheAcceptedCurrencies(currency)}}funamount():Float=amountfuncurrency():String=currencyfunadd(other:Money):Money{if(this.currency!==other.currency())throwInvalidCurrencyException.notEquals(this.currency,other.currency());valamount=this.amount+other.amount()returnMoney(amount,this.currency)}overridefunequals(other:Any?):Boolean{if(this===other)returntrueif(other==null||javaClass!=other.javaClass)throwInvalidClassException("Expected $javaClass value object.")valotherMoney=otherasMoneyif(amount!=otherMoney.amount)returnfalseif(currency!=otherMoney.currency)returnfalsereturntrue}overridefunhashCode():Int{returnamount.hashCode()+currency.hashCode()}}
Solución
SI lo es.
Valida los valores
Valida que las dos monedas son iguales a nivel de Currency.
Ahora devuelve un objeto nuevo si se le agrega un valor.
classMoney(privatevalamount:Float,privatevalcurrency:Currency){funamount():Float=amountfuncurrency():Currency=currencyfunadd(other:Money):Money{if(!this.currency.equals(other.currency()))throwInvalidCurrencyException.currencyNotEquals(this.currency,other.currency());valamount=this.amount+other.amount()returnMoney(amount,this.currency)}overridefunequals(other:Any?):Boolean{if(this===other)returntrueif(other==null||javaClass!=other.javaClass)throwInvalidClassException("Expected $javaClass value object.")valotherMoney=otherasMoneyif(amount!=otherMoney.amount)returnfalseif(!currency.equals(otherMoney.currency))returnfalsereturntrue}overridefunhashCode():Int{returnamount.hashCode()+currency.hashCode()}}
Solución
SI lo es.
Contiene otro value object y ahora es más robusto.
La lógica que regula el Currency se ha traspasado a un Value object propio.
openclassMoneyprivateconstructor(privatevalamount:Float,privatevalcurrency:Currency){companionobject{funeur(amount:Float):Money{returnMoney(amount,Currency.eur())}}funamount():Float=amountfuncurrency():Currency=currencyfunadd(other:Money):Money{if(other==null||javaClass!=other.javaClass)throwGenericException.incompatibleValueObject(javaClass,other.javaClass);if(!this.currency.equals(other.currency()))throwInvalidCurrencyException.currencyNotEquals(this.currency,other.currency());valamount=this.amount+other.amount()returnMoney(amount,this.currency)}overridefunequals(other:Any?):Boolean{if(this===other)returntrueif(other==null||javaClass!=other.javaClass)throwInvalidClassException("Expected $javaClass value object.")valotherMoney=otherasMoneyif(amount!=otherMoney.amount)returnfalseif(!currency.equals(otherMoney.currency))returnfalsereturntrue}overridefunhashCode():Int{returnamount.hashCode()+currency.hashCode()}}
Explicación
Agregamos un constructor semántico para aportar contexto. En este caso creamos el Value object con Currency Euro.
classMoney{privatereadonly_amount: number;privatereadonly_currency: Currency;publicconstructor(amount: number,currency: Currency){if(!Number.isFinite(Number(amount))){throwInvalidAmountException.invalidAmount(amount);}this._amount=amount;this._currency=currency;}publicstaticeur(amount: number):Money{returnnewMoney(amount,Currency.eur())}add(other: Money):Money{if(thisinstanceofother.constructor===false){thrownewTypeError("Incompatible value object");}if(!this.currency.equals(other.currency())){throwInvalidCurrencyException.notEquals(this.currency,other.currency());}constsum=this.amount()+other.amount();returnnewMoney(sum,this._currency);}amount():number{returnthis._amount;}currency():Currency{returnthis._currency;}equals(other: Money):boolean{if(thisinstanceofother.constructor===false){thrownewTypeError("Incompatible value object");}returnthis.amount()===other.amount()&&this._currency.equals(other.currency());}}
Explicación
Nos aseguramos que la clase sea de solo lectura después de crearla.
En el caso que usemos herencia, nos aseguramos que aunque se creen clases que extienden de Money, solo puedan compararse entre sus mismas clases finales.
Agregamos un constructor semántico para aportar contexto. En este caso creamos el Value object con Currency Euro.