Ninja + Clean Code = Enlightenment

I've been reading John Resig's book "Secrets of the JavaScript Ninja" lately. It's a great book and I highly recommend it to anyone wanting to step-up their JavaScript game.

Also, there are a number of principles explained in a book written by Robert C. Martin (aka Uncle Bob) titled "Clean Code". There are many ideas in this book that will challenge some of the ways you think about code and will ultimately make it better in many ways.

One principle that is core to clean code is the Single Responsibility Principle (SRP). It's formal definition is that a class/module or method/function should have only one reason to change. However, when I first heard this definition, it didn't quite register with me. Another way I like to phrase it is a class/module or method/function should do only one thing. SRP results in small methods/functions, maybe less than 5 lines of code as well as classes/modules that are small consisting of maybe less than 5 methods/functions. This also leads to names that are a bit longer and more descriptive.

So, I'd like to take some Ninja JavaScript and refactor it following the Single Responsibility Principle.

Here's listing 7.4 taken from Secrets of the JavaScript Ninja with a few tweaks; the getOpacity function is the same exact code:

HTML

JavaScript

The HTML consists of a div having an opacity of 50%. The JavaScript is wrapped in a IIFE (Immediately Invoked Function Expression). It finds the div in the DOM, obtains the opacity of the div by calling getOpacity and then logs a message to the console if the returned result equals the expected value of 0.5.

Here's a breakdown of what's going on in the getOpacity function:

  1. Obtains the filter property of the element style (elem.style.filter).
  2. If the filter property does not exist (is falsy), the opacity property of the element style (elem.style.opacity) is returned.
  3. If the filter property does exist, the filter is checked to see if it contains the text "opacity=".
  4. If the filter property doesn't contain "opacity=", it returns a zero-length string ("").
  5. If the filter property does contain "opacity=", it uses a regular expression to parse the opacity value, divides it by 100 and returns this value.

Let's take a closer look at the regular expression:

/opacity=([^)]+)/

The two forward slashes indicate where the regular expression begins and ends. The string "opacity=" is just a string literal. The parenthesis indicate a capturing group which is a value that the regular expression parser will make available to us. Here's the capturing group:

([^)]+)

The brackets indicate a character class. In a character class, you can specify multiple characters that should be used when matching a string value. In this case, the character class contains ^) meaning not a closing parenthesis.

The plus sign after the character class indicates we must have one or more matches resulting from it.

The match function returns an array containing the results of the regular expression matching. The first element contains the full string value that was matched. The following elements in the array contain the result of any capturing groups defined in the regular expression. In this case, we've defined one capturing group which would contain the opacity value defined for the HTML element.

The main issue with the getOpacity function is that the ternary expression was a bit overdone making it fairly hard to read. Additionally, one line of code is creating a regular expression, performing a regex match on a string, dereferencing the returned array from the regex match to obtain the opacity value, parsing it as a float value, dividing it by 100 and finally casting the result to a string (+ "").

Here's a refactored version of this code that adheres to the Single Responsibility Principle more closely:

Refactored Code Page 1

Refactored Code Page 2

Let's take a closer look at the refactored version of the code. First we define a few new functions as follows:

isElement: Returns true or false indicating whether the parameter passed is a DOM element or not.

isOpacityDefined: Returns true or false indicating whether the element style has a property containing the element opacity. Newer browsers will have this property available.

getOpacityFromStyle: Returns the opacity property from the element style.

Next we define the getOpacity function which contains the following functions:

areParametersValid: Returns true or false indicating whether the parameters provided to getOpacity are valid or not.

getRegex: Returns a regular expression used to parse the opacity value from an element's style attribute.

getFirstCapturingGroup: Returns the first capturing group found by a regular expression match.

castValue: Returns the value provided converted to a number.

shiftValue: Returns the value provided divided by 100.

The above functions are very specific to the getOpacity function and are therefore defined within getOpacity. Since they don't appear to be very reusable, I've scoped them to the getOpacity function. This is one of the things I like about JavaScript: the ability to easily define functions within a function.

Here's the logic contained within the getOpacity function:

  1. It determines whether the parameters passed are valid or not.
  2. If the parameters are not valid, it throws an error.
  3. It checks to see if the element's style has the opacity setting available.
  4. If the opacity setting is available, it returns this value.
  5. Gets the regular expression object needed to parse the opacity value.
  6. Obtains the style attribute from the element.
  7. Applies the regular expression to the style attribute resulting in matches.
  8. Retrieves the first capturing group from these matches.
  9. Casts the opacity value to a number.
  10. Shifts the decimal place in the opacity value two places to the left.
  11. Returns the resulting opacity value.

Here are my observations resulting from the code refactoring:

  1. The amount of code increased in size from 17 lines of code to 71 (an increase of 54 lines of code).
  2. The code much more clearly demonstrates what it's doing. It's much easier to read and reason about.
  3. Each function is focused and handles one responsibility.
  4. One could say the getOpacity function is doing multiple things. However, it's responsibility is to coordinate the various function calls and return the result.
  5. Three functions have been factored out of getOpacity: isElement, isOpacityDefined and getOpacityFromStyle. At some point, these functions could be moved into a HTML helper module making them available to other parts of the system.
  6. Now that we have several functions, each of them could be unit tested independently.

The final point I'd like to make comes directly from the book "The Art of Unix Programming" by Eric Steven Raymond:

"There are two ways of constructing a software design. One is to make it so simple that there are obviously no deficiencies; the other is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult."