I ran into a situation where I was creating a testing
Mockito seemed like a good fit, since most of our classes were coupled to a wide number of concrete classes and there's very little in the way of interfaces anywhere. So, Mockito's ability to mock concrete classes (via CGLIB trickery) really saved my unit-test-writing-bacon. Requiring the team to introduce interfaces or injection everywhere would have made this effort DOA.
The trouble with mocking a complex class (many methods and many references) is that setting up all the stubs is not just tedious, it can be downright frightful. The astute reader has been chanting "time to refactor" several times by now, to which I would agree. But, refactoring means development time which means there are powers-that-be who must approve such an effort, and the battle to convince them of such benefits involve careful reasoning about long-term costs, technical debt, and things of this nature. So, let's invite Ward Cunningham to dinner and discuss ways to get that done some other time.
(Take note - I'm all about refactoring. So, if you're like me, you're already grabbing your monitor and screaming "refactor this crap!". But, this is a situation where refactoring is off the table, and if you've never encountered such a situation in commercial software development before, I have nothing but envy for you).
So, back to mocking a really complex class. The first/naive approach is to use the Mockito API to create our mock, and then use all of its slick JDK 1.5 methods to stub out values we're going to need. The problem is that we certainly don't want every unit test writer to have to do all this "setup code". Remember, we've got tons of methods. That means, there's probably redundancy (suppose there's one method returning a URL as a String, another as a URL, etc. -- not only does the writer have to mock both, he has to know and remember! to mock both). In short, if tests are painful to write, you can bet that tests won't get written (or worse, maintained).
The second approach is to use a Factory. This design pattern is probably in competition with Singleton for the most used. I say that, because sometimes this is OK, and sometimes this is just developers giving a knee-jerk response when the words "object creation" and "pattern" are used in the same sentence. The trouble with a Factory in this situation is that our unit test abstraction layer is meant to serve the needs of all unit test writers (present and future). With 50 methods and members on a class, there are an obscene number of object instance permutations to be handled. Factories aren't very good at obscene permutations. Factory won't cut down the unit test set up code very much.
This led me to my third approach -- the Builder pattern. Builders are a great fit here, since one of their strengths is providing the client code a great deal of control over object creation, without being involved in object construction. In other words, the ability to specify in great detail how an object gets built, while not seeing a single constructor, or any other part of that class' definition.
So, great, I'll have a Builder for every class that I need to mock. I'll let the unit test writer grab a builder, configure it, and then build their mock. Even better, they don't even need to know we're using Mockito. Shoot - they don't even know if we're using "real" objects, spies, mocks, dynamic proxies, subclasses, etc. All they know is that they asked for an instance of type X that has the specifications they provided. I'll be an even nicer guy and insure that the built objects have all kinds of very friendly default values, so most of the time, things "just work".
So, I was all happy and danced a jig. Clean, simple design - check. No coupling between unit tests and mocking library - check. Unit tests can get "set up" with the absolute minimum involvement of the test writer (only write as much as they deviate from the defaults). Time to write it up, and notify my coworkers. But wait, just to sort of "check myself" (I've got that tendency - it's some combination of personal paranoia and writing code for a while), I pull down a design pattern book off the shelf and look up the Builder pattern.
Oh, yuck.
What the heck is all this? Director, AbstractBuilder, ConcreteBuilder, the product, a Product interface, etc., etc. Part of me was saying to myself, "dude, you screwed this up", but the rest was saying, "what is all this stuff"? Where is Keep It Simple Stupid when you need him?
My takeaway (and the impetus of this post), is that sometimes even the seminal design patterns need a little trimming. Here's my justification with respect to the Builder pattern:
- Product. Obviously, need to build something. So, I keep this one.
- ConcreteBuilder. Similarly, need something to build it with. It stays.
- AbstractBuilder. Don't need it. I've got a ConcreteBuilder for every class I need, and those ConcreteBuilders know about their references, so the Products that get built are composites and life is good. Also, the codebase I'm dealing with has almost no inheritance, so the Builders map one-for-one to the types of Products I need. (Yes, I know, refactor!)
- Marker interface (for Product). Not appropriate. The users (unit test writers) do want to be coupled directly to the class being built. That's the point of the test.
- Director. In some sense, the unit test themselves are in this role. But, the point is there isn't a new class being introduced to support this.
The point is not that the Builder design pattern is all crazy-bloated, and the GoF are nutjobs. No, the point is that when building software, layers of abstraction and complexity should always be taken into account. While it's great to follow the Builder pattern to the letter (because you just say "Builder" and some other dev either knows what you mean, or goes and looks it up), it may be a fancier tool than you need. And, the worst thing you can do in software is exceed necessary complexity. I'll take maintaining software without a single pattern over software with excessive complexity any day.
I also don't truck with the notion of "if you don't have all those layers, then it won't be flexible enough down the road". My philosophy is I'd rather have just enough layers of abstraction to get through today. If I knew what tomorrow held, I'd own more stocks. Taken from a different angle, and borrowing someone else's phrasing (I couldn't pin down the original source):
There's no problem in Computer Science that can't be solved by adding another layer of abstraction to it. Except for the problem of too many layers of abstraction.
No comments:
Post a Comment