In Appendix A to The Art of Unit Testing, Roy Osherove, speaking about ways to write testable code from the start, says,
An abstract class shouldn’t call concrete classes, and concerete classes shouldn’t call concrete classes either, unless they’re data objects (objects holding data, with no behavior). (259)
The first half of the sentence is simply Dependency Inversion from SOLID. The second half seems rather extreme to me. That means that every time I’m going to write a class that isn’t a simple data structure, which is most classes, I should write an interface or abstract class first, right? Is it really worthwhile to go that far in defining abstract classes an interfaces? Can anyone explain why in more detail, or refute it in spite of its benefit for testability?
3
That means that every time I’m going to write a class that isn’t a simple data structure, which is most classes, I should write an interface or abstract class first, right?
Yup.
Is it really worthwhile to go that far in defining abstract classes an interfaces?
No, but you should err toward more interfaces than less.
Can anyone explain why in more detail, or refute it in spite of its benefit for testability?
I would argue that it is fine to use concrete classes directly as long as those concrete classes are not part of the interface that the consumer is implementing/providing. As soon as they’re part of the public interface rather than the implementation of a module, they either need to be abstracted or they need to be stable enough that you’re sure that code isn’t going to need to change or be polymorphic.
I don’t think he means you should write an abstract class for each class, necessarily. I think he means there should be a way to substitute a test class for the concrete class. This might be done via a DI container, for example.
It’s usually worthwhile to go that far because you are usually going to actually write a test that substitutes a test class for the concrete class, but if you don’t need the test then you don’t need to bother.
IMHO a more pragmatic approach is not to think in “classes” when decoupling parts of your system for better testability, but to think in “components” – bigger functional units with a well-defined task which may be composed of one or more classes. If one component needs the service of another one, then it should not use it directly, but only via an interface. How this interface looks like can be very component dependent, it may be utilize the language definition of an interface
in your programming language, it may be just a function or call-back, it may use streams or some kind of “data objects”.
To my experience, it is also a good idea for testability to design your component interfaces in a way you can provide all input and output in-memory. For example, say you have component A which uses objects of your database as input and output, and component B, which relies on the output of A, and utilizes the database, too. Then provide a way that A produces the data objects just in-memory, and a way that B can accept those objects from A in-memory too, without any interaction with a database. That makes writing automatic tests for A and B a hell lot of easier (and the tests running much faster).
Its only worth going to those extremes if you drink the unit testing/TDD Kool-Aid. To make unit testing easier to accomplish certain designs have to be followed. Some of these designs go against traditional OO design concepts. It is up to you to decide whether the potential gains from unit tests out weigh the cost of having to make some poor design considerations. Unit tests are far from the silver bullet many try to make them out to be, but it is the current design fad.
5
The Gang of Four also imply that an abstract class or interface should be defined for every concrete class. Right after introducing the principle Program to an interface, not an implementation—and thus, as a direct consequence of it—they say (18):
Don’t declare variables to be instance of particular concrete classes.
This is not possible unless for every concrete class that is instantiated, an abstract class or interface exists.
Thus, it would appear that this is a case where designing for testability promotes what was already considered good object-oriented design. Given that Design Patterns came out almost two decades ago, I would say that the concurrence of the Gang of Four with Osherove is a sign that all that unit testing hasn’t made him sick in the head. Rather, he is simply promoting good design.
Design Patterns is one of the most highly respected books in object-oriented design, even now. So though, like with other programming principles, there is no reason to adhere to Osherove’s advice with a legalism that looses site of other aspects of good design such as KISS, his advice obviously stands in a tradition of well-respected thought on object-oriented design.