For 4 years, I worked in the insurance software industry. I still can’t tell you how I ended up there. I had a nice string of fun jobs writing home-control software (sorry if I woke you) and email distribution systems (sorry if I spammed you), and one day I just couldn’t stand life anymore with a particular employer and I started looking for a new job where I ended up…I ended up writing Workers’ Compensation Insurance software.
During my first week, I spent my time absorbing code. My desk was covered in printouts of our software that I’d marked up in pen to high heaven. This class calls that class which creates a factory which returns a command which calls another class which makes a request that queues a call to an anonymous class which runs a threaded method that…wait, what were we trying to do again? I’ve lost track.
There’s something about writing so-called enterprise code that seems to make developers want to hide all the concrete functionality deep underneath an unimaginable number of abstracted layers. I HATE IT! I hate it and I want it to stop. Enterprise has nothing to do with abstraction so take your abstraction and EAT IT. Enterprise is about writing rock solid, correct code and hiding your functionality behind 3 interfaces, 4 abstract classes, 2 proxies, and 2 abstract factories is not conducive to reliable code.
Let’s walk through what leads to over-abstracted code and how it can be stopped.
On every interview I give, I always ask the candidate about design patterns. Do they use them? How many do they know about? QUICK, WHAT’S THE VISITOR PATTERN AND WHEN HOW COULD IT BE APPLIED?? The way I see it, all good developers know some design patterns, but that doesn’t mean all developers that know some design patterns are good.
Factories, Template Methods, Bridges, Adapters, these are all well and good, and I’ve used most of them, but DO NOT IMPLEMENT THESE ON FIRST PASS! Write your code in iterations. The first time through, make your code works as simply as possible. Once it’s working correctly, identify the sections of your code that are candidates for future extension and refactor — only then should you be using these patterns if they are appropriate.
If you opt to implement th design pattern on first pass, you may find that the abstraction needs to change later due to:
- …changes to the inheritance hierarchy.
- …refactoring which allows you to narrow several code paths into just one.
- …functional changes.
- …requirements changes.
The problem is that when you run into one of these problems, you are most likely not going to rollback the abstraction you created in the first place. The extra time the abstraction took to write, plus the pride you take in having done it in the first place, handcuffs you to the implementation.
Your best bet is to write the code with basic logic and inheritance the first time and then, once everything is working, only then refactor the parts that need it. When starting from working code, you can make more informed decisions about what the right level of abstraction should be.
In fact, this is the soul of Agile design. We make our best decisions when we have the most information. You don’t buy a car based on a spec sheet. You get in the car first. You drive the car first. Then, you have more information to go on. You’ll make a better decision on if that car is the right fit for you. The difference is deciding to buy a var based on reading how much horsepower it has versus feeling how much horsepower it has. So, why would you create abstractions to support functionality that you don’t require yet? Why would you implement redirection when you haven’t written what it is you’re redirecting to?
Are you really in a position to make the best choice yet?
Code From the Bottom Up
Good Design™ generally dictates we code in a top-down approach, progressing our levels of abstraction generally lower as classes implement methods which call other methods. However, this leads to premature abstraction that can lead to the kind of unexpressive code that I’ve already described. One way to keep ourselves on the straight-and-narrow, ironically, is to intentionally code in the opposite direction.
Let me explain.
Say you need to write a key/value persistence system. You might conceptualize, and implement, the system with a top-down approach:
I want to be able to save key/value pairs. These can come from a user, or just be specified by the system. A flat file would work for now, but a database could be a better choice in the future. It’d also be nice to have an in-memory store. I got it! I’ll write an interface, IKeyValueStore, and then create a factory that will return one of many possible stores.
And this is perfectly reasonable. Good thought has been put in to this decision. You go about writing it. You go ahead and design the interface to be as implementation-agnostic as possible. The key/value pairs are easy, and save() is obvious. What about loading? You decide that should be kept private since you’ll need to think about database credentials and file a file path for the various implementations. Next, you set to work creating all three stub implementations. You create the factory that returns one of the three. You only fully implement the flat file implementation for now because that’s all you need.
But now, how will you pass the path to the flat file in? Is it specified in the factory? Or is it in settings? Wait! That’s what’s being written. And now you’re making a lot of decisions and you still don’t have a working key/value persistence system.
Contrast that with this line of approach:
I want to be able to save key/value pairs. This will initially be a flat file. I’ll write that. If we need other methods of persistence later, I’ll refactor.
Feel strange? It feels like we went to implementation too quickly, doesn’t it? We feel like writing a concrete class so soon is sorta dirty. Surely there will be some tight coupling as a result. And what happens if we later want to switch to a database? This can’t be right.
And if this was all we ever did, it would be wrong. In reality, we implement the file-based store and hardcode a file path in. But, as we do so, we realize that in order to unit test appropriately we’ll pull need to pull the file path into the constructor so we can better specify where it goes.
See how that decision was much easier to make? We based it on real needs, not theoretical desires. The decisions “bubble up.”
In a short time, we have a working key store and a clear mind. The application uses the file-based key store concretely; does this feel right. Why or why not? Perhaps we forsee changes later. So what? Why do we need to try to predict the future? When the time comes to add a new kind of key/value store, we can do it. So let’s fast-forward now…
New requirements have come and instead of a database store we want a networked REST store so that the settings can be shared via the Cloud. We go to our implementation and determine what needs to change: three of our methods assume a synchronous implementation and we obviously need a new constructor. Since we’ll have two key/store implementations now, we also need to pull out an interface. The new interface now supports asynchronous calls and we write the new REST implementation for it.
See what we did? We haven’t wasted any effort. As we’ve gone along, we’ve used our experience to anticipate future changes by writing flexible code, but we haven’t written any code for imaginary requirements. You see, anticipating change in the future is far different than prematurely building to it. Instead of trying to predict the future by writing high-level code before the low-level, we’re letting the real requirements of the application drive where the abstraction goes. Our decisions are made easier because we’re reacting to real stimuli.
Give It A Try!
The next time you find yourself having to write a complex system, start with the low-level classes and work your way up. You’ll find that the decisions are easily solvable when you have the benefit of existing, working code. Ideating on the theoretical leaves too many unknowns for you to make the best choices.
The end result will be concise, get-to-the-point code with no unnecessary abstraction because you only added the abstraction you needed, not what you anticipated.
Now come on, enterprise. Is that so hard?