Skip to content

Latest commit

 

History

History
224 lines (169 loc) · 6.54 KB

05.only-immutable-types.md

File metadata and controls

224 lines (169 loc) · 6.54 KB

Only Immutable Types

So, where do we have mutable types?

public class Bank {
    ...
    
      public void AddExchangeRate(Currency from, Currency to, double rate)
          => _exchangeRates[KeyFor(from, to)] = rate;


    ...
}

public class Portfolio {
    ...
    public void Add(Money money) => this.moneys.Add(money);
    ...
}

Bank

By making it immutable, we want to have a method AddExchangeRate with a signature like this Currency -> Currency -> double -> Bank.

🔴 Let's start by adapting a test to make it red wih a compilation error.

Failure from Bank

🟢 Make it green again.

public Bank AddExchangeRate(Currency from, Currency to, double rate)
{
    var updatedRates = this._exchangeRates;
    var key = KeyFor(from, to);
    updatedRates.TryAdd(key, default);
    updatedRates[key] = rate;
    return new Bank(updatedRates);
}

🔵 Now we can refactor and simplify our test.

[Fact(DisplayName = "Conversion with different exchange rates EUR -> USD")]
public void ConvertWithDifferentExchangeRates()
{
    _bank.Convert(new Money(10, EUR), USD)
        .Should()
        .Be(new Money(12, USD));

    _bank.AddExchangeRate(EUR, USD, 1.3)
        .Convert(new Money(10, EUR), USD)
        .Should()
        .Be(new Money(13, USD));
}

Still, ExchangeRates can be mutated accidentally from within the Bank class.

🔵 We can address this point by using ReadOnlyDictionary from the private constructor.

public class Bank {
    private readonly ReadOnlyDictionary<string, double> _exchangeRates;
  
    private Bank(IDictionary<string, double> exchangeRates) => _exchangeRates = new ReadOnlyDictionary<string, double>(exchangeRates);
  
    public static Bank WithExchangeRate(Currency from, Currency to, double rate) => 
    new Bank(new Dictionary<string, double>()).AddExchangeRate(from, to, rate);
  
    public Bank AddExchangeRate(Currency from, Currency to, double rate)
    {
        var updatedRates = new Dictionary<string, double>(this._exchangeRates);
        var key = KeyFor(from, to);
        updatedRates.TryAdd(key, default);
        updatedRates[key] = rate;
        return new Bank(updatedRates);
    }
}
public class PortfolioTest {
    private readonly Bank bank;

    public PortfolioTest()
    {
        this.bank = Bank
            .WithExchangeRate(Currency.EUR, Currency.USD, 1.2)
            .AddExchangeRate(Currency.USD, Currency.KRW, 1100);
    }
    
    ...
}

Portfolio

We have to adapt the Add method too but, it is used in a lot of places.

We'll have to use a different refactoring strategy for this.

Failure from Bank

Let's use the Wrap Technique like we did already.

Wrap Technique

🔵 Rename the existing method to AddOld.

public void AddOld(Money money) => this.moneys.Add(money);

🔴 Adapt a test to have a red test that will call the new method.

Failure from Bank

🟢 Generate the new Add method from test and call the old method from the new one to guarantee the same behavior.

public Portfolio Add(Money money)
{
    this.AddOld(money);
    return this;
}

🔵 We are now able to refactor this.

  • We add a private constructor
  • And define a public one as well
public class Portfolio {
    private readonly ICollection<Money> moneys;

    public Portfolio()
    {
        this.moneys = new List<Money>();
    }

    private Portfolio(IEnumerable<Money> moneys)
    {
        this.moneys = moneys.ToImmutableList();
    }

    public Portfolio Add(Money money)
    {
        List<Money> updatedMoneys = this.moneys.ToList();
        updatedMoneys.Add(money);
        return new Portfolio(updatedMoneys);
    }
    ...
}

🟢 Our test is now green again.

🔵 Let's refactor our tests to make it simple to instantiate Portfolio. One solution is to encapsulate Portfolio instantiation in a dedicated method taking Moneys as args.

[Fact(DisplayName = "5 USD + 10 USD = 15 USD")]
public void Add_ShouldAddMoneyInTheSameCurrency()
{
    var portfolio = PortfolioWith(
        new Money(5, Currency.USD),
        new Money(10, Currency.USD));
    
    portfolio.Evaluate(bank, Currency.USD).Should().Be(new Money(15, Currency.USD));
}

private static Portfolio PortfolioWith(params Money[] moneys) =>
    moneys.Aggregate(new Portfolio(), (portfolio, money) => portfolio.Add(money));

Let's plug remaining tests to the new Add method.

[Fact(DisplayName = "5 USD + 10 EUR = 17 USD")]
public void Add_ShouldAddMoneyInDollarAndEuro() =>
    PortfolioWith(new Money(5, Currency.USD), new Money(10, Currency.EUR))
        .Evaluate(this.bank, Currency.USD)
        .Should()
        .Be(new Money(17, Currency.USD));

[Fact(DisplayName = "1 USD + 1100 KRW = 2200 KRW")]
public void Add_ShouldAddMoneyInDollarAndKoreanWons() =>
    PortfolioWith(new Money(1, Currency.USD), new Money(1100, Currency.KRW))
        .Evaluate(this.bank, Currency.KRW)
        .Should()
        .Be(new Money(2200, Currency.KRW));

[Fact(DisplayName = "5 USD + 10 EUR + 4 EUR = 21.8 USD")]
public void Add_ShouldAddMoneyInDollarsAndMultipleAmountInEuros() =>
    PortfolioWith(new Money(5, Currency.USD), new Money(10, Currency.EUR), new Money(4, Currency.EUR))
        .Evaluate(bank, Currency.USD)
        .Should()
        .Be(new Money(21.8, Currency.USD));

[Fact(DisplayName = "Throws a MissingExchangeRatesException in case of missing exchange rates")]
public void Add_ShouldThrowAMissingExchangeRatesException()
{
    Action act = () => PortfolioWith(new Money(1, Currency.EUR), new Money(1, Currency.USD), new Money(1, Currency.KRW))
        .Evaluate(this.bank, Currency.EUR);
    act.Should().Throw<MissingExchangeRatesException>()
        .WithMessage("Missing exchange rate(s): [USD->EUR],[KRW->EUR]");
}

[Fact(DisplayName = "5 USD + 10 USD = 15 USD")]
public void Add_ShouldAddMoneyInTheSameCurrency()
{
    PortfolioWith(new Money(5, Currency.USD), new Money(10, Currency.USD))
        .Evaluate(bank, Currency.USD).Should().Be(new Money(15, Currency.USD));
}

We now can safely delete the AddOld method.

By now, you've noticed how easy it can be to 🔵 refactor when you have a test suite protecting you.

Only immutability