5

Leaking Value Objects from your Domain

 2 years ago
source link: https://codeopinion.com/leaking-value-objects-from-your-domain/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Value Objects are a great way to explicitly capture concepts within your domain. They are immutable, always in a valid state, provide behavior, and are defined by their value. This sounds a lot like Messages (Commands, Events) that are also immutable and should be in a valid state. However, exposing your Value Objects by using them within Commands or Events can have a negative impact on your ability to evolve your domain model.

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Value Objects

First, let’s cover what Value objects are since they have many characteristics that define them. They are explicit domain concepts that should always be in a valid state. They are immutable once created, which means they are always created in a valid state. Since they cannot be mutated they also have the benefit of being defined by their value. Lastly, they should have behavior, which is a characteristic that is often overlooked.

A typical example of a Value Object is Money. Money isn’t just a decimal. Especially in a multi-currency environment. The combination of the amount and the currency is required to make it valid.

using Xunit;

namespace ValueObject { public record Currency(string Symbol) { public static Currency CAD => new("CAD"); public static Currency USD => new("USD"); }

public record Money { public Currency Currency { get; } public decimal Amount { get; }

public Money(Currency currency, decimal amount) { Currency = currency; Amount = amount; } }

public class MoneyTests { [Fact] public void Test() { var money1 = new Money(Currency.CAD, 100); var money2 = new Money(Currency.CAD, 100);

Assert.Equal(money1, money2); } } }

Another typical example is Distance. Again, the distance isn’t simply a number, but rather can be a number along with a unit of measure.

using System; using Xunit;

namespace ValueObject { public abstract record UnitOfMeasure;

public record Kilometers : UnitOfMeasure;

public record Miles : UnitOfMeasure;

public record Distance { public UnitOfMeasure UnitOfMeasure { get; } public decimal Value { get; }

public Distance(UnitOfMeasure unitOfMeasure, decimal value) { UnitOfMeasure = unitOfMeasure; Value = value; }

public Distance ToMiles() { switch (UnitOfMeasure) { case Miles: return new Distance(new Miles(), Value); case Kilometers: return new Distance(new Miles(), Math.Round(Value * 0.621371m, 2, MidpointRounding.AwayFromZero)); default: throw new InvalidOperationException(); } }

public Distance ToKilometers() { switch (UnitOfMeasure) { case Miles: return new Distance(new Kilometers(), Math.Round(Value * 1.60934m, 2, MidpointRounding.AwayFromZero)); case Kilometers: return new Distance(new Kilometers(), Value); default: throw new InvalidOperationException(); } } }

public class DistanceTest { [Fact] public void Test() { var distance1 = new Distance(new Kilometers(), 100); var distance2 = distance1.ToMiles(); var distance3 = distance2.ToKilometers();

Assert.Equal(distance3, distance1); } } }

Messaging

Commands and Events in a Message or Event Driven Architecture look very similar to Value Objects. Messages are explicit domain concepts that are immutable and in a valid state. So can you have Value Objects inside a Command or Event?

using System; using ValueObject.Command;

namespace ValueObject { public record PlaceOrderCommand(Guid CustomerId, Product Product, int Quantity, Currency Currency); }

In the example above, the PlaceOrderCommand has two Value Objects: Product and Currency. These are explicit concepts we’ve defined within a boundary.

Since messages are for the purpose of decoupling between boundaries, this means that other boundaries must be aware of these as concepts.

Putting Value Objects in your messages means you’re going to be leaking details outside of your service boundary. The consequences of this are that since messages are contracts, you’ll need to think about versioning any time you want to change a Value Object since it’s a part of a Message.

Rather, you want to keep domain concepts from leaking outside of your service boundary. Concepts within your service boundary you want to be able to refactor, change, rename, remove without having to concern yourself with other services. The purpose of messaging is decoupling and using messages as a stable contract. The moment you leak something like a Value Object into a message, you’ve coupled other services to concepts within your service boundary.

Conversion

Instead of leaking Value Objects, you can create Messages/DTOs that may look similar. Simply have some type of conversion that accepts your Value Objects as parameters but have the message being built be simple primitives or nested types.

using System; using ValueObject;

namespace ValueObject2 { public record PlaceOrderCommand(Guid CustomerId, string ProductSku, int Quantity, string CurrencySymbol) { public PlaceOrderCommand(Guid customerId, string productSku) : this(customerId, productSku, 1, Currency.USD.ToString()) { }

public PlaceOrderCommand(Guid customerId, string productSku, string currency) : this(customerId, productSku, 1, currency) { }

public PlaceOrderCommand(Guid customerId, string productSku, int quantity) : this(customerId, productSku, quantity, Currency.USD.ToString()) { } } }

As another example, that’s using a nested type.

using System; using ValueObject;

namespace ValueObject3 { public record PlaceOrderCommand(Guid CustomerId, string ProductSku, int Quantity, PlaceOrderCurrency Currency) { public PlaceOrderCommand(Guid customerId, string productSku) : this(customerId, productSku, 1, PlaceOrderCurrency.Default) { }

public PlaceOrderCommand(Guid customerId, string productSku, Currency currency) : this(customerId, productSku, 1, new PlaceOrderCurrency(currency.Symbol)) { }

public PlaceOrderCommand(Guid customerId, string productSku, int quantity) : this(customerId, productSku, quantity, PlaceOrderCurrency.Default) { } }

public record PlaceOrderCurrency(string Symbol) { public static PlaceOrderCurrency Default => new("USD"); }; }

Don’t Leak Value Objects

They are as much a domain concept as Entities are. If you’re not going to expose Entities then do not expose Value Objects. Although they may seem trivial, you’ll be handcuffed into versioning your messages if you do need to change a them in any way.

Messages are contracts for other services. You want to message contracts to have some stability. Although Value Objects have similar characteristics as messages (Commands and Events), they are meant to be internal while Messages are meant for other services boundaries.

Source Code

Developer-level members of my CodeOpinion YouTube channel get access to the full source for any working demo application that I post on my blog or YouTube. Check out the membership for more info.

Related Links

Follow @CodeOpinion on Twitter

Leave this field empty if you're human:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK