Factories

Now, we can implement more value objects that will be used for other fields in our entity. Remember, we have three methods in the entity that expressed its basic behaviour: SetTitle(string), UpdateText(string) and UpdatePrice(double). The easiest one to deal with would be the last one since we already have a value object type for it - Price. Let's focus on other two methods and see what constraints we can implement using value objects instead of plain strings for ad title and text.

Complete value object class for the classified ad title could look like this:

using System;
using Marketplace.Framework;

namespace Marketplace.Domain
{
public class ClassifiedAdTitle : Value<ClassifiedAdTitle>
{
public static ClassifiedAdTitle FromString(string title) =>
new ClassifiedAdTitle(title);

private readonly string _value;

private ClassifiedAdTitle(string value)
{
if (value.Length > 100)
throw new ArgumentOutOfRangeException(
"Title cannot be longer that 100 characters",
nameof(value));

_value = value;
}
}
}

Let's go through it to understand how it works.

First, we use our abstract Value<T> base class to remove the boilerplate code, just as we did before in the identity and price value objects. Then, skipping the static method, you can see the private value field, again, like in other value objects we have created before. But then we have a private constructor, which accepts a regular string argument. Inside the constructor, we enforce the constraint that the ad title cannot be longer than 100 characters. It will allow us not to spread such checks to other parts of the application. You might ask a question - why the constructor is private in this case? It is because we might have different sources of data for the title string and before calling the constructor, we might need to take some additional operations. It is not done in the code snippet above just yet, but we will add such functionality later. The next question would be - how do we construct new instances of this class if the constructor is private? It is where the factory pattern becomes useful.

Factories are functions that are used to create instances of domain objects, which are by definition valid. Factory functions can execute some logic to construct valid instances, and such logic could be different per factory. It is why we would expect to have multiple factory methods in one value object class, although this is not a requirement. Factories also make help making implicit things more explicit by using proper naming. In our ClassifiedAdTitle class, we only have one factory, which converts the string to the value object instance. It is quite clear what it does and what kind of argument it accepts.

Let's see how can we use factories to handle different use cases. Imagine that we get a requirement for ad title to support Markdown partially. In fact, we only need to support italic and bold. We do need to validate the existing factory argument since any string is a valid Markdown string anyway. But if we can get input from some online editor that can only produce pure HTML, we can do a conversion in a new factory function:

public static ClassifiedAdTitle FromHtml(string htmlTitle)
{
var supportedTagsReplaced = htmlTitle
.Replace("<i>", "*")
.Replace("</i>", "*")
.Replace("<b>", "**")
.Replace("</b>", "**");
return new ClassifiedAdTitle(Regex.Replace(
supportedTagsReplaced, "<.*?>", string.Empty));
}

I have to admit that this function is not perfect because it is insufficient in the number of tags it handles. It also cannot correctly handle HTML tags that are written using capital letters. But it is good enough for the demo purposes to give you an idea of what kind of logic can be included in factory functions.

Now, let's move to the Price class and see if can create some factories and apply more rules to it. Since Price inherits from Amount, we can look to make the Amount class more strict:

using System;
using Marketplace.Framework;

namespace Marketplace.Domain
{
public class Money : Value<Money>
{
public static Money FromDecimal(decimal amount) =>
new Money(amount);

public static Money FromString(string amount) =>
new Money(decimal.Parse(amount));

protected Money(decimal amount)
{
if (decimal.Round(amount, 2) != amount)
throw new ArgumentOutOfRangeException(
nameof(amount),
"Amount cannot have more than two decimals");

Amount = amount;
}

public decimal Amount { get; }

// Public methods go here as before
}
}

As you can see here, Money class now has a protected constructor, which cannot be called from outside, except inherited classes like Price. The constructor now checks if the amount argument has more than two decimal points and throws an exception if this is the case. Finally, we have two factory functions that create instances of Money from decimal or string arguments. Most probably we will receive strings from the API so we can try parsing them inside the factory. It will of course throw the decimal parsing exception should the given string not represent a valid number.

We are checking if an amount of money has two decimal places and usually this is what we need to do. But bear in mind that not all currencies support two decimals. For example, the Japanese yen must have no decimals at all. Amounts in yen are always round. You'd probably be surprised to know that Omani rial supports three decimal places, so if you plan to deliver your application in Oman, you should not use the Money class from this book, or at least change the rules.

Always check if rules that you apply are valid on all markets that you plan to support. Things like currencies, date and time formats, people names, bank accounts and addresses can have surprisingly large variety across the globe, and it is always worth checking if you are applying rules that make sense.

Now, let's imagine that our application needs to support different currencies. I means that currency information would also need to be included in this value object. After adding it, we get code like this:

using System;
using Marketplace.Framework;

namespace Marketplace.Domain
{
public class Money : Value<Money>
{
private const string DefaultCurrency = "EUR";

public static Money FromDecimal(
decimal amount, string currency = DefaultCurrency) =>
new Money(amount, currency);

public static Money FromString(
string amount, string currency = DefaultCurrency) =>
new Money(decimal.Parse(amount), currency);

protected Money(decimal amount, string currencyCode = "EUR")
{
if (decimal.Round(amount, 2) != amount)
throw new ArgumentOutOfRangeException(
nameof(amount),
"Amount cannot have more than two decimals");

Amount = amount;
CurrencyCode = currencyCode;
}

public decimal Amount { get; }
public string CurrencyCode { get; }

public Money Add(Money summand)
{
if (CurrencyCode != summand.CurrencyCode)
throw new CurrencyMismatchException(
"Cannot sum amounts with different currencies");

return new Money(Amount + summand.Amount);
}

public Money Subtract(Money subtrahend)
{
if (CurrencyCode != subtrahend.CurrencyCode)
throw new CurrencyMismatchException(
"Cannot subtract amounts with different currencies");

return new Money(Amount - subtrahend.Amount);
}

public static Money operator +(
Money summand1, Money summand2) =>
summand1.Add(summand2);

public static Money operator -(
Money minuend, Money subtrahend) =>
minuend.Subtract(subtrahend);
}

public class CurrencyMismatchException : Exception
{
public CurrencyMismatchException(string message) :
base(message)
{
}
}
}

First, we said currency information to the constructor and both factory methods. By default, factories will use EUR if no currency is specified. We also keep currency information inside the class. Second, Add and Subtract methods started to check if both operands have the same currency. In case currencies of operands don't match, these methods throw an exception.

We also added a domain-specific exception that explicitly tells us that operations on two instances of Money cannot be completed because they have different currencies.

Imagine how many bugs such simple technique can prevent in a multi-currency system, where developers too often forget that monetary value for the same decimal amount can be drastically different, depending on which currency this amount of money is issued? For example, 1 US dollar is roughly equal to 110 Japanese yen and adding 1 to 110, in this case, won't give you the right result.

One thing that remains uncovered with our Money object is that we can supply any string as currency code and it will be accepted. Imagine, we can have this failure very easily:

var firstAmount = Money.FromDecimal(10, "USD");
var secondAmount = Money.FromDecimal(20, "Usd");
var thirdAmount = Money.FromDecimal(30, "$");

Looking at the Money class code, we can quickly conclude that no operations can be performed on combinations of these objects. Firstamount + secondAmount will crash because our class will decide that they have different currencies. The thirdAmount is utterly invalid because the dollar sign is not a valid currency code, but our class still accepts it. Let's see what can we do to fix it.

To be able to check the currency code validity, we either need to keep all valid country codes inside the code of our value object class or use some external service to do the check. The first option is self-contained, so we will not have any dependencies for the value object class. However, by doing this, we will inject a somewhat alien concept to the value object code, which we will need to change each time something happens in the world of finances. One might argue that new currencies do not appear every day but at the same time Eurozone was being expanded during the last few years and each time a new country starts using Euro, their old currency disappears, and this needs to be taken into account. These factors are utterly external to our system, and it would not be smart to create such easy-to-forget time bomb in our code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.188.201