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
2d vector in components
- x: a real number
- y: a real number
3d vector in components
- x: a real number
- y: a real number
- z: a real number
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
2d vector in direction and magnitude
- magnitude: a non-negative real number (magnitude ≥ 0)
- direction: an angle between 0 and 360
3d vector in direction and magnitude
- magnitude: a non-negative real number (magnitude ≥ 0)
- direction: two angles (α and β) between 0 and 360
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. 🚀