Roy Osherove has noted a discrepancy between pure object-oriented design and testable design principles, and suggests that it's time to revisit how we think about software design. His current example is how the FxCop team has decided to internalize an apparently rather large portion of their API. Although I don't want to go into the particular discussion about FxCop, I can feel Roy's pain. Some of the APIs put out by ISVs (including Microsoft, I'm afraid) are notoriously hard to subject to unit tests, and often, this is because they take the principle of encapsulation too far.
Roy suggests adding the word test to object-orientation, turning it into Object Oriented Testable Design (OOTD) and Object Oriented Testable Programming (OOTP). However, I'd humbly like to suggest that we call it Testable Object Oriented Design (TOOD) and Testable Object Oriented Programming (TOOP) - that's makes for some more pronouncable acronyms 🙂
In any case, I'm all for it!
Although the term is new (to me, at least), I've been using TOOP for years, so here's my take on it. The first thing we need to establish is that TOOD must embrace the best of both worlds. Although testability is important, so are many of the design principles of OOP, which means that we can't just blindly sacrifice OO principles for testability. If we do that, we are just test-driven, but not following TOOP. Obviously, the reverse is also true.
This means that to apply TOOD, you must be able to answer both of the following questions:
- Is my API testable?
- Is my API object-oriented?
If you can't answer yes to both questions, your API is not a TOOD.
The first question is quite easy to answer: In its extreme, testability is a binary feature of an API: It's either testable or it's not. On the other hand, whether an API is object-oriented or not is a lot more fuzzy. This is where I become a pragmatic idealist: If it's not testable, it's certainly not a TOOD. In practice this means that testability is a much more fixed feature of TOOP than object-orientation: Whenever we have an unresolvable conflict between testability and object-orientation, testability must win out.
This doesn't mean that I think that object-orientation is secondary to testability in importance. Note that I wrote unresolvable conflict. The trick is to not have any conflicts that are unresolvable. How do you achieve this quality in practice?
In my experience, utilizing test-driven development (TDD) is a very good tool in this regard, as this very effectively ensures that your API is testable; you will very quickly discover if it's not 🙂 Every time you make an API design decision, you must then also think about whether it makes sense in a object-oriented way, since it not okay to expose members or types that should have remained hidden, just for the sake of testability.
Most of the discrepancies noted in Roy's original post concerns the conflict between testability and encapsulation. Although I agree that you can certainly have too much encapsulation, you can certainly also have too little. Each time you decide to expose an internal implementation detail, you make it part of the public API - not just for test purposes, but for all developers using your API. That can make refactorying very hard if you also want to maintain backwards compatibility.
Back at TechEd: Developer 2006, I had a rather interesting discussion with Udi Dahan about encapsulation vs. testability, where Udi's stance was that encapsulation was secondary to testability if you don't write frameworks or other APIs for mass consumption (I hope I don't misrepresent Udi here). This is a standpoint I meet a lot. To paraphrase:
The .NET framework design guidelines [which are, incidentally, policed by FxCop] are nice, but they don't apply to enterprise development, since I own all clients of my API, so I don't need to worry about backwards compatibility, etc.
In my book, all production code should be treated as framework code, even if I'm the only developer, and I'm writing and maintaining all upstream callers myself. Maintainability is excactly the main point here: If I follow a consistent set of conventions for my API, it will be much easier for me to return to it half a year later, and then still immediately understand what's going on. Someone else would in theory be able to do the same, since I follow conventions that should be broadly recognized.
Although FxCop (or the Code Analysis feature of Visual Studio) doesn't capture all object-oriented principles, it does capture a lot of them, so using this tool rather rigorously ensures a good degree of OOD.
In practical terms, then, a good approach to TOOP is TDD combined with Code Analysis, and I hope it goes without saying that suppressing code analysis violations to enable testability is not allowed 🙂