I've been working on a technical assessment of a system for a new client during the last few weeks. As I looked at line after line of the source code they gave me, I saw test-driven design (TDD), inversion of control (IOC), dependency injection (DI), and plenty of other TLAs (three letter acronyms). I saw “convention over configuration.” I saw layer upon layer of abstraction. There was more unit test code than code. Code coverage was very high. Marvelous! I can almost hear some of you salivating.
I had seen this type of project before. It had clearly been built by developers who “get it.” Super-developers who write superior code. But the project was 300% over budget, 2 times over schedule and it still wasn't done. Worse, it wasn't stable enough for a beta release and there were concerns about performance and scalability. You might expect this kind of result from an inexperienced team, but not from super-developers who “get it.” The client was in trouble and needed help.
In my last column, I talked about the idea that, “It is what it is” (regardless of what we think it should be). This project was failing. As a result of following all of the latest trends and chasing ideals, common sense suffered. Junior and mid-level developers couldn't follow the code. It took months to get a new developer up to speed. There was elaborate code for doing simple things. Developers spent entire sprints re-factoring code or fixing tests with nothing new to show for it. They showed up for reviews where the app didn't even run but assured us that, “all of the tests pass.” Obviously, I just couldn't see all of the goodness that was plainly there. “You just don't understand,” I was told.
I was clearly missing something. I started discretely checking around with other managers in the business. At first, I was afraid of looking like an idiot, but the more people talked, the more I found that I was not alone. “Between you and me….” They didn't want to look like idiots either. Who in his right mind would speak out against best practices? “Between you and me, we had some of those guys too, but they could never get anything done. We ended up letting them go,” they said. The emperor wasn't wearing any clothes after all.
Before you get all up in arms and fetch the torches to burn the heretic, let me say this. I see the goodness potential, but potential does not equal results. You can't lose focus on what counts: software that gets used. You have to meet deadlines, your co-workers have to be able to maintain your code, the app has to run and the users have to use it. Everything else comes after.
A lot of design patterns that were developed to solve legitimate problems have become anti-patterns, and I think unit testing was the biggest offender in this case. I think unit testing is necessary and nothing short of wonderful when used well, but in a typical business app, only about 20-30% of the logic needs to be tested. Calculations need to be tested and state machine transitions need to be tested. Assigning values to properties does not need to be tested. If statements do not need to be tested. Having 85% code coverage on a business app is an anti-pattern.
The saddest casualty of the unit test crusade has to be the code that's been bent out of shape in the name of testing. IOC and DI have their purpose, but testing is not it. Declaring interfaces for every class and configuring boot-strappers to magically fire up the right concrete class for each interface and having those concrete classes magically passed into class constructors seemingly from nowhere is mind-blowing for a lot of developers. They'll have to understand how it all works to know why F12
won't help them find the class they're firing up and why they're not getting the implementation they expected at runtime. That's going a long way around the block just to make testing easier.
If you need to stub out a method call inside a method you're testing, why not use a static factory class? It's simple, easy to understand, and easy to debug. Or even better, use something like Shims in the new Fakes capabilities in Visual Studio. Products that do this type of thing have been around for a long time. They let you stub out method calls within the method you're testing right inside your tests. Hey, we tried IOC, DI, and convention over configuration for unit testing. It didn't work out. Learn from it. Take the good stuff. Leave the stuff that didn't work. Move on.
Over-architecture has to be the second biggest anti-pattern I see killing projects. Complexity is a project killer. Any half-decent developer should be able to follow the code and work on the project with minimal ramp-up time. If it takes months to ramp up on a project or years to get good at working on the project, then it's bad code. Period. I see too many projects where even the most senior developers have a hard time figuring out how find their way around and they spend precious days uncovering the original developer's clever “time-savers” and abstraction layers.
Let's not forget that even the best patterns, practices, frameworks, tools, and add-ins can't keep you from doing stupid things. With all of the advanced techniques I'd seen in this project, one really basic mistake kept popping up: The project was a tangle of interdependencies. Everywhere I looked, I found places where the app wasn't built in discrete, independent layers. I saw data access objects spun up and used in HTML (Razor) pages. I saw business objects with dependencies on the UI. I saw circular references. I saw spaghetti.
The project ended up being a “do over.” It was less expensive to scrap everything and start fresh. We put together a new team who cared more about useful code than ideals. I'm happy to say that the initial version of the rewrite is about to be rolled out. It's far less complicated, faster, more scalable and better looking than the original and took only a fraction of the time to build and, most importantly, our client is happy.
Anyway, that's been my experience. You can get the torches now and burn the heretic for speaking out against best practices and the latest trends if you like, but remember, software isn't judged by how well it pleases the developer. It's judged by how well it pleases the customer.
Mike