If you are using OOP in a modern language, the biggest danger is typically not "spaghetti code" but "ravioli code". You can end up dividing and conquering problems down to where your codebase is composed of itsy-bitsy pieces, teeny functions and objects, all loosely coupled and performing singular but teeny responsibilities, all tested against unit tests, with a spiderweb of abstract interactions going on that makes it so difficult to reason about what's going on in terms of things like side effects. And it's easy to stubbornly think you engineered this beautifully, since the individual pieces might indeed be beautiful and all adhere to SOLID, while still finding your brain on the brink of exploding from the complexity of all the interactions when trying to comprehend the system in full.
And while it is very easy to reason about what any one of these objects or functions do individually, since they perform such a singular and simple and maybe even beautiful responsibility while expressing at least their abstract dependencies through DI, the problem is that when you want to analyze the big picture, it's hard to figure out what a thousand teeny things with a spiderweb of interactions ultimately add up to do. Of course people say, just look at the big objects and big functions that are documented and don't drill down to the teeny ones, and of course that helps to at least comprehend what's supposed to happen in a high-level kind of way...
However, that doesn't help so much when you need to actually change or debug the code, at which point you have to be able to figure out what all these things add up to doing as far as lower-level ideas like side effects and persistent state changes and how to maintain system-wide invariants. And it's pretty hard to piece together the side effects that go on between interactions of thousands of teeny things whether they're using abstract interfaces to communicate with each other or not.
So the ultimate thing that I found to mitigate this issue is actually entity-component systems, but that might be overkill for many projects. I've fallen in love with ECS to the point where now, even when I write small projects, I use my ECS engine (even though that small project might only have one or two systems). However, for people not interested in ECS, I've been trying to figure out why ECS simplified the ability to comprehend the system so much and I think I'm onto some things that should be applicable for many projects even when they don't use an ECS architecture.
A basic start is to favor more homogeneous loops which tends to imply more passes over the same data, but more uniform passes. For example, instead of doing this:
for each entity: apply physics to entity apply AI to entity apply animation to entity update entity textures render entity
... it somehow seems to help so much if you do this instead:
for each entity: apply physics to entity for each entity: apply AI to entity etc.
And that might seem wasteful looping over the same data multiple times, but now each pass is very homogeneous. It allows you to think, "All right, during this phase of the system, nothing is going on with these objects except physics. If there are things being changed and side effects going on, they are all being changed in a very uniform way." And somehow I find that helps to reason about the codebase so, so much.
While it does seem wasteful, it can also help you find more opportunities to parallelize the code when uniform tasks are being applied over everything in each loop. And it also tends to encourage a greater degree of decoupling. Just by nature when you have these divorced passes that don't try to do everything to an object in one pass, you tend to find more opportunities to easily decouple the code and keep it decoupled. In ECS, systems are often completely decoupled from each other and there's no outer "class" or "function" manually coordinating them together. The ECS also doesn't suffer repeated cache misses necessarily since it doesn't necessarily loop over the same data multiple times (each loop might access different components located completely elsewhere in memory, but associated to the same entities). The systems don't have to be manually coordinated since they're autonomous and responsible for the looping themselves. They just need access to the same central data.
So that's one way to get started that can help you establish a more uniform and simple kind of control flow over your system.
Another is to reduce the dependency on event-handling. Event-handling is often necessary to figure out external things that happened without polling, but often there are ways to avoid cascading push events that lead to very hard-to-predict control flows and side effects. Event-handling, by nature, tends to deal with complex things happening to one tiny object at a time, when we want to focus on simple and uniform things happening to many objects at a time.
So, for example, instead of an OS resize event resizing a parent control which then starts pushing resize and paint events for each child which might cascade more events to who-knows-where, you might only trigger resize events and mark the parent and children as
dirty and needing to be repainted. You might even just mark all the controls as needing to be resized at which point a
LayoutSystem might pick that up and resize things and trigger resize events for all relevant controls.
Then your GUI rendering system might be woken up with a condition variable and loop through the dirty controls and repaint them with a broad pass (not an event queue), and that whole pass is focused on nothing except painting a UI. If there's a hierarchical order dependency for repainting, then figure out the dirty regions or rectangles and redraw everything in those regions in proper z-order so that you don't have to do a tree traversal and can just loop over the data in a very simple and "flat" fashion, not a recursive and "deep" fashion.
It seems like such a subtle difference but I do find this quite helpful from a control flow standpoint for some reason. It's really about reducing the number of things that happen to individual objects at one time, trying to aim for something similar to SRP but applied in terms of loops and side effects: the "Single-Task Loop Principle", "The Single Type of Side Effect Per Loop Principle".
This type of control flow let's you think about the system more in terms of big, hefty but extremely uniform tasks applied in loops, not all the functions and side effects that can go on with an individual object at a time. As much as this might not seem like it would make a huge difference, I found it made all the difference in the world at least as far as my own mind's ability to comprehend the behavior of the codebase in all the areas that mattered when making changes or debugging (which I also found much less need to do with this approach).
Dependencies Flow Towards Data
This is probably the most controversial part of ECS, and it may even be disastrous for some domains. It is in direct violation of the Dependency Inversion Principle of SOLID which states that dependencies should flow towards abstractions, even for low-level modules. It's also in violation of information hiding, but for ECS at least, not nearly as much as it appears since typically only one or two systems will access any given component's data.
And I think the idea of dependencies flowing towards abstractions works beautifully if your abstractions are stable (as in, unchanging). Dependencies should flow towards stability. However, at least in my experience, abstractions often weren't stable. Developers would never get them quite right and would find need to change or remove functions (adding wasn't too bad) as well as deprecating some interfaces a year or two later. Clients would change their minds in ways that break the careful concepts the developers built, bringing down the abstract factory for the abstract house of abstract cards.
Meanwhile, I find data to be far more stable. As an example, what data does a motion component need in a game? The answer is quite simple. It needs some kind of 4x4 transformation matrix and it needs a reference/pointer to a parent to allow motion hierarchies to be created. That's it. That design decision could last the lifetime of the entire software.
There might be some subtleties like whether we should use single-precision floating-point or double-precision floating-point for the matrix, but both are decent decisions. If SPFP is used, then precision is a challenge. If DPFP is used, then speed is a challenge, but both are good choices that don't need to be changed or necessarily hidden behind an interface. Either representation is one we can commit to and keep stable.
However, what are all the functions necessary for an abstract
IMotion interface, and more importantly, what are the ideal minimal set of functions it should provide to do things effectively against the needs of all subsystems that will ever deal with motion? That is so, so much harder to answer without understanding so much more of the entirety of the application's design needs upfront. And so when so many parts of the codebase end up depending on this
IMotion, we may find ourselves having to rewrite so much with each design iteration unless we can get this right the first time around.
Of course in some cases the data representation could be very unstable. Something might depend on a complex data structure which might need replacement in the future due to inadequacies in the data structure while the functional needs of the system associated with the data structure are easily anticipated upfront. So it's worth being pragmatic and decide things on a case-by-case as to whether dependencies flow towards abstractions or data, but sometimes at least, the data is easier to stabilize than abstractions, and it wasn't until I embraced ECS that I even considered making dependencies predominantly flow towards data (with astoundingly simplifying and stabilizing effects).
So, while this may seem odd, in such cases where it is much easier to come up with a stable design for data over an abstract interface, I actually suggest directing the dependencies to plain old data. This might save you many repeated iterations of rewrites. However, pertaining to control flows and spaghetti and ravioli code, this will also tend to simplify your control flows when you don't have to have such complex interactions before you finally get at the relevant data.
Code standards in general are useful.
Modules are definitely necessary. You also need consistency in the way "classes" are implemented, i.e. "methods on the prototype" vs "methods on the instance". You should also decide which version of ECMAScript to target, and if you are targeting i.e. ECMAScript 5 use the provided language features, (e.g. getters and setters).
I use Chirpy http://chirpy.codeplex.com/ , which also supports SASS and coffeeScript.