I am facing a BIG BALL OF MUD pattern (according the Foot and Yoder paper) at my current job attribution.
Interestingly that's not really classified as an Anti-Pattern, and yes the Product works at least.

My question is a bit different regarding the

It's not about the largeness, or legacy nature (there isn't really, we rule all of the source).
And also amazingly that code looks pretty well at the GUI and serves most of the User's requirements very well.

Though the underlying codebase is just horror from an architechtural POV.

Besides my colleagues already had done some efforts to heal the most blatant NIH syndromes (like inventing their own C++ std::string, and containers and such) of the former developers (which have finally all left now), I still have problems how to approach a totally intermingled software architecture. Intermingled by means of unclear division of GUI and Business Logic for example.

Also, there are weird factory/lookup constructs kind of constructs relying on class-id's and a base class.

Well, besides Refactoring and building Facades, I don't see really good options here, to drive that code to a really mature extensible and maintainable architecture.

Are there any (low cost) strategies recommended at that point?

  • I have accepted the duplicate as answering my problem. Not that it really solves it, because much of the good advice in the answers there won't be applicable for political and other reasons. Even worse the intended workflow is "Agile", which tends to even worsen the situation IMO. Though thanks for all the input given here. – πάντα ῥεῖ Apr 16 '17 at 10:37

What does an architectural point of view actually mean? A possible definition is "the total of all design decisions which are hard to change later" - if they are easy to change later - like replacing an own string class by std::string, they are implementation details. Typical architectural decisions are the choice of programming language, the choice of the framework, the decision about which kind of logic to implement in which layer of your application, or the component structure of a system (or the absence of the latter).

So the answer to your question

are there any (low cost) strategies

if obviously no, since "low cost" would mean "easy to change", thus self-contradictory to the meaning of "architectural".

Note I am not saying you cannot change or improve an architecture of a legacy code base. That is perfectly possible, but it will typically involve a lot effort, as described here. One obvious way is to rewrite the software or isolated parts of it from scratch (note I do not recommend this, however, there are situations where this is your only sensible option). The situation

unclear division of GUI and Business Logic

is often easier to solve. One has to refactor the not-GUI related business logic parts out of the GUI - one dialog/form/screen at a time. However, since you wrote the users are happy with the GUI, I recommend to apply such kind of refactorings only for the GUI parts which you really need to change, and not "just in case".

  • Meh. Programming languages & frameworks are implementation details too, not architecture. – RubberDuck Apr 17 '17 at 13:57
  • 3
    @RubberDuck: if you think so. But I guess most software engineers (including me) have a different understanding of the term "software architecture". So you think it will be easy to change the programming language used for the Linux kernel from C to something different? Good luck. – Doc Brown Apr 17 '17 at 14:07
  • Good architecture means I can completely through away a component and rewrite in a totally different language if that language would be better suited to solve the problem. Just my opinion. YMMV. – RubberDuck Apr 17 '17 at 22:23
  • @RubberDuck: you are talking about component structure beeing an architectural aspect, which is surely true. However, that does not make choice of language an "implementation detail", this is also an architectural aspect which cannot be changed "easily", by "a little bit of refactoring or search&replace", only by a full rewrite of the component for which one wants to switch the language. But YMMV ;-) – Doc Brown Apr 18 '17 at 6:11

My approach is to start splitting classes out into thier own libraries. Work out which classes are tightly coupled and the dependency graph for them.

Take out a class and recompile. see what breaks, add the new library, recompile... etc

This allows you to chip away at the system and get manageable chunks that you can further refactor into services or whatever later.

I don't think its possible to give more that general advice. Theres no magic one size fits all solution to 'change the architecture of this software from A to B'

  • I'm aware of that modus operandi, problem will be the availabe time to do that while catching up with the backlog. Thanks for your input anyways. – πάντα ῥεῖ Apr 16 '17 at 19:09

Most developers tend to see such tasks as architectural ones, while they are administrative in nature. To implement this properly, you should change workflow, not code! Consider, what would happen, if after all your fixes, old team would come back and continue their old ways? Most of your work would be wasted immediately!

Note that your command won't be able to achieve improvement without support from stakeholders. In most cases, this means that you are asking for impossible, as team leader is unable to communicate concept of technical debt to product owner. Your task as a developer is to collect objective proof, that current architecture decreases project's business value by slowing down progress and introducing bugs.

Do not change anything without a reason. Never start any change just because you don't like current implementation.

Introduce policies enforced by team leader and followed by everyone:

  • Continuous integration and pre-validation are not an option - they are life support.
  • Code review - no change should be merged until it passes whole test suite and a review by another developer.
  • Every change should improve architectural properties of code base. This implies any refactoring necessary to achieve a given task, but nothing more. The exact ways to so could be obtained from other answers, but should not. It's your team responsibility to both improve current design and keep change scope minimal. In particular, most integration tests do not require major rewrite, and testing should not be a reason for touching thousands of files introducing unit testability.
  • Every change should introduce more passing relevant tests (any kind of automated tests, not necessarily unit).
  • Every bug fix should include a regression test failing before change and passing after it.
  • With every code review , discuss most important decisions taken and reuse them in following reviews. Do not try to document them, it does not work, just build up your team's synergy and propagate it using reviews. Decisions may include some architectural changes proposed in other answers, exact result is irrelevant, just try to make use of your respected members experience.

These requirements enforce experience propagation and facilitate consistency in your code base. Unfortunately they rely highly on personal experience of your team members, so team success will be limited by your best senior. This is unavoidable, but can alleviated a bit by learning best practices form outside sources like this SE.

This does not mean, you are to leave the mess as is. For every request to change functional behaviour or fix a bug, investigate a scope of change. Scope should include necessary refactoring that would leave behind a cleaner state and good test coverage. Such scope would be big as long as code base is a mess. It will reduce in size if you properly follow boy-scout principle every time. The key to success is to reuse assets you already have, introducing new value incrementally.

  • This is a really important point! A "big ball of mud"-architecture is not the result of a single architectural mistake or oversight, it it the result of a dysfunctional process. – JacquesB Apr 18 '17 at 9:21

As said there is no "One Size Fits All" solutions but there are a couple of good practice or CYA, cover your assets, points to consider before making a start.

  1. Obtain or reverse engineer a functional specification and get it agreed in writing that it is what you need to aim for by all of the stake holders. Do not let anybody get away with "it just needs to do the same as the existing system!"
  2. Write a, preferably automated, set of tests that demonstrates that the current implementation fits those requirements, (or where it doesn't). Aim for as close to 100% behavioural coverage as you can.
  3. Identify a set of tools that can, again preferably automatically, give you metrics on things like level of coupling, code complexity, etc. and record those metrics for your starting point - the old system. It is nice if the metrics are widely used and have accepted desirable levels that your current code does not meet, e.g. I have a system to try and get sorted out that has a maximum McCabe of 650 - our coding standard says anything over 10 is undesirable, over 15 has to be justified and over 20 has to be fixed.
  4. Take a snapshot of the existing implementation that you can (re-)run tests on.
  5. Make sure you know where all of the existing system is, e.g. by getting it running on new test hardware - on such systems there are often hidden, undocumented, chunks of functionality - e.g. patched system libraries, utility or data files in undocumented and obscure locations, etc.

Given the above you can start to unwind the ball of string and separate the business logic from the GUI with some certainty that:

a) You are not changing the behaviour or that you are only changing it in desirable ways.

b) You will be able to demonstrate via your tests and metrics the improvements that you are making.

c) You may even be able to focus your refactoring effort on areas that the current implementation that remove undesirable or implement missing desirable behaviour from the existing implementation that way the stakeholders perceive that you are doing something.

d) When, not if, something stops working exactly the way that it did before and breaks a behaviour that someone else was counting on, "we knew it was a bug but we used it anyway" you have the agreed set of behaviour to point to.

Keep extending your test suite and re-running on the old implementation as well as the new.

Ensure that you are following really good source code version control practices as if/when you break something you need to be able to go back to where you were before you broke it easily and reliably.

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