From chaos to Backbone to React

Refactoring an unstructured legacy code base

photo source: The Economist

This article is written to be accessible to readers of all levels of experience with front end web technologies. It is part of a series.

How this article is laid out:

  • Project overview
  • Framework selection
  • Challenges encountered
  • Solutions employed
  • Conclusions

Project overview:

When I started working on Goldstone, my manager had built an impressive and fully functioning JavaScript prototype. His skills as an engineer cross multiple disciplines and are not constrained to a single programming language. He didn’t have much experience with JavaScript, but was able to apply design fundamentals and familiarity with core language concepts towards putting together a solid prototype.

Framework selection:

I was hired just after finishing a 12 week JavaScript intensive at Hack Reactor, and was put on the task of modularizing the code within a framework, and adding a test runner.

We discussed the possibility of using Backbone or Angular, as well as the pros and cons of each as far as we understood them. We decided to go with Backbone, ostensibly due to the fact that compared to Angular, there was an impression of there being a lot less “magic” between the interface and internal workings. Anyone who has spent time looking through the source code of either would probably agree that Backbone is easier to comprehend. The decision was also made around the time that the Angular team was getting a lot of notoriety for releasing major breaking changes across the API, and Backbone was known to be stable.

Challenges encountered:

A code base written without adherence to the principles of DRY (don’t repeat yourself) will be WET (write everything twice). Implementing code reuse patterns can be challenging when the system was not designed with this in mind in the first place. I share some representative examples below.

For the following: these may be gross oversimplifications, but I recommend you focus not on the specific functions, but on the type of logic that goes into refactoring a series of inadequate functions that can be replaced by one with greater utility.

The following function definitions…

function addOne(n) {
return n + 1;
}
andOne(2) // => 3
function addTwo(n) {
return n + 2;
}
andTwo(8) // => 10
function addThree(n) {
return n + 3;
}
andThree(4) // => 7

…could be replaced by a single function that takes an additional argument: allowing for greater flexibility, and the ability to encompass the utility of multiple functions that you are replacing:

function addSome(n, base) {
return n + base;
addSome(4, 2) // => 6

Again, please don’t confuse the specific functions for the lesson behind it that sometimes by modifying the function signature, you can create something that will have greater flexibility and utility, allowing you to replace a lot of unnecessarily repeated code. Take complexity into consideration. If readability and comprehension decline sharply because of your desire to save lines of code, I would recommend against that type of abstraction.

So that refactoring example was fairly obvious, but what about:

function addOneAndDrum(n) {
playDrumroll();
return n + 1;
}
function addTwoAndFlute(n) {
playFlute();
return n + 2;
}
function addThreeAndCalliope(n) {
playCalliope();
return n + 3;
}

Now you have 3 functions just like the previous 3, except this time you also have an additional side effect inside of each function body to deal with. Can you rely on the same abstraction as before?

function addSomeDoSomething(n, base, sideEffect) {
sideEffect();
return n + base;
}
addSomeDoSomething(1, 2, climbRope) // => 3 ( climbRope was also invoked );

Based on your preference for how to design your interfaces, yes you can. But now imagine following this logic further. If you choose to accommodate each additional abstraction with an additional function argument, it will not be long before you are moving towards excessive complexity and unreadability.

At this point, I would recommend taking a step back and evaluating whether or not your functions are becoming unreasonably complicated. What about breaking apart the functions based on their intended use?

from this:
function addSomeDoSomething(n, base, sideEffect) {
sideEffect();
return n + base;
}
to this:
function addSome(n, base) {
return n + base;
}
function doSomething(sideEffect) {
sideEffect();
return;
}

Now we’re back to where we started with the addSome function, and we have broken out the doSomething function. This points to the original functions we were replacing having been poorly thought out from the point of view of code reusability.

In case you are skipping around the article, I will mention for the last time that the function examples are not the important point, but instead the types of abstractions that you will likely encounter when refactoring a code base that was written without a framework, or a well thought out plan for modular code.

In the case of the project that I was working on, the prototype that I was refactoring had been executed by a senior engineer with an excellent grasp of computer science fundamentals and object-oriented programming, but limited experience with JavaScript and its ecosystem. These are the trade-offs that are made when designing a prototype under the deadlines of the startup world. “Fast, Cheap, Accurate: Pick Two” By taking a “loan” of sorts from Accurate, an organization can leverage that towards churning a project out Fast. This is also what leads to the accumulation of what is known as technical debt. Eventually the cumulative lack of accuracy that afforded the ability to produce a prototype that was Fast & Cheap will catch up in terms of system complexity and speed of iteration. Every engineer experiences this pain at some point, and ideally the experience provides valuable lessons about the pros and cons of a particular design strategy.

Solutions employed:

As detailed above in Framework Selection, after discussing viable libraries or frameworks to use for the refactoring and modularization phase of the project, we decided I should use Backbone. Here are the libraries that I employed for the process of building the client for Goldstone:

  • Backbone.js (JavaScript framework)
  • D3.js (data visualization library)
  • DataTables (jQuery table formatting plug-in)
  • Moment.js (date/time library)
  • Bootstrap (responsive design framework)
  • Grunt task runner
  • Karma test runner
  • Casper (end-to-end testing library)
  • Jed (JavaScript GETTEXT internationalization utility. We implemented Japanese in addition to English)

Conclusions:

On the Goldstone project, Backbone.js was an ideal framework for modularizing an unstructured code base, and provided ample utility functions for implementing an MV* architecture with a solid events api and robust built-in router. In addition to its utility, it is an excellent framework for learning and applying object-oriented design principles. There is little about it that appears to work by “magic”. The entire source code is relatively concise and available in an annotated version.

For my final project with this company, I did a research spike into React and Redux during one of our sprints. After hearing and reading about it with increasing frequency, and seeing it increasingly discussed at conferences and talks, I advocated for my team supporting me in looking into it as a viable framework for our next project. I was incredibly impressed with the experience of going through a number of tutorials and building some test projects, and for my next project I had full ownership of a client micro service that was deployed alongside the server technologies of our project. Unfortunately that project was not open source so I cannot link to it. It was a React app compiled by webpack, and employing Redux for state management.

I am happy with the learning that came from employing these technologies in this order.

This article is part of a series