Make Money with TDD - 08
No Primitives
I'm VERY against primitives' being passed around in a system. As soon as I started putting in hard coded strings of the currency I knew it'd have to be extracted out into something that's not a primitive.
I didn't do this earlier because it wasn't being asked for by the code. We kept it contained within our system, so it wasn't as concerning. Now, with our need to remove currency specific methods, callers are required to provide the currency, we can't have them typing the hard coded string. It's much to easy to make a mistake like that.
One of my favorite ways to determine the complexity of a system is how much data moves around. The more data movement in the system the more complex it is. That complexity creates friction to maintainability. I want my systems to be easy to maintain, so I try to prevent complexity as best as I can.
Let's get onto our current requirement
5 USD * 2 = 10 USD10 EUR * 2 = 20 EUR4002 KRW / 4 = 1000.5 KRW5 USD + 10 USD = 15 USD5 USD + 10 EUR = 17 USD12 USD == 10 EUR1 USD + 1100 KRW = 2200 KRW2.50 USD * 6 = 15 USDConvert between currencies
Currency not exposed primitives
Remove named currency methods for general one
We can do an enum with the currency as the string name. We could just use the int value of the enum, it's only our system, so should be OK.
I've actually been thinking about how a consumer could extend the system to provide their own currency types and their own exchange rates... which... Yeah... I don't think int values would be very useful. Especially with how our ExchangeRates
data is set up; that'd be terrible.
Let this be an important bit about how design decisions impact the code evolution. While we can very much change everything of our data struct, especially since we've kept it nice and isolation - What it currently is does constrain our choices a bit, unless we want to put in the extra work.
This is one of the reasons I don't want to extract things too early, It constrains future decisions unless we do the work to undo/change the abstraction. If we have to undo the abstraction, it was clearly not the right abstraction.
We won't always make the right abstraction. It's almost certain it'll have to change, but too early means more changes. It's a balance, you'll learn your comfort level with time.
OK - If an enum is out. What's next... public const string
? That doesn't really prevent strings from floating around our system. We would still have string
as our param types, which is what we want to avoid.
The only one really left is a type that take a string, and can produce it on demand.
public class Currency{
private readonly string currency;
protected Currency(string currency) => _currency = currency
}
As the basic. We still need a way to read the value out. And create the values.
Let's see what this looks like implemented.
public class Currency
{
private readonly string _currency;
protected Currency(string currency) => _currency = currency;
public string AsString() => _currency;
}
Basically identical. Now; I've quickly coded this up without any tests. I wanted to see what it'd look like and how it'd feel to me.
There's some bits I'm not thrilled with, but what I like to do will require pulling in some techniques outside the scope of the TDD exercise we're going through. You'll see them implemented after we get through all the requirements.
For now, I'm good with it; but let's TDD this into existence.
And yes, I'm doing the exists test.
[TestMethod]
public void Exists()
{
new Currency();
}
Which won't compile until we use our tool to generate the code
public class Currency
{
}
OH YEAH!
And now let's create the constructor to take a string
. To simplify the series of changes, I will use the Exists
test to change the constructor.
[TestMethod]
public void Exists()
{
new Currency("USD");
}
I alternate between this and a new test depending on how I'm feeling.
Which gives us
public class Currency
{
public Currency(string currency)
{
}
}
Aside from naming the variable, I haven't written any of that code. If I'd been planning ahead, I'd have used a currency string in the test of "currency"
as the tool uses the value as the initial name. Then I'd have to write zero code of the class. Use the tools.
The next simple test is for the AsString
method. One of the reason I like to sketch out a class before I start the test is to see how it looks. Is it the shape I want? Does it have the functionality I'm thinking? Is there anything obvious I missed?
While pairing, it's a quick way to get on the same page before applying TDD to generate it. I'll do it during mobbing as well... but definitely not as often. And almost never during strict mobbing. It tries to force everyone on the same page, but I always feel it limits the navigator choice of what to do during their time. Just go when mobbing, the mob will course correct. The dynamic in pairing is different and the early sketches can help align understanding and enable better progress.
OK; the test.
[TestMethod]
public void AsStringExists()
{
string actual = new Currency("blort").AsString();
}
While forcing the method to exist doesn't require a return type, this provides more information to the tool allowing it to generate the code I want so I don't have to type it.
public string AsString()
{
return "";
}
And the only thing I had to type here was return ""
to prevent it from throwing an exception. Which TECHNICALLY would still have the method existing... but it'd fail unless I added exception expectations. Extra code, not doing it.
The next test we want is to ensure that the returned value is the value passed into the constructor. But we can triangulate to get there.
[TestMethod]
public void AsStringReturnsUSD()
{
//Arrange
Currency subject = new Currency("USD");
//Act
string actual = subject.AsString();
//Assert
actual.Should().Be("USD");
}
A quick and easy test.
nd... HAHAHA - I made a mistake that's very hard to show (because I'm lazy and not doing screen shots right now). My test runner didn't auto-include the CurrencyTests
so while I was working with non-compiling tests I didn't notice that my Green state wasn't running the tests. It wasn't until I had a test that would fail on an assert that I got confused. I was all green... but... that assert would not pass.
Oops.
Make sure your tests are running. That's key.
Now that it's running; I can see that it fails for the reason we expect. Expected actual to be "USD" with a length of 3, but ""
Let's make it pass!
public string AsString()
{
return "USD";
}
That's right! Hard Code the Value!!!
Since we want the exact same test with a different value; let's follow our
- Copy & Paste
- Edit Values
- Edit Name
process to create a new test. And this fails for the reason we expect Expected actual to be "Anything" with a length of 8, but "USD" has a length of 3, differs near "USD" (index 0)
. Looking at our existing implementation... kinda hard to do anything but the right thing.
public class Currency
{
private readonly string _currency;
public Currency(string currency) => _currency = currency;
public string AsString() => _currency;
}
Which gets us pretty close. We should refactor the class out now.
Object => Values?
We have the object, now how do we construct the values?
We could do the knowledge classes as we did before. That's not terrible. It's allow us to make Currency abstract
. I haven't made it sealed at the moment since I'm not sure what I want it to be.
I don't really like the Knowledge Classes for this. I don't really want many instance of objects floating around. A currency should be reference equal to the same currency.
That means they have to be static. Like this
public class Currency
{
public static Currency UsDollar = new("USD");
public static Currency KoreanWon = new("KRW");
public static Currency Euro = new("EUR");
...
}
I don't like that it's not abstract or sealed. A huge concern I have if I seal it is that currencies can't be extended or added by consumers. We're limiting our system by doing that. I could abstract it, but then I'd have to create an internal derived class just to instantiate it. Which feels like overkill.
This is one of the few times an object will stay neither sealed nor abstract. Unless... Depending on the system, I might make it abstract with the private derived class to keep the "always sealed or abstract" pattern in the code base. It's much harder to allow an exception when there's no exceptions.
And I've talked myself into it.
OK, first we need tests to create the new currencies.
[TestMethod]
public void UsdAsCurrency()
{
//Arrange
Currency subject = Currency.UsDollar;
//Act
string actual = subject.AsString();
//Assert
actual.Should().Be("USD");
}
The tests for each one follows this pattern. It quickly allows us to implement the required static values.
Now that we have these, we can create our internal derived class to allow us to abstract Currency.
AKSHULLY... As mentioned, not a fan of the internal private. I'm gonna switch it to be abstract but have the derived class be external to Currency
. We'll have to shift some other stuff around.
I've quickly hacked in
public abstract class Currency
{
private readonly string _currency;
protected Currency(string currency) => _currency = currency;
public string AsString() => _currency;
}
public sealed class DefaultCurrency: Currency
{
public static Currency UsDollar = new DefaultCurrency("USD");
public static Currency KoreanWon = new DefaultCurrency("KRW");
public static Currency Euro = new DefaultCurrency("EUR");
private DefaultCurrency(string currency) : base(currency){}
}
Which breaks a lot of tests.
I'm gonna just update the tests. I feel this is a refactor; but a RISKY refactor. This is definitely a R!!
for the commit notation.
The test updates actually reduces it down to 3 tests, one for each currency type
[TestMethod]
public void UsdAsCurrency()
{
//Arrange
Currency subject = DefaultCurrency.UsDollar;
//Act
string actual = subject.AsString();
//Assert
actual.Should().Be("USD");
}
This confirms all of the behaviors we want.
There sure is a lot of duplication in these tests like we had before. Does it cry out for simplification? Not to me.
If it does cry out for simplification; try. Listen to the code. If you're hearing "SIMPLIFY ME!!!" try to. Practice is the only way you'll be able to hear the code more accurately.
It is repetition; but it's doesn't cry out duplication to me. The tests all flow differently. There's no way to reduce the code so the tests all hit a single path. The lack of obvious simplification in the code, it's not something the code is calling out for.
Update the places
Now that we have the currency in place; we should update the places using currency to use these new things.
5 USD * 2 = 10 USD10 EUR * 2 = 20 EUR4002 KRW / 4 = 1000.5 KRW5 USD + 10 USD = 15 USD5 USD + 10 EUR = 17 USD12 USD == 10 EUR1 USD + 1100 KRW = 2200 KRW2.50 USD * 6 = 15 USDConvert between currenciesCurrency not exposed primitives
Remove named currency methods for general one
The biggest impact is the ExchangeRates
; so let's go there. We need to support both string
and Currency
types to be able to migrate the rest of the system and maintain a green state.
This is where I like to go step by step green, and a great colleague prefers to make the change and chase the compilation issues until it works again. I think either is OK. When I pair with him I absolutely use his method. It helps me see other approaches. For this, Let's go the "always green" approach.
What we need to fix first is our ExchangeRates#From
method. Well... not really fix, but create an overload and take a Currency
. public IExchangeRateTo From(Currency currency)
.
How do we handle the body though? Until the Dictionary
changes... we can't use currency
directly. So let's use AsString
and call the string method.
public IExchangeRateTo From(Currency currency) => From(currency.AsString());
Should we write a test for this?
We're green. We're refactoring. Do we write tests while refactoring? No. The process here will update the existing behaviors currently under test. Once we're done, our tests will be updated and working against the new method. No new tests will be required to have the same behavior coverage.
What to update now?
With this method, where do we update? Where do we use currency strings? Where do we use the From
method? In Money
.
Plus
,Equals
, and To
are all using the From
method. Only one of these can switch to the new Currency
type; To
. That is, via the ToWon
and ToDollar
methods.
Which... Crap. The currency we have control over is the To
Fluent Method... Well then... Lets go do that one first.
We'll overload the To
method to have a Currency
version which just uses the string
version.
public double To(Currency currency)
{
return To(currency.AsString());
}
Our Money#To
method and it's callers now look like
public Money ToWon() => To(DefaultCurrency.KoreanWon);
public Money ToDollar() => To(DefaultCurrency.UsDollar);
private Money To(Currency currency)
{
double exchangeRate = _exchangeRates.From(_currency).To(currency);
return new(_amount * exchangeRate, currency.AsString());
}
We can now update the To
tests in ExchangeRatesTests
to use the Currency
overload.
The next thing we need to approach is where else uses the string
version of the IExchangeRateTo#To
method? It looks like it's still in Money
. The Plus
and Equals
methods. A quick and simple way to find these is make the method scope private and find the compile errors.
Both of these methods use the this._currency
type into the To
Method... which means we need to construct Money
with a Currency
. It's times like this that I do think fondly of the "break it" approach.
The "stay green" way is a little slower, but it forces you to understand what's being used where and gives you confidence you're not breaking something along the way. I like that confidence and improved or confirmed understanding of the code.
Since we've created a sealed class and all instances are internal to money; pretty easy/quick update. To stay green, we're going to create a temporary constructor to convert from a Currency
to the string
.
private Money(double amount, Currency currency):this(amount, currency.AsString()){}
Some simple constructor chaining.
Now we can update the Won
, Euro
, and Dollar
methods to provide the currency.
public static Money Won(double amount) => new(amount, DefaultCurrency.KoreanWon);
public static Money Euro(double amount) => new(amount, DefaultCurrency.Euro);
public static Money Dollar(double amount) => new(amount, DefaultCurrency.UsDollar);
We're still green.
Let's do a little bit of a bigger change; let's change our _currency
type to be a Currency
. This will be a bit of "break it and fix". While it's possible to do a slow shift... it's over kill. All the breaks will be internal to Money
. When it's all in the class you're working in; I'm OK with the "break it" approach.
A couple calls to ExchangeRates#From
require the AsString
call on the _currency
showing we still have that to fix up. Which... if I'd done the interface update when I added the From(Currency)
method, I wouldn't need those. So... doing that now.
The question is now, what uses the string
versions of the fluent interfaces? Let's delete them from the interfaces and find out!
Some tests in ExchangeRatesTests
, which are easily fixed.
Except the one validating the exception thrown from the From
method. We need to create a new Currency
that's not in our exchange rates.
A quick test only private subclass of Currency sets us up to get back to Green.
That's (almost) all
Except for the dictionary; that's all of the default values... so let's hit the dictionary!
Again, all the breaking happens in this one file, so we can do the sweeping changes required here w/o trying to stay green for every little change inside the class.
I didn't actually have a good distinction for when I want to stay green and when I'm fine with the breaking changes. Contained to the same file, assuming good file practices, is a simple heuristic which seems to fit most of the times I can recall being good with it.
Having switched everything from the hard coded currency strings to the new Currency
type, we're still green.
That went a bit faster and smoother than I'd expected.
I should probably go through and add the changes to this. I did make a bunch of small commits. Hopefully I catch this and do that. But not now... at this 2am.
Method Removal
One of the goals from removing the primitive currency types was to be able to remove the ToTYPE
methods on Money
.
I think we can do that now. All we need to do is make Money#To
public and start to use it. Knowing that the ToWon
and ToDollar
methods are used just in the tests, I'm gonna delete them and use the "break it" approach to this.
public Money To(Currency currency)
{
double exchangeRate = _exchangeRates.From(_currency).To(currency);
return new(_amount * exchangeRate, currency);
}
The changes to this are pretty minimal. Just the //Act
in the test to use the new To
method with the currency
[TestMethod]
public void ConvertFromUsdToWon()
{
//Arrange
int value = new Random().Next(1, 20);
Money subject = Money.Dollar(value);
//Act
Money actual = subject.To(DefaultCurrency.KoreanWon);
//Assert
actual.Should().Be(Money.Won(value * 1100));
}
We might notice a similarity to the static create methods. Those are type specific. We should get rid of those and have the consumer provide the type. they know it, let them provide it.
With our defined types, it'll work out just fine.
A few changes in for the MoneyTests
to instantiate instead of call the static method. Fortunately the inline function of our tools does that for us very nicely. As well as let us update the constructor to public. No typing and even though it's a bit of code change, we can trust it as it's a tool done provable refactor.
Let's Refactor
Let's listen to our tool and the recommendations they give. One thing I missed was our types in DefaultCurrency
could be replaced. I didn't have them as readonly
. An oversight that our tool caught and helped us correct.
Looking at Money#Equals
the guard clause seems ... off. It's doing the same thing as if we let it get the exchange rate.
Let's change from
private bool Equals(Money other)
{
if (this._currency == other._currency) return _amount.Equals(other._amount);
double exchangeRate = _exchangeRates.From(other._currency).To(this._currency);
return _amount.Equals(other._amount * exchangeRate);
}
to
private bool Equals(Money other)
{
double exchangeRate = _exchangeRates.From(other._currency).To(this._currency);
return _amount.Equals(other._amount * exchangeRate);
}
And all our tests pass. Since our exchange rate is 1
; we get the same result. We don't need to have the extra code - CODE REMOVED! Deleting code is the best feeling.
That's all of our requirements
5 USD * 2 = 10 USD10 EUR * 2 = 20 EUR4002 KRW / 4 = 1000.5 KRW5 USD + 10 USD = 15 USD5 USD + 10 EUR = 17 USD12 USD == 10 EUR1 USD + 1100 KRW = 2200 KRW2.50 USD * 6 = 15 USDConvert between currenciesCurrency not exposed primitivesRemove named currency methods for general one
Return of the Knowledge
While we're in the refactor phase of our TDD process, I like to follow an approach a fantastic colleague has been using.
We can't just refactor the code we're actively working in. Those are local changes and refactors, which are very important to do. But we need to slowly look at the bigger and bigger picture. Go from the method to the class. From the class to other classes. Look around the code. Look at how things relate. As we go up in scope, we don't have to pay as much attention; but go through your code. Check things out. As your understanding of the system evolves, along with the system, you'll often find things that can be refactored and improved that you didn't see before.
One of my favorite aspects of doing this is directly tied to why I enjoy mob programming - I'm out of the weeds. When you're making the changes, you're in the weeds. You shouldn't be particularly cognizant of the bigger picture when you have to write a line of code.
When you can take the time to look through the code, you're going to be less in the weeds; you'll see the code as it is, not for what it's solving for you. In mob programming, as a Mobber, your role is to be aware of the bigger picture and help the navigator see those things as well.
In this case, as I looked through our latest changes the tests are showing me how the consumers will interact with our code. The tests are complaining to me about creating Money.
The most egregious of these is the test for type equality
[TestMethod]
public void SameTypesAreEqual()
{
//Arrange
Random random = new();
int value = random.Next(1, 20);
List<Money> source = new() { new((double)value, DefaultCurrency.Euro), new((double)value, DefaultCurrency.KoreanWon), new((double)value, DefaultCurrency.UsDollar) };
List<Money> compare = new() { new((double)value, DefaultCurrency.Euro), new((double)value, DefaultCurrency.KoreanWon), new((double)value, DefaultCurrency.UsDollar) };
int index = random.Next(0, source.Count);
//Act
bool actual = source[index].Equals(compare[index]);
//Assert
actual.Should().BeTrue();
}
The creation of those lists is... huge. What's the issue? Why didn't that stand out before... It wasn't as much code before.
Ignoring the syntax sugar C# is providing, how do we create Money
new Money(value, DefaultCurrency.Euro)
That's a bit of code. Not a lot, but a bit. It requires two pieces of knowledge, Knowing Money
and knowing how to access the correct currency.
This double knowledge requirement is what lead to the original creation of Knowledge Classes around creating the different types of money. Now, we're looking at the same duplication, but without the risk of typos due to primitives.
One of the guides I use when developing systems is that anything the users of an API ALWAYS have to do, we do it for them.
In this case they always have to know money and the currency type. I don't like that, clearly. Let's go back to Currency specific knowledge classes that handle that for the users.
First thing we need to do is un-sealed
Money
. This will allow us to derive classes from it.
As all Knowledge Classes will be, it's very simple
public class UsDollar:Money
{
public UsDollar(double amount) : base(amount, DefaultCurrency.UsDollar){}
}
This is almost a convivence object for our consumers. Almost. it ties into my guiding principle of software development - "Represent the concepts that exist in the code".
The above Knowledge Class is a concept that exists in the code - An Amount of US Dollars. When we limit it to creation by new Money(value, DefaultCurrency.UsDollar)
then the concept of "An Amount of US Dollars" isn't represented in our code. It exists in our code, clearly - We're creating it right there. We are creating that concept, but it doesn't exist in the code-base itself.
The Knowledge Classes are enabling us to fully represent the domain of our problem with concrete instances that represent the concepts we're using. Once we give these things a name, it becomes much easier to think about them. We've decoupled our thoughts and conversations about UsDollar
from always being tied to "an instance of Money
". Aside from being the base type, used most places, we have the concept we can talk about.
While this contrived example isn't as obvious, What if the currency code changes? It doesn't happen a lot; but it has, and I'm sure it will again.
The Mexican Peso, in 1993 changed from a currency code of MXP
to MXN
. I know this because I'm a little bit of a collector of money, and the Peso went from 20,000 Peso bills to 200 Peso bills in the new form. Note: I'm not sure the exact exchange, but that kinda change.
If we allow our consumers to specify the currency code everywhere... they have to now go update ALL of their code to the new currency code. This is instead of just updating a library if we add the concept of a MexicanPeso
.
We can also represent the old peso if that's what they needed to use by adding a new type of MexicanPesoPre1993
. This is the concept we want to talk about in the code.
new Money(value, DefaultCurrency.MexicanPesoPre1993)
Isn't the concept. That is an instance of Money
using the "Pre 1993 Mexican Peso". The conversation about code is centered around "Money". It's a extra mental manipulation we need to have; increasing the cognitive load of thinking about the system. With just MexicanPeso
and MexicanPesoPre1993
we have the concepts! We can point to the thing we mean!
Let's add the other two types. These, we could definitely add tests to generate...
[TestMethod]
public void UsDollarExists()
{
//Arrange
Money subject = new UsDollar(10);
//Act
Money actual = subject.To(DefaultCurrency.UsDollar);
//Assert
actual.Should().Be(subject);
}
Simple little things that will convert to the same type, and should continue to be equal.
We're going a little faster with these tests as we've already built out the concepts when we had the hard coded currency. We could be doing these as pure refactor phase, if we wanted. We're adding them to replace the instantiation of Money
, so there will be tests for all of the knowledge these classes contain.
Once these classes are in
public class UsDollar : Money
{
public UsDollar(double amount) : base(amount, DefaultCurrency.UsDollar) { }
}
public class Euro : Money
{
public Euro(double amount) : base(amount, DefaultCurrency.Euro) { }
}
public class KoreanWon : Money
{
public KoreanWon(double amount) : base(amount, DefaultCurrency.KoreanWon) { }
}
we can update the rest of the tests to use this instead of Money
directly. To simplify this, I'll use my colleagues approach of "break it"!. Let's make the public
constructor for Money
protected
!
That'll give us some errors. Which we fix!
The big method from above is much reduced
[TestMethod]
public void SameTypesAreEqual()
{
//Arrange
Random random = new();
int value = random.Next(1, 20);
List<Money> source = new() { new Euro(value), new KoreanWon(value), new UsDollar(value) };
List<Money> compare = new() { new Euro(value), new KoreanWon(value), new UsDollar(value) };
int index = random.Next(0, source.Count);
//Act
bool actual = source[index].Equals(compare[index]);
//Assert
actual.Should().BeTrue();
}
Still a bit to the list, but it's far less noisy.
Currently Money
is not following our abstract
or sealed
practice. We clearly can't seal it, so let's make it abstract
. Since we do need to be able to create instance of Money
without knowing the currency type, we have to have some 'general form' we can create instances of. As mentioned earlier, I'll use private nested or internal classes. This is a case I think the private class is going to be our best option. It's cleanest and minimizes the exposure in our own system for things we don't want others to do.
BUT FIRST - Seal up the knowledge classes.
OK; simple.
What we end up with is a pass through that allows us to instantiate Money
private sealed class InnerMoney:Money
{
public InnerMoney(double amount, Currency currency) : base(amount, currency){}
}
It doesn't feel the best. Never has, but it's hidden from the rest of the system. It doesn't add any complexity outside of Money
and minimal there. It's a little smell... but ... something else I do will remove this nested class.
Well done!
That's for suffering through my writing.
Next up
I promised to show a couple things through this exercise. I don't remember what they are... so hopefully when I edit this I'll find them again. The ones I know I want to share are
- PrivateCtor - Utility class to access private constructors
- ToSystemType - Not mentioned during this, but a utility I use to simplify the
AsString
behavior - MicroObjects - We haven't hit the size limit yet; let's explore that.
- ADD THE ONES I'M FORGETTING AT 2 AM
- ...