DEV Community

Cover image for When and how to write future-proof code
Graham Trott
Graham Trott

Posted on

When and how to write future-proof code

β€œLife is really simple, but we insist on making it complicated.” ― Confucius

In any web project there are 2 kinds of code; the kind you never need to look inside and... the rest. Weather widgets and maps are obvious examples of the first kind, but a careful examination of any project will reveal parts, both large and small, that can be extracted as what I'll call trusted components. The more of these you can extract the better, because it's 'the rest' that will give you all the grief.

Trusted components are part of the professional programmer's domain. No future system maintainer is ever likely to look inside them; that's very much the action of last resort. Even if a component is suspect it's easier to verify by putting it into a test harness or mocking it out than to dive into unknown code written by another professional (or, sadly, even your own code written years earlier). The more widely a component is used the less likely it is to have problems, and even if it does these will soon become well known and most likely fixed. So get those trusted components out there and get them used as widely as possible.

Having done all this you'll be left with an unpredictable and unruly mess of user interface and business logic, both of which are likely to fall victim to frequent modifications, making it hard to predict any consistent patterns that will apply for long. You may be lucky/unfortunate enough to have a long-term employment contract that guarantees your availability to do all maintenance work from now until eternity, but if not, someone else will be doing it. And that's the problem. What's the best way to write this kind of code in such a way that the owner of the website or a future maintenance person will be able to figure it out?

If your answer is "Use a framework" you need to step back a little. Frameworks represent the state of the art in software development, not future-proofing. The kind of unpredictable changes that UIs and business logic undergo over time will inevitably break anything you dream up now. Moreover, your favorite framework of today, and quite possibly the development environment it relies on, will in the future be long forgotten, so rapid is the rate of change in fashion when it comes to these things. About the only thing you can guarantee a maintainer of your system being comfortable with a decade or more from now is vanilla JavaScript.

Catering for different interests

Software projects involve a lot of different interests but I would summarise them as developers, customers (also representing users) and maintainers. Don't make the mistake of equating developers and maintainers; they are more often than not different people with very different skill sets. Developers have all the latest skills and seldom have to address legacy issues; maintainers need to build experience across a wide field. Depth versus breadth.

Separate activities

These three interests are all separate from each other, each of them has its own preoccupations and not one of them has guaranteed insight into the worlds of either of the others, leading to frequent mistakes caused by misunderstandings and assumptions. We can try to deal with this by bolting on an extra communications layer, with messages passing back and forth between the 3 parties, but this relies too much on discipline and the quality of the messages being sent.

To improve this situation we need to find some common area of overlap, so let's move the 3 circles closer together:

Overlapping interests

Here each pair has an overlapping area of interest, and in the center the white segment represents a region in which all 3 are represented. It's in here we should put everything that is of interest to all parties, but the question is, what form should it take?

Talking the same language

Above all else you need something anyone can understand, or the objective is lost. You're planning for a future world that may look very different to today, hence my challenging comment above about frameworks. Whatever route you choose has to be completely self-contained, not relying on any more than the barest minimum number of assumptions about what may be well understood or exist outside itself.

One thing you can do is create your own form of language. This may sound scary but it's quite a well-established technique, if not well documented. There are several ways to do it:

  1. Vanilla JavaScript. Beginner-level stuff only, please. Even JQuery is only borderline acceptable, and then only if it's supported with comprehensive documentation. Minimum use of functions as parameters, no ternary operators and definitely no huge Node.js dependency chains. Even with all that, your message will still be hard to extract from the mechanism you're using to deliver it.

  2. XML. It's difficult to imagine a more perverse decision than to choose XML as a language, so manifestly unsuited it is to the job, but I've seen it done in a couple of Java projects and it sort of worked. It wasn't easy to read; the message tended to be swamped in a forest of XML tags and it's a really clunky solution given the mismatch between XML and Java. Changes were a painful process, not aided by the use of dependency injection, which works fine right up to the point you want to change it. This is kind of self-defeating as no future maintainer would ever be able to get their head round it.

  3. JSON. It's surprising that for something which arrived simply as a lightweight alternative to XML, the difference is so huge. It's mostly down to the way JSON is the externalized form of JavaScript objects; the two are essentially interchangeable. A JSON array can easily be made to look like a kind of "assembly language" for a hypothetical processor whose instruction set is the things you want to do in your web page, starting with DOM objects and spreading out from there.

The joys of assembler

Anyone who was around computers in the 1970s will remember assembly language. Yes, a high-level language compiler will give you results much quicker, but with assembler you're never in any doubt about what's really happening. Each instruction does just one thing; clearly and unequivocally. And that's just what we need for UI code; not abstraction but certainty.

The kind of assembler I'm proposing here doesn't mess about with registers and bits; it has a one-to-one correspondence with what you see on the page. Just as HTML does, in fact, but we don't have to stop there. If you want your assembly language to include an instruction that embeds a map or an RTF editor or performs a REST query than you can have one; it's your hypothetical processor, after all.

A hypothetical processor needs a runtime engine that pretends to be that processor. It's best to do this using vanilla JavaScript in case it has to be updated, though you might eventually feel confident enough to promote it to trusted component status, where it is unlikely to ever need maintenance. This is a decision only you can make, but as before, the more code you can extract as trusted components the easier will be the job of a future maintainer.

So without further ado I'll present a small script and the runtime engine that handles it.

The job is very simple; we have a DIV already on the page, to which we want to add a hyperlink that when clicked pops up an alert. So in pseudocode it's

- find the element with a given id
- add an <a> component to it
- when the hyperlink is clicked, show an alert
Enter fullscreen mode Exit fullscreen mode

Here's the source of our test page:

<html>
  <head>
    <title></title>
    <meta content="">
    <style></style>
    <script type='text/javascript' src='scriptRunner.js'></script>
  </head>
  <body>
    <div id="test-div"></div>
    <pre id="script" style="display:none">
    [
        {
            "keyword": "attach",
            "id": "test-div",
            "name": "TheDiv"
        },
        {
            "keyword": "create",
            "parent": "TheDiv",
            "type": "a",
            "text": "Click me",
            "onClick": "ShowAlert"
        },
        {
            "keyword": "stop"
        },
        {
            "keyword": "alert",
            "label": "ShowAlert",
            "message": "You clicked"
        }
    ]
    </pre>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

There are only 3 components parts to note; a JavaScript file, a <div> element and an odd-looking script. This last item is an invisible <pre> element containing a JSON array of objects, each one with an arbitrary set of properties.

Next we have the JavaScript scriptRunner.js code:

window.onload = function () {
  const script = document.getElementById(`script`);
  ScriptRunner.start(script.innerText);
};

const ScriptRunner = {

  start: function (script) {
   ScriptRunner.run(JSON.parse(script));
  },

  run: function(script, pc = 0) {
    script.pc = pc;
    while (script.pc < script.length) {
      const keyword = script[script.pc].keyword;
      switch (keyword) {
        case `alert`:
        case `attach`:
        case `create`:
          const handler = `do${keyword.charAt(0)
            .toUpperCase()}${keyword.substr(1)}`;
          ScriptRunner[handler](script);
          break;
        case `stop`:
          script.pc = 0;
          break;
        default:
          script.pc++;
      }
      if (script.pc === 0) break;
    }
  },

  doAlert: function (script) {
    const command = script[script.pc];
    alert(command.message);
    script.pc++;
  },

  doAttach: function (script) {
    const command = script[script.pc];
    command.element = document.getElementById(command.id);
    script.pc++;
  },

  doCreate: function (script) {
    const command = script[script.pc];
    command.element = document.createElement(command.type);
    switch (command.type) {
      case `a`:
        command.element.setAttribute(`href`, `#`);
        command.element.innerHTML = command.text;
        command.element.onclick = function () {
          for (let n = 0; n < script.length; n++) {
            if (script[n].label === command.onClick) {
              ScriptRunner.run(script, n);
              break;
            }
          }
        };
        break;
    }
    for (let n = 0; n < script.length; n++) {
      if (script[n].name === command.parent) {
        const parent = script[n].element;
        parent.appendChild(command.element);
        script.pc++;
        return;
      }
    }
    script.pc = 0;
  }
};
Enter fullscreen mode Exit fullscreen mode

The script is extracted as plain text from the <pre> element so the first thing to do is to parse it into a JavaScript object. This is of course the great thing about using JSON with JavaScript; the easy 2-way interchange between an object and its externalized form.

In this code there are no global variables; the script itself is used as a scratchpad for all the storage needed to run the program, starting with a program counter (pc). This is simply an index into the array; it controls our movement through the program, allowing things like control transfer and event handling. Execution of a program thread can start anywhere; zero is the default entry point, and it continues executing commands until one of them returns a program counter value of zero, at which point the thread stops executing and allows another to take over.

The code is structured as a single object with member functions, which avoids any possibility of name clashes with other JS modules (as long as ScriptRunner is itself unique).

Function run() is central. It examines the keyword property of the current command and builds a function name of the form doXxx from it. This I call a wrapper and the function name must exist or it's a fatal error, though in this example code I've not included any error handling. We then call the wrapper, passing it the script.

Content wrappers

I call the doXxx() functions wrappers because each one of them wraps a chunk of content. The wrapper should be thin and the content fat. Each of the content items is ideally a trusted component, as defined earlier, and because the wrapper is thin there's only a small chance of bugs occurring. The aim is to wrap as many different trusted components, DOM elements and other functions as you can; this gives you a rich, reliable language you can apply to a wide range of problems.

These are the wrappers I've implemented here:

doAlert() extracts the current command, pulls out the message property and passes it to alert() before advancing the program counter.

doAttach() looks in the DOM for an element with the requested id and places a reference to the found element back into the script, keyed by a unique name we provide and ready for use elsewhere. Again, no error handling has been provided here.

doCreate() creates a new element of the requested type. There's only code here for a simple <a> element but in a real implementation there would be entries for all the different types of DOM element a script is likely to want. It looks up the parent, which was saved by name earlier, and adds the new element as a child. It also scans the script to find a command with the named label value; this is where we want to resume processing when the user clicks the link.

stop() is so simple it doesn't even have its own wrapper. It just stops the script.

So what should happen is the program looks for the DOM element with the given id, adds a hyperlink to it and reacts when it's clicked by putting up an alert. This is pretty simple to verify.

Reliability

Because this technique uses the same blocks of runtime code over and over again there are few places for bugs to hide. Once it's settled down, the runtime engine and the big, expensive components with wrappers around them are rarely the source of problems. The structure also makes it easy to add diagnostics, breakpoints, single-step and other features with just a few extra properties, because the central loop in run() executes for each and every instruction.

Where to go from here

You may be thinking this is a heap of trouble to go to just to create a hyperlink, and you'd be right. However, this is only an example. Not all of the things you want to do in a page are as simple as basic DOM elements. Take a magazine or social media website, for example, which has many larger, more complex blocks that can also be described at the top level by structures of this kind. Components like live maps and weather widgets have simple APIs that can be expressed in a small number of properties. String manipulation - the textual logic that's often at the core of business logic and so easy to get wrong - is also easy to describe clearly and in a way that any domain expert can understand, as are REST functions. And my example makes no mention of conditionals or timing components, which also lend themselves well to being described by simple JSON objects.

Writing and maintaining scripts requires no special programming skills; just an understanding of the options available for each "assembler" instruction. Your scripts become the lingua franca, the common language understood by all those with an interest in how a system works and in keeping it working through changes. The more functionality you can pack into each JSON instruction, while keeping the number of properties down to a minimum, the more powerful the technique becomes, because the 'language' is almost impossible to make too complex to understand. (Someone will no doubt prove me wrong and produce a totally incomprehensible system, but they'll do that whatever technology they use.)

In fact, all of the above is only half of a bigger story. What we have here - in embryonic form - is the back half of a browser scripting language, covering the intermediate code and the runtime, with the initial parser and compiler parts missing. Those parts may be the subject of a future article. Anyone interested in what a complete scripting language of this type looks like can visit EasyCoder, a real-world web app builder that's been used for a number of different websites. While not being based exactly on the code I've presented here, it uses many of the same techniques. Confucius would have been delighted.

I'm very willing to chat about issues such as how to implement particular kinds of instruction (for loops, maybe?) in a script of this kind. Just post a question below and I'll do my best to respond.

Title photo by Giancarlo Revolledo on Unsplash

Top comments (1)

Collapse
 
anwar_nairi profile image
Anwar

Hi Graham, thank you for this detailed overview of EasyCoder! I appreciate your effort when you split up each parts and explain it in depth. The concept is really clever and I can't wait to see in one of your future post the part when you preprocess the pseudo code into the JSON array of keywords. As always, great job :)