158

I have been working as a software developer for many years now. It has been my experience that projects get more complex and unmaintainable as more developers get involved in the development of the product.

It seems that software at a certain stage of development has the tendency to get "hackier" and "hackier" especially when none of the team members that defined the architecture work at the company any more.

I find it frustrating that a developer who has to change something has a hard time getting the big picture of the architecture. Therefore, there is a tendency to fix problems or make changes in a way that works against the original architecture. The result is code that gets more and more complex and even harder to understand.

Is there any helpful advice on how to keep source code really maintainable over the years?

13
  • 9
    highly recommend books : 'Software Project Survival Guide' by Steve McConnell , 'Rapid Development' by Steve McConnell, 'Refactoring' by Martin Fowler Commented Jan 10, 2012 at 10:20
  • 16
    ... and 'Clean Code' by Uncle Bob ;) (Robert C. Martin)
    – Gandalf
    Commented Jan 10, 2012 at 10:56
  • 35
    Isn't this very question something that has spawned several decades worth of heavy reading and entire courses at universities?
    – detly
    Commented Jan 10, 2012 at 13:46
  • 17
    @Eric Yin - I disagree on the comments. To me they are a code smell and in longer term projects tend to do more harm than good because they inevitably get of date and become misleading.
    – JohnFx
    Commented Jan 10, 2012 at 15:35
  • 8
    @Eric Yin: strive for self-documenting code. Use comments of intent only where they enhance understanding. Commented Jan 11, 2012 at 13:25

20 Answers 20

136

The only real solution to avoid code rot is to code well!

How to code well is another question. It's hard enough even if you're an excellent programmer working alone. In a heterogeneous team, it becomes much harder still. In outsourced (sub)projects... just pray.

The usual good practices may help:

  1. Keep it simple.
  2. Keep it simple. This applies especially to the architecture, the "big picture". If developers are having hard time to get the big picture, they are going to code against it. So make the architecture simple so that all the developers get it. If the architecture has to be less than simple, then the developers must be trained to understand that architecture. If they don't internalize it, then they shouldn't code in it.
  3. Aim for low coupling and high cohesion. Make sure everyone in the team understands this idea. In a project consisting of loosely coupled, cohesive parts, if some of the parts becomes unmaintainable mess, you can simply unplug and rewrite that part. It's harder or near impossible if the coupling is tight.
  4. Be consistent. Which standards to follow matters little, but please do follow some standards. In a team, everyone should follow the same standards of course. On the other hand, it's easy to become too attached with standards and forget the rest: please do understand that while standards are useful, they are only a small part of making good code. Don't make a big number of it.
  5. Code reviews may be useful to get a team to work consistently.
  6. Make sure that all tools - IDEs, compilers, version control, build systems, documentation generators, libraries, computers, chairs, overall environment etc. etc. - are well maintained so that developers don't have to waste their time with secondary issues such as fighting project file version conflicts, Windows updates, noise and whatever banal but irritating stuff. Having to repeatedly waste considerable time with such uninteresting stuff lowers the morale, which at least won't improve code quality. In a large team, there could be one or more guys whose main job is to maintain the developer tools.
  7. When making technological decisions, think what it would take to switch the technology; which decisions are irreversible and which are not. Evaluate the irreversible decisions extra carefully. For example, if you decide to write the project in Java, that's a pretty much irreversible decision. If you decide to use some self-boiled binary format for data files, that's also a fairly irreversible decision (once the code is out in the wild and you have to keep supporting that format). But colors of the GUI can easily be adjusted, features initially left out can be added later on, so stress less about such issues.
8
  • 8
    These are great points. I must admit that I struggle with "keep it simple".It seems to mean different things to different people in different contexts, which makes "simple" rather complex (but then I do have a natural tendancy to complicate things).
    – Kramii
    Commented Jan 10, 2012 at 10:06
  • 3
    I agree perfectly with your points, especially "KIS". But I see a tendency that more and more (younger?) developers use rather complex structures to describe even the simplest contexts.
    – chrmue
    Commented Jan 10, 2012 at 10:29
  • 1
    Yes, clean code, clean architecture and professionalism is the key. To achieve what Joonas proposed, the following resources might help you and others: Follow the the six Paths of the Code Scouts codescouts.dejung.id.au or better if you understand German use the original site and follow the principles and practices of the Clean Code Developers clean-code-developer.de
    – Gandalf
    Commented Jan 10, 2012 at 10:35
  • 10
    @chrmue: See "how to write Factorial in Java" ;-) Commented Jan 10, 2012 at 10:49
  • 2
    A really good system metaphor helps a lot. Even if people haven't spent a lot of time on the project it gives them an idea that hundreds of pages of architecture documents (which they won't read) will not. Commented Jan 10, 2012 at 22:17
54

Unit tests are your friend. Implementing them forces low coupling. It also means that the "hacky" parts of the program can easily be identified and refactored. It also means that any changes can be tested quickly to ensure they don't break existing functionality. This should encourage your developers to modify existing methods rather than duplicating code for fear of breaking things.

Unit tests also work as an extra bit of documentation for your code, outlining what each part should do. With extensive unit tests your programmers shouldn't need to know the whole architecture of your program to make changes and use exiting classes/methods.

As a nice side effect, unit tests will also hopefully reduce your bug count.

1
  • 3
    Very important point. I took over a legacy system, many classes, many lines of code, no documentation, no unit tests. After diligently creating unit tests for all code fixes and enhancements, the system design has evolved into a cleaner and more maintainable state. And we have the "courage" to rewrite significant core parts (covered by unit tests). Commented Jan 10, 2012 at 20:08
41

Everybody here is quick to mention code rot, and I completely understand and agree with this, but it still misses the bigger picture and the bigger issue at hand here. Code rot doesn't just happen. Further, unit tests are mentioned which are good, but they don't really address the problem. One can have good unit test coverage and relatively bug free code, however still have rotted code and design.

You mentioned that the developer working on a project has difficulty implementing a feature and misses the bigger picture of the overall architecture, and thus implements a hack into the system. Where is the technical leadership to enforce and influence the design? Where are the code reviews in this process?

You are not actually suffering from code rot, but you are sufferring from team rot. The fact is that it shouldn't matter if the original creators of the software are no longer on the team. If the technical lead of the existing team fully and truly understands the underlying design and is any good at the role of being a tech lead, then this would be a non-issue.

7
  • Very good point, you hit the bulls eye! Sad to say, but that's exactly what's happening here. And it seems to be impossible to change things without the tech lead...
    – chrmue
    Commented Jan 10, 2012 at 12:57
  • 4
    @chrmue Just the way things go I guess, but I am growing weary of it. In many ways I wish I was a junior developer again when I wasn't so aware of how wrong everything around me seems to be. It seems I am hitting my mid-career crisis early. But I am rambling... glad to help.
    – maple_shaft
    Commented Jan 10, 2012 at 13:11
  • 1
    @Murph Why shouldn't you have an all-knowing team lead during the Maintenance phase? Every boss I have ever had expected nothing less from a team lead regardless, and when I was a team lead I expected nothing less from myself.
    – maple_shaft
    Commented Jan 10, 2012 at 13:27
  • 1
    @maple_shaft because a) I don't assume that there is a team lead dedicated to that one project and that's more or less the first requirement and b) I think you need to understand the design and the implemenation (all of it) and that's hard. On the one hand we all argue that we shouldn't have a single font of all knowlege on our projects and yet here we are saying that we have to have one? That doesn't add up?
    – Murph
    Commented Jan 10, 2012 at 13:36
  • 2
    @maple_shaft probably me being a grumpy old programmer (-: But there is a problem in that it is often implementation "style" that needs to be followed deep in an existing codebase - and that may be alien to both coder and lead (for lots of real world reasons).
    – Murph
    Commented Jan 10, 2012 at 14:44
19

There are several things we can do:

Give one person overall responsibility for architecture. When choosing that person, ensure they have the vision and skill to develop and maintain an architecture, and that they have the influence and authority to help other developers follow the architecture. That person should be a seasoned developer who is trusted by management and who is respected by their peers.

Create a culture where all developers take ownership of the architecture. All developers need to be involved in the process of developing and maintaing architectural integrity.

Develop an enviroment where architectural decisions are easily communicated. Encourage people to talk about design and architecture - not just in the context of the current project, but in general, too.

The best coding practices make architecture easier to see from code - take time to refactor, to comment code, to develop unit tests, etc. Things like naming conventions and clean coding practices can help a lot in communicating architecture, so as a team you need to take time out to develop and follow your own standards.

Ensure that all necessary documentation is clear, concise, up-to-date and accessible. Make both high- and low-level architecture diagrams public (pinning them to the wall can help) and publically maintainable.

Finally (as a natural perfectionist) I need to recognise that architectural integrity is a worthy aspiration, but that there can be more important things - like building a team that can work well together and actually ship a working product.

2
  • 1
    Its a good idea to have one person being the root responsible for architecture. However you have a "team-smell" if that responsibility is used often: The team should naturally come to common conlusions, instead of relying on one person to provide the answers. Why? The total knowledge of the project is always shared, pinning it on one person will lead to bigger problems in the end: Only his view is satisfied, effectively cutting the wings of the rest of the team. Instead hire the best people and let them work it out together.
    – casper
    Commented Jan 12, 2012 at 15:44
  • 1
    @casper: Exactly. You expresses what I had in mind rather better than I did.
    – Kramii
    Commented Jan 12, 2012 at 23:12
18

The way I go about this issue is to snip it at the root:

My explanation will be using terms from the Microsoft/.NET, but will be applicable to any platform/toolbox:

  1. Use standards for naming, coding, checkins, bug flow, process flow - basically anything.
  2. Don't be afraid to say goodbye to team members who doesn't adhere to standards. Some developers simply cannot work within a defined set of standards and will become 5th column enemies on the battlefield to keep the code-base clean
  3. Don't be afraid to allocate lower skilled team members to testing for long periods of time.
  4. Use every tool in your arsenal to avoid checking in rotting code: this involves dedicated tools, as well as pre-written unit tests that test the build files, project files, directory structure, etc.
  5. In a team of about 5-8 members, have your best guy do refactoring almost constantly - cleaning up the mess the others leave behind. Even if you find the best specialists in the field, you will still have a mess - it's unavoidable, but it can be constrained by the constant refactoring.
  6. Do write unit tests and maintain them - do NOT depend on the unit tests to keep the project clean, they do not.
  7. Discuss everything. Don't be afraid to spend hours to discuss things in the team. This will disseminate the information and will remove one of the root causes for bad code: confusion on technologies, goals, standards, etc.
  8. Be very careful with consultants writing code: their code will, almost by definition, be the real shitty stuff.
  9. Do reviews preferably as the process step before checkin. Don't be afraid to rollback commits.
  10. Never use the open/close principle unless in the last stage before release: it simply leads to rotting code being left to smell.
  11. Whenever a problem is encounted, take the time to understand it to the fullest before implementing a solution - most code rot comes from implementing solution to problems not fully understood.
  12. Use the right technologies. These will often come in sets and be fresh: It's better to depend on a beta version of a framework you are ensured support from in the future, than to depend on extremely stable, but obsolete frameworks, that are unsupported.
  13. Hire the best people.
  14. Lay off the rest - you are not running a coffee shop.
  15. If management is not the best architects and they interfere in the decision making process - find another job.
6
  • 8
    Disagree with #3. Testing should not be seen as a punishment for the untrained. In fact it should be done by all devs, and counted as equally as important as the coding & designing! If you have Mr Untrained on the team, then get him off the team for some training!.
    – user25446
    Commented Jan 11, 2012 at 9:54
  • 1
    "Disagree with #3. Testing should not be seen as a punishment for the untrained." - It shouldnt and is not what I wrote, let me explain: Testing is a good way of letting people who you do not, yet, trust to commit changes to get into he code. The best testers I have found are the ones aspiring to become contributors of code and are proving their competency by looking through code, running the programme and shows the ability to correlate their findings in the runtime with the source code. It is not a punishment - its training.
    – casper
    Commented Jan 12, 2012 at 15:37
  • 1
    "Disagree with #10 - that helps with code quality if done correctly" This is absolutely false in my working experience: Code locked away cannot be refactored, meaning it will be staying in its current state until unlocked. This state can be verified to be working at some stage, but at a later stage this verification is a false positive: All code should be left open for refactoring up until the stage before final system test.
    – casper
    Commented Jan 12, 2012 at 15:37
  • 3
    @casper: IMHO, The open/closed principle should not be understood as "you can't change the source", but rather "design the code like it will become frozen". Make sure that it is possible to extend the code as nessisary without requring changes to it. The result is inheirently more loosly coupled and highly cohesive than average code. This principle is also crucial when developing a library for use by third parties, since they cannot just go in and modify your code, so you need it to be properly extensible. Commented Jan 12, 2012 at 21:00
  • 1
    @Kevin: Well if you are able to understand open/closed in the abstract "for the future" sense that you do here, then sure: All code should really be made in this way, even internal code. I have made it a habit to always follow the design guidelines for frameworks, even when I make an application. This however is not the definition of open/closed, as it reads in answers on this post, nor as it is defined on wiki: [the open/closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"]
    – casper
    Commented Feb 8, 2012 at 10:02
12

Clean rotted code by refactoring, while writing unit tests. Pay down (this) design debt on all the code you touch, whenever you:

  • Develop a new feature
  • Fix a problem

Greatly speed your test-first development cycle by:

  • Refactoring to convert code modules to a scripting language
  • Use fast, cloud-based test machines

Refactor code to use low coupling (of highly internally-cohesive units) by:

  • Simpler, (more) functions (routines)
  • Modules
  • Objects (and classes or prototypes)
  • Pure functions (without side effects)
  • Preferring delegation, over inheritance
  • Layers (with API's)
  • Collections of small, one-purpose programs which can operate together

Organic growth is good; big up-front design is bad.

Have a leader who is knowledgeable about the current design. If not, read the project's code until you are knowledgeable.

Read refactoring books.

0
11

Simple answer: you can't.

That's why you should aim for writing small and simple software. It's not easy.

That's only possible if you think long enough about your seemingly complex problem to define it in as a simple and concise manner as possible.

Solution to problems that truly are big and complex can often still be solved by building upon small and simple modules.

In other words, as others pointed out, simplicity and loose coupling are the key ingredients.

If that's not possible or feasible, you are probably doing research (complex problems with no known simple solutions, or no known solution at all). Don't expect research to directly produce maintainable products, that's not what research is for.

9

I work on a code-base for a product that has been in continuous development since 1999, so as you can imagine it is pretty complex by now. The biggest source of hackiness in our codebase is from the numerous times we've had to port it from ASP Classic to ASP.NET, from ADO to ADO.NET, from postbacks to Ajax, switching UI libraries, coding standards, etc.

All in all we've done a reasonable job of keeping the code base maintainable. The main things we have done that contributed to that are:

1) Constant refactoring - If you have to touch a piece of code that is hacky or hard to understand, you are expected to take the extra time to clean it up and are given the leeway in the schedule to do so. Unit tests make this a lot less scary, because you can test against regressions more easily.

2) Keep a neat development environment - Be vigilant about deleting code that is no longer used, and don't leave backup copies/working copies/experimental code exist in the project directory.

3) Consistent coding standards for the life of the Project - Let's face it, our views on coding standards evolve over time. I suggest sticking with the coding standard you started with for the life of a project unless you have time to go back and retrofit all the code to comply with the new standard. It's great that you are over Hungarian notation now, but apply that lesson to new projects and don't just switch mid-stream on that new project.

8

Since you've tagged the question with project management, I've tried to add some non-code points :)

  • Plan for turnover - assume that the entire development team will have disappeared by the time it hits its maintenance phase - no developer worth their salt wants to be stuck maintaining his / her system forever. Start preparing handover materials as soon as you have time.

  • Consistency / uniformity cannot be stressed enough. This will discourage a culture of 'go it alone' and encourage new developers to ask, if they are in doubt.

  • Keep it mainstream - technologies used, design patterns and standards - because a new developer to the team (at any level) will have more chance of getting up and running quickly.

  • Documentation - especially architecture - why decisions were made, and coding standards. Also keep references / notes / roadmaps into documenting the business domain - you would be amazed how difficult it is for corporate business to explain what it is they do to a developer with no domain experience.

  • Lay down the rules clearly - not just for your current development team, but think about future maintenance developers. If this means putting a hyperlink to relevant design and coding standard documentation on every page, so be it.

  • Ensure that the architecture and especially code layers are clearly demarcated and separated - this will potentially allow for the replacement of layers of code as new technologies come along, for example, replace a Web Forms UI with an HTML5 jQuery UI, etc., which may buy a year or so of added longevity.

7

One property of highly maitainable code is function purity.

Purity means that functions should return the same result for the same arguments. That is, they should not depend on side effects of other functions. Additionally, it is useful if they do not have side effects themselves.

This property is easier to witness than coupling/cohesion properties. You don't have to go out of your way to achieve it, and I personally consider it more valuable.

When your function is pure, its type is a very good documentation by itself. In addition, writing and reading documentation in terms of arguments/return value is much easier than one mentioning some global state (possibly accessed by other threads O_O).

As an example of using purity extensively to help maintainability, you can see GHC. It is a large project about 20 years old where large refactorings are being done and new major features are still being introduced.

Last, I don't like the "Keep it simple" point too much. You can't keep your program simple when you are modelling complex things. Try making a simple compiler and your generated code will likely end up dead slow. Sure, you can (and should) make individual functions simple, but the whole program will not be simple as a result.

1
  • 2
    Another name for what you describe is the property of being deterministic Commented Feb 20, 2014 at 20:41
6

In addition to the other answers, I'd recommend layers. Not too many but enough to separate different types of code.

We use an internal API model for most applications. There is an internal API that connects to the database. Then a UI layer. Different people can work on each level without disrupting or breaking other parts of the applications.

Another approach is to get everyone read comp.risks and The Daily WTF so they learn the consequences of bad design and bad programming, and they will dread seeing their own code posted on The Daily WTF.

6

Since many of these answers seem to focus on biggish teams, even from the outset, I am going to put my view as part of a two-man development team (three if you include the designer) for a startup.

Obviously, simple designs and solutions are best, but when you have the guy that literally pays your salary breathing down your neck, you don't necessarily have time to think about the most elegant, simple and maintainable solution. With that in mind, my first big point is:

Documentation Not comments, code should be mostly self documenting, but things like design documents, class hierarchies and dependencies, architectural paradigms, etc. Anything that helps a new, or even existing, programmer to understand the code base. Also, documenting those odd pseudo-libraries that pop-up eventually, like "add this class to an element for this functionality" can help, since it also prevents people from re-writing functionality.

However, even if you do have a severe time limit, I find that another good thing to keep in mind is:

Avoid hacks and quick fixes. Unless the quick fix is the actual fix, it is always better to figure out the underlying problem to something, and then fix that. Unless you literally have a "get this working in the next 2 minutes, or you're fired" scenario, doing the fix now is a better idea, because you aren't going to fix the code later, you're just going to move onto the next task you have.

And my personal favorite tip is more of a quote, though I cannot remember the source:

"Code as if the person that comes after you is a homicidal psychopath that knows where you live"

1
  • I've always found function and class comments to be helpful even if just to provide separation of code segments at function and class locations by using syntax highlighting. I very rarely put comments into function code, but I write a line for every class and function, such as /** Gets the available times of a clinic practitioner on a specific date. **/ or /** Represents a clinic practitioner. **/. Commented Oct 19, 2018 at 5:17
5
  • Be a scout. Always leave the code cleaner than you found it.

  • Fix the broken windows. All those comments "change in version 2.0" when you're on version 3.0.

  • When there are major hacks, design a better solution as a team and do it. If you can't fix the hack as a team, then you don't understand the system well enough. "Ask an adult for help." The oldest people around might have seen this before. Try drawing or extracting a diagram of the system. Try drawing or extracting the use cases that are particularly hacky as interaction diagrams. This doesn't fix it, but at least you can see it.

  • What assumptions are no longer true that pushed the design in a particular direction? There might be a small refactoring hiding behind some of that mess.

  • If you explain how the system works (even just one use case) and find yourself having to apologize of a subsystem over and over again, it's the problem. What behavoir would make the rest of the system simpler (no matter how hard it looks to implement compared to what is there). The classic subsystem to rewrite is one that pollutes every other subsystem with its operating semantics and implementation. "Oh, you have to groz the values before you feed them into the the froo subsystem, then you un-groz them again as you get output from the froo. Maybe all values should be groz'ed when read from the user & storage, and the rest of the system is wrong? This gets more exciting when there are two or more different grozifications.

  • Spend a week as a team removing warnings so that real problems are visible.

  • Reformat all the code to the coding standard.

  • Ensure your version control system is tied to your bug tracker. This means future changes are nice and accountable, and you can work out WHY.

  • Do some archeology. Find the original design documents and review them. They might be on that old PC in the corner of the office, in the abandoned office space or in the filing cabinet nobody ever opens.

  • Republish the design documents on a wiki. This helps institutionalize knowledge.

  • Write checklist-like procedures for releases and builds. This stops people having to think, so they can concentrate on solving problems. Automate builds wherever possible.

  • Try continuous integration. The sooner you get a failed build, the less time the project can spend off the rails.

  • If your team lead does not do these things, well that's bad for the company.

  • Try to ensure all new code gets proper unit tests with measured coverage. So the problem can't get much worse.

  • Try to unit test some of the old bits that are not unit tested. This helps cut back the fear of change.

  • Automate your integration and regression test if you can. At least have a checklist. Pilots are smart and get paid lots and they use checklists. They also screw up pretty rarely.

4

One principle that has not been mentioned but that I find important is the open / closed principle.

You should not modify code that has been developed and tested: any such piece of code is sealed. Instead, extend existing classes by means of sub-classes, or use them writing wrappers, decorator classes or using whatever pattern you find suitable. But do not change working code.

Just my 2 cents.

3
  • What if the business requirements expressed in working code change? A do-not-touch on poorly factored but technically 'working' code may hurt you in the long run, especially when it comes time to make necessary changes. Commented Feb 20, 2014 at 0:49
  • @jinglesthula: Open for extension means that you can add the new functionality as a new implementation (e.g. class) of an existing interface. Of course, poorly structured code does not allow this: there should be an abstraction like an interface that allows the code to "change" by adding new code instead of by modifying existing code.
    – Giorgio
    Commented Feb 20, 2014 at 6:52
  • Not changing old code is exactly how code rot occurs. If you depend on garbage, you can only produce garbage. Sometimes to clean up code rot, you have to be willing to throw away thousands of lines of code. The only thing in your way is deadlines.
    – Beefster
    Commented Jul 21, 2020 at 16:47
4

Read and then re-read Code Complete by Steve McConnell. It's like a bible of good software writing, from initial project design down to a single line of code and everything in between. What I like most about it is that it is backed up by decades of solid data; it's not just the next best coding style.

3

I've come to call this the "Winchester Mystery House Effect". Like the house, it started simple enough, but over the years many different workers added on so many odd features without an overall plan that nobody really understands it anymore. Why does this staircase go to nowhere and why does that door only open one way? Who knows?

The way to limit this effect is to begin with a good design that's made where it's flexible enough to handle expansion. Several suggestions have already been offered on this.

But, often you'll take a job where the damage has already been done, and it's too late for a good design without performing an expensive and potentially risky redesign and rewrite. In those situations, it's best to try to find ways to limit the chaos while embracing it to some degree. It may annoy your design sensibilities that everything has to go through a huge, ugly, singleton 'manager' class or the data access layer is tightly coupled to the UI, but learn to deal with it. Code defensively within that framework and try to expect the unexpected when 'ghosts' of programmers past appear.

2

Code refactoring and unit testing are perfectly fine. But since this long-running project is running in to hacks, this means that the management is not putting its foot down to clean the rot. The team is required to introduce hacks, because somebody is not allocating sufficient resources to train people and analyze the problem/request.

Maintaining a long running project is as much a responsibility of project manager as an individual developer.

People don't introduce hacks because they like it; they are forced by circumstances.

1
  • Forced by the circumstances? People introduce hacks because (a) they don't know better => needs coaching, (b) they do not see the bigger picture => communication, documentation and discipline needed, (c) they think they are smarter => that's the biggest hurdle to overcome (d) forced by the circumstances => quick hotfixes are ok when under time pressure, but someone has to take responsibility and clean the code up afterwards. Any other "circumstance" is simply BS. There are exceptions to that rule of thumb, but the most so-called exceptions spell "lazy".
    – JensG
    Commented Jun 5, 2014 at 8:17
2

I just want to place a non-technical issue and a (maybe) pragmatic approach.

If your manager doesn't care about technical quality (manageable code, simple architecture, reliable infrastructure, and so on), it becomes hard to improve the project. In this case it is necessary to educate said manager and convince to "invest" efforts into maintainability and addressing technical debt.

If you dream with code quality found in those books you also need a boss that is concerned about this.

Or if you just want to tame a "Frankenstein project" these are my tips:

  • Organize and simplify
  • Shorten functions
  • Prioritize readability over efficiency (when acceptable of course, and some efficiency gains are too miserable to be kept)

In my experience, programming is entropic rather than emergent (at least in the popular imperative-structured paradigm). When people write code to "just work" the tendency is to lose its organization. Now organizing code requires time, sometimes much more than making it just work.

Beyond feature implementation and bug fixes, take your time for code clean-up.

2
  • "...the project is doomed" -- in my experience, this isn't necessarily so. Alternative is to educate said manager and convince to "invest" efforts into maintainability and addressing technical-debt
    – gnat
    Commented Feb 5, 2014 at 19:09
  • Sorry, I couldn't hold myself of writing that as I already had an experience when the manager ignored all my advices of technical-debt. But I think you are right: about a year after I gave up that project the manager lost all his authority over the tech-team for his inability to manage them.
    – Eric.Void
    Commented Feb 5, 2014 at 19:30
1

I was surprised to find that none of the numerous answers highlighted the obvious: make the software consist of numerous small, independent libraries. With many small libraries, you can construct a big and complex software. If the requirements change, you don't have to throw the entire codebase away or investigate how to modify a big honking codebase to do something else than what it is currently doing. You just decide which of those libraries are still relevant after requirements change and how to combine them together to have the new functionality.

Use whatever programming techniques in those libraries that make the usage of the library easy. Note that e.g. any non-object-oriented language supporting function pointers is supporting actually object-oriented programming (OOP). So, e.g. in C, you can do OOP.

You can even consider sharing those small, independent libraries between many projects (git submodules are your friend).

Needless to say, each small, independent library should be unit-tested. If a particular library is not unit-testable, you are doing something wrong.

If you use C or C++ and dislike the idea of having many small .so files, you can link all libraries together into a larger .so file, or alternatively you can do static linking. The same is true for Java, just change .so into .jar.

1
  • This seems to be a bit too theoretical from my point of view. Of course the projects I mentioned in my question were constructed of multiple libraries and modules. My experience in the last 26 years of software development was that the older the project gets the initial organization
    – chrmue
    Commented Jul 21, 2017 at 9:29
0

Simple: reduce the maintenance costs of most of your code down to zero until you have a maintainable number of moving parts. Code that never needs to be changed incurs no maintenance costs. I recommend aiming for making code truly have zero maintenance cost, not trying to reduce the cost over many small and fussy refactoring iterations. Make it cost zero right away.

Okay, admittedly that's a lot, lot harder than it sounds. But it's not hard to get started. You can take a chunk of the codebase, test it, construct a nice interface over it if the interface design is a mess, and start growing the parts of the codebase that are reliable, stable (as in lacking reasons to change), while simultaneously shrinking the parts that are unreliable and unstable. Codebases that feel like a nightmare to maintain often don't distinguish the moving parts that need to be changed away from the parts that don't, since everything is deemed to be unreliable and prone to change.

I actually recommend going all the way of separating the organization of your codebase into "stable" and "unstable" parts, with the stable parts being a huge PITA to rebuild and change (which is a good thing, since they shouldn't need to be changed and rebuilt if they truly belong in the "stable" section).

It's not the size of a codebase that makes maintainability difficult. It's the size of the codebase that needs to be maintained. I depend on millions of lines of code whenever I, say, use the operating system's API. But that doesn't contribute to the maintenance costs of my product, since I don't have to maintain the source code of the operating system. I just use the code and it works. Code that I merely use and never have to maintain incurs no maintenance costs on my end.

Not the answer you're looking for? Browse other questions tagged or ask your own question.