or “How big is an interface?”… Uuuh, what?
Well, consider these two interfaces:
1 2 3 4 5 6 7 8 9 10 11 12 |
public interface ISomeService1 { void SetStuff(double value); void SetSomeOtherStuff(string anotherValue); void SetMoreStuff(bool moreStuff); void SetEvenMoreStuff(bool eventMoreStuff); } public interface ISomeService2 { SomeValue GetValue(); } |
– which is bigger?
If you think that ISomeService1 is the bigger interface, then there’s a good chance you’re wrong! Why is that?
It’s because an interface is not just the signatures of the methods and properties it exposes, it also consists of the types that go in and out of its methods and properties, and the assumptions that go along with them!
This is a fact that I see people often forget, and this is one of the reasons why everything gets easier if you adhere to the Tell, Don’t Ask principle. And by “everything”, I almost literally mean “everything”, but one thing stands out in particular: Unit testing!
Consider this imaginary API:
1 2 3 4 |
public interface ISomeKindOfValueStore { Values GetValues(string name); } |
Now, in our system, on various occasions we need to change the values for a particular name. We start out by doing it like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class BusinessLogic { ISomeKindOfValueStore someKindOfValueStore; public BusinessLogic(ISomeKindOfValueStore someKindOfValueStore) { this.someKindOfValueStore = someKindOfValueStore; } public void Run() { var values = someKindOfValueStore.GetValues("MegaCorp"); values.Value1 = 2.3; //< secret biz constants values.Value2 = 4.5; // more logic in here } } |
Obviously, if GetValues returns a reference to a mutable object, and this object is the one kept inside the value store, this will work, and the system will chug along nicely.
The problem is that this has exposed much more than needed from our interface, including the assumption that the obtained Values reference is to the cached object, and an assumption that the Value1 and Value2 properties can set set, etc.
Now, imagine how tedious it will be to test that the Run method works, because we need to stub an instance of Values as a return value from GetValues. And when multiple test cases need to exercise the Run method to test different aspects of it, we need to make sure that GetValues returns a dummy object every time – otherwise we will get a NullReferenceException.
Now, let’s improve the API and change it into this:
1 2 3 4 |
public interface ISomeKindOfValueStore { void SetValues(string name, double newValue1, double newValue2); } |
adhering to the “Tell, Don’t Ask” principle, allowing a potential usage scenario like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class BusinessLogic { ISomeKindOfValueStore someKindOfValueStore; public BusinessLogic(ISomeKindOfValueStore someKindOfValueStore) { this.someKindOfValueStore = someKindOfValueStore; } public void Run() { someKindOfValueStore.SetValues("MegaCorp", 2.3, 4.5); // more logic in here } } |
As you can see, I have changed the API from a combined query/command thing to a pure command thing which appears to be much cleaner to the eye – it actually reveals exactly what is going on.
And testing has suddenly become a breeze, because our mocked ISomeKindOfValueStore will just record that the call happened, allowing us to assert it in the test cases where that is relevant, ignoring it in all the other test cases.
Another benefit is that this coding style lends itself better to stand the test of time, as it is more tolerant to changes – the implementation of ISomeKindOfValueStore may change an in-memory object, update something in a db, send a message to an external system, etc. A command API is just easier to change.
Therefore: Tell, Don’t Ask.