8

I've done quite a lot of javascript over the years and I'm using a more object-oriented approach, specifically with the module pattern. Which kind of approach do you do use to avoid a bigger code-base to becoming spaghetti. Are there any "best approach"?

  • 1
    How would you know if you've written readable and easily maintainable code? - Your peer tells you after reviewing the code. – gnat Oct 14 '12 at 11:01
  • But if a developer think stuff gets complicated after 3 lines of code? What to do then? – marko Oct 14 '12 at 11:09
  • 2
    You cannot determine this yourself because you know more as the author than the code says by itself. A computer cannot tell you, for the same reasons that it cannot tell if a painting is art or not. Hence, you need another human - capable of maintaining the software - to look at what you have written and give his or her opinion. The formal name of said process is "Peer Review" (quote source - top voted answer to a question which is same as yours) – gnat Oct 14 '12 at 11:11
  • That would be awesome if we did at work. – marko Oct 14 '12 at 17:29
7

Dan Wahlin gives specific guidance about avoiding function spaghetti code in JavaScript.

Most people (including myself) start out writing JavaScript code by adding function after function into a .js or HTML file. While there’s certainly nothing wrong with that approach since it gets the job done, it can quickly get out of control when working with a lot of code. When lumping functions into a file, finding code can be difficult, refactoring code is a huge chore (unless you have a nice tool like Resharper 6.0), variable scope can become an issue, and performing maintenance on the code can be a nightmare especially if you didn’t originally write it.

He describes a few JavaScript design patterns that give structure to the code.

I prefer the revealing prototype pattern. My second favorite is the revealing module pattern, which differs slightly from the standard module pattern in that you can declare public/private scope.

  • Yeah, it is kind of what I'm doing, when building bigger stuff. – marko Oct 14 '12 at 17:38
  • 1
    But where I saw the named pattern for the first time, must have been in Javascript Patterns - (amazon.com/JavaScript-Patterns-Stoyan-Stefanov/dp/0596806752/…). – marko Oct 15 '12 at 17:40
  • I hate to be the guy to say "just use a framework"... but for me, I'm always thinking about the guy coming after me, and to have a well-tested, documented, and community supported framework... it's a no brainer to me. My favorite is Javascript MVC (soon to be CanJS). It has scaffolding scripts and a very intuitive dependency manager (steal()) which allows you to have a very well structured application while compiling scripts down to 1 or 2 minified files. – Ryan Wheale Oct 15 '12 at 23:33
4

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.

ECS

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.

Homogeneous Loops

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.

Flattening Event-Handling

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.

  • 1
    I really enjoyed this answer, even though it might not directly answer the question it was very insightful. Thank you for contributing, perhaps you could write a blog post series on the topic? I would read it! – Ben Dec 15 '17 at 4:51
  • Cheers! As for blog, I like to use this place to just dump my thoughts! :-D – user204677 Dec 15 '17 at 5:20
2

Code standards in general are useful.

That means:

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).

See also: TypeScript, which could help you standardize e.g. classes. A bit new at the moment, but I don't see any disadvantages in using it since there is hardly any lock-in (because it compiles to JavaScript).

  • I think Typescript is unnecessary. – marko Oct 15 '12 at 18:28
  • 1
    @marko: Unnecessary for what application? Would you implement a 25 kloc compiler without static typing? – Janus Troelsen Oct 15 '12 at 20:25
2

A separation between working code and deployed code is helpful. I use a tool to Combine and Compress my javascript files. So I can have any number of modules in a folder, all as separate files, for when I am working on that specific module. But for deploy time, those files get combined into a compressed file.

I use Chirpy http://chirpy.codeplex.com/ , which also supports SASS and coffeeScript.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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