Collaboration and DIP
We know that this code violates the Dependency Inversion Principle (DIP):
public class Atm
{
private readonly InMemoryTransactions _inMemoryTransactions;
public Atm()
{
_inMemoryTransactions = new InMemoryTransactions(new List<Transaction>());
}
public void Deposit(int amount)
{
_inMemoryTransactions.Deposit(amount);
}
}
In particular, line 3 violates the principle by creating a dependency on a specific implementation, and line 7 violates it by knowing how to construct that dependency.
We can rewrite this code to apply the DIP:
public class Atm
{
private Transactions _transactions;
public Atm(Transactions transactions)
{
_transactions = transactions;
}
public void Deposit(int amount)
{
_transactions.Deposit(amount);
}
}
Line 3 now depends on an abstraction, and we use constructor injection to supply this dependency.
Inheritance and DIP
So how about this code:
public class LoggingInMemoryTransactions : InMemoryTransactions
{
private readonly Logger _logger;
public LoggingInMemoryTransactions(List<Transaction> initialTransactions, Logger logger) : base(initialTransactions)
{
_logger = logger;
}
public override void Deposit(int amount)
{
_logger.Log($"Deposit {amount}");
base.Deposit(amount);
}
}
Well, this class depends on a Logger abstraction, which is injected in the constructor, so that dependency seems to have been satisfactorily inverted.
However, in line 1 we can see that this class inherits from InMemoryTransactions. This inheritance is also a dependency, and we’re inheriting from a concrete instance, not an abstraction.
If we then look at the constructor on line 5, we can see that it calls the base constructor; in other words, it has to know how its base class is instantiated.
There is a direct parallel between these two cases: each of them has a dependency on a specific concrete class, which is instantiated in a specific way.
We can rewrite this code to use composition rather than inheritance:
public class LoggingInMemoryTransactions : Transactions
{
private readonly Logger _logger;
private readonly Transactions _transactions;
public LoggingInMemoryTransactions(Logger logger, Transactions transactions)
{
_logger = logger;
_transactions = transactions;
}
public void Deposit(int amount)
{
_logger.Log($"Deposit {amount}");
_transactions.Deposit(amount);
}
}
This simple example of the Decorator pattern behaves in exactly the same way as the previous implementation, but is now only dependent on abstractions.
Unit Testing
If we try to write unit tests around our code, we will soon see the benefit of DIP.
If we try to test our first implementation of Atm
, we find that our test boundary includes InMemoryTransactions
, as that class is a hard-wired dependency. We can’t test the behaviour of Atm
without also testing the behaviour of InMemoryTransactions
. If we also have tests of InMemoryTransactions
, then we may end up duplicated test scenarios.
In this case we have an additional problem: we have only defined read access to the data, so we have no way of testing this class without violating encapsulation:
using NUnit.Framework;
[TestFixture]
public class AtmShould
{
[Test]
public void Perform_deposit_transaction()
{
const int amount = 100;
var atm = new Atm();
atm.Deposit(amount);
// HELP! WHAT CAN I ASSERT AGAINST?
}
}
In the second implementation, we can write collaboration tests between Atm
and Transactions
, bringing our test boundary in much closer, and restricting the behaviour under test to that of Atm
.
using NSubstitute;
using NUnit.Framework;
[TestFixture]
public class AtmShould
{
[Test]
public void Perform_deposit_transaction()
{
const int amount = 100;
var transactions = Substitute.For<Transactions>();
var atm = new Atm(transactions);
atm.Deposit(amount);
transactions.Received().Deposit(amount);
}
}
Similarly, to test the first implementation of LoggingInMemoryTransactions
, we also have to test the behaviour it inherits from InMemoryTransactions
, as one is inseparable from the other. Again, if we also have tests of the base class, then we may end up with duplicated test scenarios.
using NSubstitute;
using NUnit.Framework;
[TestFixture]
public class LoggingInMemoryTransactionsShould
{
private LoggingInMemoryTransactions _loggingInMemoryTransactions;
private Logger _logger;
private const int Amount = 100;
[SetUp]
public void SetUp()
{
_logger = Substitute.For<Logger>();
_loggingInMemoryTransactions = new LoggingInMemoryTransactions(new List<Transaction>(), _logger);
}
[Test]
public void Log_transaction()
{
_loggingInMemoryTransactions.Deposit(Amount);
_logger.Received().Log($"Deposit {Amount}");
}
[Test]
public void Store_deposit()
{
_loggingInMemoryTransactions.Deposit(Amount);
// HELP! HOW CAN I CHECK THIS WAS SAVED?
// I might be tempted to assert against the new List<Transaction>(), but this would leak implementation details.
}
}
In the second implementation, the specific behaviour of LoggingInMemoryTransactions
is separated, and we can write collaboration tests independently of the behaviour of the inner implementation.
using NSubstitute;
using NUnit.Framework;
[TestFixture]
public class LoggingInMemoryTransactionsShould
{
private LoggingInMemoryTransactions _loggingInMemoryTransactions;
private Logger _logger;
private Transactions _transactions;
private const int Amount = 100;
[SetUp]
public void SetUp()
{
_logger = Substitute.For<Logger>();
_transactions = Substitute.For<Transactions>();
_loggingInMemoryTransactions = new LoggingInMemoryTransactions(_logger, _transactions);
}
[Test]
public void Log_transaction()
{
_loggingInMemoryTransactions.Deposit(Amount);
_logger.Received().Log($"Deposit {Amount}");
}
[Test]
public void Store_deposit()
{
_loggingInMemoryTransactions.Deposit(Amount);
_transactions.Received().Deposit(Amount);
}
}
Updated 26 May 2016: I’ve given code examples for the unit tests we might write in each case, and have removed DateTime createdOn
from the example to avoid confusing the issue.
Updated 26 September 2017: I’ve removed a reference to an individual whose political views are not consistent with the spirit of this blog.