A QUICK NOTE: This was supposed to be a single treatise on the coding and design principles that *I* think are most important for writing maintainable code. A draft of this has been on my hard drive for a long, long time and it's turning into my own great white whale. Just to get it out, I'm breaking it into pieces that will follow shortly, depending on ongoing bouts with writer's block. I'm going to intermix quick discussions of these design concepts with some case studies of systems I've built or worked with that illustrate both the positive outcome of following the principles and the pain incurred by failing to apply the principles. Some day, I'll gather the pieces back up into a single coherent article.
Enable Change or Else!
Change is a constant in an enterprise software system. A system that is expensive or risky to change is an opportunity cost to your business. Especially if you're a small company, poor code quality in your flagship product will jeopardize your company's future.
A couple of months ago I talked about my vision for creating a Maintainable Software Ecosystem in which I claimed that the single most important quality for an enterprise software system is maintainability, i.e. the ease in which a system can be modified or extended. I spent a lot of screen space talking about topics like source control, test automation, and build automation. There's a lot of supporting practices that can greatly aid in shipping working code, but I purposely put off the single most important factor — the Code! Maintainability will not happen without a commitment to quality code, now, and throughout the lifecycle of a system. I can always throw code quality to the wind and code faster now, but that slop will catch up to me or the next team in the future, and the future has a funny way of happening sooner than we expect. Besides, I've been the "next" team, and it wasn't pretty.
I spent much of the past two years at my previous job extending, restructuring, or flat out re-writing legacy code. I frequently saw my team's efforts hindered because of existing code that was poorly factored or just flat out hard to understand. We were consistently faster when we were working with the newer code that we wrote and designed inside of an Agile process than we were working with the older legacy code. Some of the disparity in productivity between new and legacy code was our familiarity with the newer code and the better build and test infrastructure of the newer code, but I'd still place much of the blame on the structural flaws of the legacy code. Ironically, and certainly not for the first time, I thought some of the biggest impediments to extending our system were directly attributable to well meaning attempts at creating extensibility in the existing code.
Extensibility yes, but how?
One solution for system maintainability is to build in "extensibility" points or use metadata-driven design approaches. It's great if the extensibility points match up well with the actual direction of the later change, but the wrong extensibility points can cause a lot of harm by making a design harder to understand or awkward to extend. David Hayden recently posted some frustrations with this style of design.
If it's true that most code spends much more time being maintained (changed) than the initial write, then it certainly behooves us to create code that can be changed. Extensibility points can certainly help, but the wrong extensibility points can also do plenty of harm, so they're not the whole answer. In My Programming Manifesto, I expressed a strong preference for creating maintainable code throughout the codebase rather than concentrating on specific extensibility points for future needs. I've always thought that the Simplest Thing That Could Possibly Work is largely true because the percentages say that you simply will not be able to anticipate a great deal of the future change with consistency. Overall, odds are that the best chance for successful maintainability is in creating "malleable" code that can be easily and safely changed in unforeseen ways. Instead of focusing on future proofing code in a few "strategic" spots, concentrate on making your code easy to change as a simple matter of course.
Coder to Craftsman
When people first learn how to write code they necessarily focus on just making the code work, with little or no thought for style or structure, and certainly no thought for the future. Any particular piece of code tends to land wherever the coder happened to be working when they realized that they needed that piece of code — or wherever the RAD wizards felt like dropping the code.
I think there is an inflection point where a coder mindlessly spewing out code transforms into a thoughtful software craftsman capable of creating maintainable code. That inflection point happens the day a coder first stops, lifts his/her nose out of the coding window, and says to him/herself "where should this code go?" That might also lead to questions like "how can I do this with less code?" or "how can I write this to make it easier to understand?" or even "how can I solve one problem at a time?" The rest of a developer's career is spent pursuing better and better answers to the question "where should this code go?"
My first "enterprise-y" system was an ASP Classic web application on top of Access to track project auditing for my engineering team. If you opened up any of the early ASP scripts from that application you'd see SQL statement construction mixed with post form handling, data access and HTML creation all intermixed. Business logic happening willy-nilly at various points in the ASP page whenever I was coding away and realized I needed some logic. A lot of functionality was duplicated because each ASP page was self-contained, causing more effort to write and then change the application. The pages themselves became difficult to understand because all the code was dumped into one bucket with no rhyme or reason. Troubleshooting business logic meant sifting through a lot of unrelated http handling code. Finding data access problems meant reading through quite a bit of the html templating along the way. The code stunk and I knew it, even as a coding newbie working solo. There had to be better ways to build the application. I improved things just by creating a set of common utility subroutines that could be used to reduce the amount of duplicated code. It wasn't much, but it was a start.
The Maintainable Code Checklist
So exactly how do I know "where this code should go" for maintainability? To guide your coding and design for maintainability, I've come up with a checklist of the half dozen questions in the table below that I think should be answered in the positive. There are three major themes running through the checklist — intention revealing code, getting rapid feedback from the code, and being able to do one thing at a time. Assuming that maintainability is important to you, it's also time to talk about the design principles that provide guidance to answering this Maintainable Code Checklist. In the table below I've tried to tie the maintainability questions to some of the design principles that will guide the thoughtful developer to answers. This certainly isn't a comprehensive list, but it's a start.
|Question||Yes comes from…|
|Can I find the code related to the problem or the requested change?||Good Naming, High Cohesion, Single Responsibility Principle, Separation of Concerns|
|Can I understand the code?||Good Naming, Composed Method, The Principle of Least Surprise, Can You See The Flow of the Code?, Encapsulation|
|Is it easy to change the code?||Don't Repeat Yourself, Wormhole Anti-Pattern, Open Closed Principle|
|Can I quickly verify my changes in isolation?||Loose Coupling, High Cohesion, Testability|
|Can I make the change with a low risk of breaking existing features?||Loose Coupling, High Cohesion, Open Closed Principle, Testability|
|Will I know if and why something is broken?||Fail Fast|
You can't help but notice that many of the principles are closely related. I think you could say that many of the principles are simply looking at the exact same problems from a different angle. Because of this, I'm going to first do a survey of the principles, then I'll try to illustrate the principles in code with some real life examples from my career.
It All Starts with Separation of Concerns
Separation of Concerns is the Alpha and Omega of design principles. No other design principle that I'm going to discuss — be it loose coupling, high cohesion, encapsulation, minimal duplication, or orthogonality — is possible without Separation of Concerns. Simply put, strive to do one thing at a time in your code. Layering. Divide and conquer. A lot of the other principles are about enabling a system to change with minimal effort and risk. Before you can even think about that, you need to be able to build the system, and then understand that code. The human mind and eye can only handle so much complexity at any one time. At a minimum, separating the traditional concerns of user interface, business logic, and data access into "layers" of the application can help minimize the complexity of any single piece of the code. The system is still as complex as it has to be, but you stand a much better chance of understanding the business rules or data access mapping in isolation than you could if it was all mixed together.
Back to my original ASP application. Like almost all ASP code circa 1998 there was no separation of concerns. Business logic, user interface presentation, and data access details were hopelessly intermingled in a single code file. It was difficult to "see" the business logic flow because it was obscured by all the intermingled html markup and ADO database manipulation. My first exposure to separation of concerns was the 3- or n-tier architectures for Windows DNA applications on Wrox's old ASPToday website (forget about the physical deployment options and let's just talk about logical layering here). At a bare minimum, layered systems should be easier to understand because you can look at presentation logic or business logic in isolation.
A couple of months ago at my previous job we were discussing the technical tasks necessary to localize the application with foreign language support. The company has essentially run out of growth room in the United States and was looking at the European market with great hope. Localizing the application, and supporting Unicode encoding for that matter, was a major opportunity for the company. One of the developers was repeating the typical opinion that localization is easy to do upfront and always much more difficult to retrofit. Sure, but in this particular case the flagship product was going to be extremely tedious to localize because they were building strings to display on the screen very deep within big stored procedures as well as the C# middle tier code. For that matter, code that created HTML text with string concatenation was intermixed with business logic. In that particular case, localization after the fact could have been made much less costly simply by a better separation of concerns. Instead of mucking around with every single area of the code, a good separation of concerns would have enabled us to focus strictly on the presentation layers to make the localization changes.
When you say layering, we probably come up with a knee jerk list:
- User interface, presentation, controller logic
- Business logic, rules, domain model
- Database and data persistence
- Service layer
That's an awfully good start, but I think the layering metaphor of higher layers talking to lower layers might not fit perfectly anymore. "Concern" or "Layer" might be interpreted too coarsely. Even within a single traditional "layer," you may have finer grained areas of concern that should be separated as well. I'll discuss this more in the section about the Single Responsibility Principle.
Orthogonality is more a goal than a principle. In geometry, two or more axis of movement are said to be orthogonal if a change in position on one axis does not effect the position in the other axis. If I walk north for a mile I've changed my latitude, but not my east-west longitude. The Pragmatic Programmers applied the term Orthogonality to software as:
The basic idea of orthogonality is that things that are not related conceptually should not be related in the system. Parts of the architecture that really have nothing to do with the other, such as the database and the UI, should not need to be changed together. A change to one should not cause a change to the other. Unfortunately, we've seen systems throughout our careers where that's not the case.
In essence, Orthogonality in software design is the ability to change one thing at a time. An orthogonal codebase allows different concerns to be changed independently. Another way to think about Orthogonality is that it's the ability to work in isolation with only one aspect of a system at a time.
A classic, positive example is the invention of Cascading Style Sheets (CSS) in the late 90's. When I first learned how to create html websites (in Frontpage 97!), I embedded all of the style properties directly into the html. Mixing the content and the structure of the content with the formatting of the content often made large scale websites hard to maintain. Moving presentation rules into a CSS stylesheet greatly simplified the creation of the html content, while also allowing the style of the web page to be more readily changed than before. CSS started to make the formatting and content of an html page orthogonal.
In a negative example, I inherited a codebase had very poor orthogonality between the user interface and the business logic. In this case we had a significantly complex piece of code that worked through business logic to build an html response inside code. One of the significant problems with this code was that the business logic could only be tested and verified through an examination of the resulting html. Everytime we had to change the user interface, we broke all of the automated tests that were intended to test the business logic. We weren't able to change business logic easily because we had html concatenation cluttering up the business logic. The very first mistake was a failure to separate the business logic and user interface concerns (<sarcasm>another tip, when you're rewriting a bad existing system, you might not want to reproduce the existing bad design in the new programming language</sarcasm>).
So how do you make Orthogonality happen? Tune in next time — assuming that I can break through my case of writer's block.