Bulletproof JavaScript with Systematic Development

Kevin Xiang Li
7 min readDec 7, 2019

--

Photo by Craig Davis

JavaScript affords us so much flexibility that we can sketch up a program in minutes. Sketching is excellent for trying out new concepts or experimenting with new techniques. However, when we are trying to produce scalable and reliable software, sketching often produces unmaintainable spaghetti code. I propose a simple structure called Systematic Development through three stages of Plan, Build, and Test to create reliable software. Systematic Development applies to all programming languages but I will focus on how to use this structure in JavaScript.

I will illustrate the principles by creating a vector calculation library. First through a common sketching approach and then through the Systematic Development approach. The point is not to teach you how to write a vector calculation library so you don’t need to understand all the implementation details. However, I hope that a real-world project will help you understand and apply Systematic Development more easily.

Sketching Approach

A vector has an x and y component

We can add vectors

We can negate a vector

We can subtract vectors

All are good so far. However, now we want to handle not just 2d vectors but also 1d and 3d vectors. We need a total refactoring of all three functions because they assume the vectors are 2d. To detect what dimension the input vector is in, we will create three helper functions — is1d, is2d, and is3d.

Now addVectors look like:

Looking a little nastier with all the repeated object literal code.

We need to apply the same nastiness to negateVector :

We copy the addVectors function and change all + to - to get subtractVectors :

Now, we also want to represent vectors in magnitudes and directions and be able to mix the two forms in calculations. We need to refactor all three functions again to include vector form detection. 😨 We become frustrated and want to abandon the library.

Systematic Development Approach

Before writing any code, we need to make a plan.

Plan Stage

Ask ourselves three questions:

1. What do we want the program to achieve?

We want a vector library that

  • supports 1d, 2d, and 3d vectors
  • supports two vector forms — component and angular (magnitude and direction)
  • can add, subtract, and negate vectors

Setting the goal of the program is very crucial. For instance, if we decided to support n-dimensional vectors, the program will be very different.

2. How can we represent data in the program?

We will use TypeScript to manage data structures so our program is type-checked at compile time.

Now we translate our English description of data (in this case vectors) from question 1 to types in TypeScript. First, a vector can either be a component or an angular vector. So we create a new custom type called Vector that has two variants — VectorCom for component vectors and VectorAng for angular vectors.

How can we represent VectorCom?

Let’s look at 1d, 2d, and 3d vectors case by case:

1d vector in components

  • x: a real number
1d vector in components

2d vector in components

  • x: a real number
  • y: a real number
2d vector in components

3d vector in components

  • x: a real number
  • y: a real number
  • z: a real number
3d vector in components

Because the number of components varies based on the dimension of the vector, it’s hard to represent VectorCom using objects. Instead, we will use a simple number array to account for the varying length.

How can we represent VectorAng?

Let’s look at 1d, 2d, and 3d vectors case by case:

1d vector in direction and magnitude

  • magnitude: a non-negative real number (magnitude ≥ 0)
  • direction: left or right
1d vector in direction and magnitude

2d vector in direction and magnitude

  • magnitude: a non-negative real number (magnitude ≥ 0)
  • direction: an angle between 0 and 360
2d vector in direction and magnitude

3d vector in direction and magnitude

  • magnitude: a non-negative real number (magnitude ≥ 0)
  • direction: two angles (α and β) between 0 and 360
3d vector in direction and magnitude

Because angular vectors in different dimensions share the same properties (magnitude and direction), we naturally represent angular vectors in objects which are key-value pairs. However, since the direction property is different between dimensions, we would need three separate object types for each dimension.

1d angular vector

We use -1 and 1 to represent the left and right direction respectively. You can also use a boolean but -1 and 1 makes more sense to me as they capture the negative (left) and positive (right) directions commonly used in Physics. In addition to mag (magnitude) and dir (direction), we also add a dim (dimension) property so we can handle each dimension differently in the calculation.

2d angular vector

Because TypeScript cannot restrict the range of the angle at compile-time, we can only specify dir as a number.

3d angular vector

Add one more angle to represent how high/low the vector is at the vertical z-axis.

3. How can we define the functionalities of the program?

We need three functionalities (in our case vector operations): add, negate, and subtract vectors.

Let’s look at each functionality in more detail and think about:

  • the input of the functionality
  • the output of the functionality

Add

  • input

Since adding a series of vectors tip to tail is relatively common, we will accept a list of vectors as input.

  • output

We will return a resultant vector as output. Because a vector can either be in a component or angular form, we will add an input parameter called form for the user to specify what form they want. We will define a VectorForm type that can either be "ang" (angular) or "com" (component).

Let’s write the function signature for add:

Negate

  • input

We will accept one vector v as input.

  • output

We will return a negated vector as output in form specified by the user.

Let’s write the function signature for negate:

Subtract

  • input

We will accept two vectors v1 and v2 as input.

  • output

We will the result of v1 — v2 as output in form specified by the user.

Let’s write the function signature for subtract:

🎉🎉🎉 We have successfully created a robust outline for our vector library! Let’s move on to the build stage!

Build Stage

We will build the three functionalities in sequence.

Add

Following the outline we created in the Plan Stage, we will put implementation details inside the addVectors function.

First, we will create a helper function called addVectorComs for adding component vectors.

STOP and TEST!

Even though the Test Stage is after the Build State, we should never put testing off until all functionalities are implemented. Instead, we will always do unit testing after implementing every single function. The reason is unit testing makes tracing, identifying, and fixing bugs much easier because you have much less code that can potentially contain bugs.

The above is just a demo test for addVectorComs using Jest. You should add more tests and especially more edge cases like zero vector.

In the addVectors function, we will convert all input vectors to component vectors and call the addVectorComs to produce a result. So we create another helper function to decompose a vector into its components.

STOP and TEST!

Now that we can add vectors, we need a way to convert the final component vector result to the form user wants. We will add a formVector helper function for that purpose.

Before defining the formVector function, we will specify the default form used by all functionalities (add, negate, and subtract) to be "com"(component) because all our vector operations produce a component vector by default.

We also need a way to convert component vectors to angular vectors. Let’s define a helper function called angularizeVector to do that.

STOP and TEST!

Let’s define formVector using decomposeVector and angularizeVector helper functions:

STOP and TEST!

No matter how simple the functions seem to be, write some tests to make sure they work as we intended.

Finally, let’s put all the helper functions together to create the addVectors function:

As we can see, the add function is quite simple and is built on top of modular helper functions. Because we did unit testing on all the helper functions, we are fairly confident that the addVector function will work as intended. but…

STOP and TEST!

Is this test complete? What’s missing?

Pause and look through the tests carefully and compare them to the actual inputs and outputs of addVectors.

The answer:

  • input

addVectors accept a list of vectors as input but we only tested lists containing 2 vectors. We should add empty lists and lists containing more than 2 vectors as input.

  • output

addVectors can return both component and angular vectors as output but we only tested component vectors as output. We should add angular vector outputs by specifying theform parameter to be "ang".

🎉🎉🎉 We have successfully built our first functionality — add!

We can follow a similar process of Build and Test for the other two functionalities. Due to the length of the article, I will not walk through them step by step. You can check out my GitHub repo for all the code.

Hopefully, you understand the three stages of Systematic Development — Plan, Build, and Test — and are able to apply them to create your own robust projects. 🚀

--

--

No responses yet