No developer sets out to write unmaintainable code (unless they're a sociopathic software engineer who wants to watch the world burn). So why is it that once a development project is completed, it often looks way more complicated and over-architected than was probably intended when the team started out?
Every developer eventually learns the usual rules of software development: DRY, SOLID, Always code as if the person maintaining your software is an axe-wielding maniac who knows where you live... The message - directly or indirectly is "once you get this stuff under your belt, you can do no wrong and your code will become a shining beacon to all who follow". Unfortunately software projects - especially those undertaken by software vendors - often get in the way of good execution.
Regardless of whether your shop is running a pure DevOps model or still has a traditional delivery / support structure, the operational side of the business expects projects to be transitioned across in a solid, usable state and with the appropriate amount of documentation to back it up so that the client sees no discernible impact to service delivery and the quality of code is at least maintained or, preferably, improved. The difficulty here is that by the time a project is in a state to be moved into an operational phase, a lot of water will have flowed under the decision-making bridge. That project has a huge number of factors at play: time, scope, budget, resourcing, technologies... all of these factor in to the state in which a project leaves the delivery shop floor and becomes the responsibility of the maintenance crew.
The road to hell is paved with good intentions
Even simple projects can become unwieldy nightmares given enough time, client input or distributed contributions. It's easy to sacrifice readability and maintainability on the alter of code decoupling. Adding builder classes, factory factories and helper methods all the way down are often design decisions made in the purest of motivations and in isolation might look like perfectly reasonable judgements. Viewed in the solution's entirety however, the code base overall can become overweight and burdensome. Understanding how data gets from database to UI becomes a software equivalent of geocaching.
"Everyone wants to go the party but no one wants to stay and clean up" - Jason Harmon
Let's be clear about something first: this is nobody's "fault" and it's not a suggestion that "all projects end up this way". But it's very easy to do. Start with any web project. Obviously we want it to be testable so we'll ensure we're using a DI / IoC framework (e.g. Ninject or similar). But then we also want to ensure we're writing code that doesn't need to be refactored constantly so let's build tests that have factories and builders that instantiate actual business layer objects.
Unfortunately now there's a proliferation of interfaces because if the Interface Segregation Principle has taught us anything, it's that interfaces should have as few responsibilities as possible. We're injecting up to five or six in some of our constructors and the tests are getting a little silly as well. But the project's finished and it's time to hand this thing off to the Ops / Support team. Except the client has a very limited budget so the handover is crammed into as short a space of time now and the project team move on to their next client.
A lot can happen between starting a project, delivering it and supporting it over several years. Keeping the code maintainable over that long tail is bloody hard work. That's probably why so many blog posts have been written on this subject. It's not just a matter of saying "Oh, well just write it for other people to read". The code might make great sense today when you have the client's change request in front of you, but in 12 months another developer will be scratching her head, wondering WTF you were thinking when you wrote that. You might even be that developer!
Writing maintainable code means actually having to maintain it. Review parts that haven't been touched in over a year. Do they make sense today? Can I still follow the logic from the front end UI through to the data access layer? Is my documentation for this solution easy to find, clearly written and of a standard that someone can gain an adequate context from which to start working with the code? Are the branches and changes all cleaned up in source control and is there a solid change / release process?
I recently had an opportunity to pick up a solution that had more or less been the responsibility of a sole developer who found himself swamped with work and needed to move some solutions away to the support team in an effort to provide more responsive turnaround times and reduce the "single point of failure" risk that he presented.
On the face of it, the code was sound: it had good test coverage, was decoupled and generally looked like a straightforward piece of work. Once we got into it though, small things started to pop out: mocking frameworks had been abandoned in favour of test stub classes that built up dependencies and concrete objects. A "FooRepositoryStub" would have a Foo object created from another "FooBuilder.Build()" method call, which itself had other methods called "For" and "With". You'd ultimately end up with test setup fixtures with a pseudo-fluent type of structure like
FooBuilder().With(new BarBuilder().WithName("Fred" + i).Build()).Build();
in your method that you had to know to use and have seen an example elsewhere in the code.
This isn't a complaint necessarily about this code. It does what it's supposed to and is probably in line with the ideology the developer had at the time. But is it maintainable? Well, no. The expected design and behaviour pattern is different and therefore needs time for new team members to get used to; if they even subscribe to the rationale for the design at all! Comments were absent throughout, XML comments were missing so class members weren't as intuitive to work with, there was no documentation for much of the project ("The documentation is the scrum cards") so understanding the business problem and context was that much harder.
It sounds like a laundry list when it's read back like this but no single raindrop believes it is to blame for the flood. Small - almost insignificant - decisions over the course of the project mount up and cumulatively can create a more complex or cumbersome result that needs to then be maintained - often with a minimal budget - for several years afterwards.
Any project gathers technical debt at some point; it's often unavoidable. The debt isn't the problem though, it's whether a plan is in place to identify technical debt when it arises at any point in the project (from analysis & design, documentation, through to development and delivery) and how and when that debt should be paid down. Too often it's the sort of thing that is recognised after the fact, agreed to need attention and then never looked at again until the next developer uncovers it again several months - or even years - later. Maintainable code isn't just something that adheres to SOLID, DRY or is easily changed; it's having a plan to identify and correct sections that get in the way of delivering value quickly for the client. It's those areas; not the new features; that will incur the greater cost as time goes on.