JavaScript Design Pattern - Module Pattern

By ticking the checklist from the previous post on design pattern, we will look at Module Pattern in JavaScript.

✅ Common Pattern of the Recurring Problem

Web in its early history was very simple. Therefore there was no need to strategically manage a long code base. However, nowadays, the web is growing more and more complicated. As a file gets longer, the possibility of side effects increases; e.g. name collision, repetition, poor readability, and maintainability. A need to modularize code into shorter, reusable pieces arose.

Imagine we want to implement a simple counter that implements, decrements count value, and logs updated value.

let count = 0

const increment = () => {
  const currCount = count;
  console.log(`incrementing count from ${currCount} to ${currCount + 1}...`)
  count++
};

const decrement = () => {
  const currCount = count;
  console.log(`decrementing count from ${currCount} to ${currCount - 1}...`)
  count--
};

increment() // incrementing count from 0 to 1... 
increment() // incrementing count from 1 to 2... 
increment() // incrementing count from 2 to 3... 
decrement() // decrementing count from 3 to 2... 
console.log(count) // 2

Despite being a very simple app at the moment, it harbors the possibility of many side effects.

❌ separation of interest: index.js is not only consuming the counter but also it's where counter logic is declared. index.js would be too long if there are more modules it imports.

❌ encapsulation: though we might want a user to modify the value of count bt 1, using designated methods increment and decrement, nothing stops the user from modifying it directly, however much they want( count = 100).

❌ readability

❌ compartmentalization

❌ name collision

If index.js starts implementing more functionalities than the counter, it would become difficult to distinguish counter-related logic from others. Also, a longer file is vulnerable to an accidental declaration of variables with the same name as count. In that case, count will be overwritten and will cause unexpected behaviors.

✅ Solution

Module Pattern

Closure

Before the module system officially became part of the standard JavaScript in ES6, Closure was one of the ways to achieve modularity by taking advantage of JavaScript's native features, without the help of 3rd party libraries. Nowadays, one can also use Closure in combination with ES6 Module.

Let's refactor the above counter app using Closure.

var Counter = (function () {
  var _count = 0

  return {
    get count() {
      return _count
    },
    increment: function () {
      var currCount = _count
      console.log(
        `incrementing count from ${currCount} to ${currCount + 1}...`
      )
      _count++
    },
    decrement: function () {
      var currCount = _count
      console.log(
        `decrementing count from ${currCount} to ${currCount - 1}...`
      )
      _count--
    }
  }
})()

Counter.increment() // incrementing count from 0 to 1... 
Counter.increment() // incrementing count from 1 to 2... 
Counter.increment() // incrementing count from 2 to 3... 
Counter.decrement() // decrementing count from 3 to 2... 
console.log(Counter.count) // 2

Module Pattern implemented with closure has the following tradeoffs:

🟢 encapsulation: In the code above, we've put into returned object what may be exposed as a public member of the module. Those that are not returned become private. As a result, the consumer of Counter module is forced to modify count only with the interface provided by Counter. count is now a read-only value and an attempt to set the value directly will cause an error.

Counter.count = 100;
// ❌ TypeError: Cannot set property count of #<Object> which has only a getter

🟢 namespace: related methods of Counter are namespaced inside Counter and it's much clearer to group together interrelated variables and methods.

❌ compartmentalization

❌ readability

Without the separation of files, modules in one long file were still poorly compartmentalized and hard to read.

EcmaScript Module (ESM)

In the year of 2015, ES Module became official and developers could write modules in separate files without resorting to 3rd party libraries.

Now let's refactor our counter app using ESM.

We've refactored by separating the counter.js file, putting all counter-related logic in it, and having index.js import it.

Now it's

🟢 more concise,

🟢 more readable.

🟢 Just like how we did with Closure, related methods are namespaced.

🟢 Also, with a proper namespace, it's clear what index.js and counter.js are doing, without having to read Counter line by line.

import "./styles.css";
import Counter from "./counter.js";

Counter.increment();
Counter.increment();
Counter.increment();
Counter.decrement();
console.log(Counter.count);

🟢 In case of name collision, we can simply import Counter with a different name, since export default doesn't enforce the name of the import.

import "./styles.css";
// import Counter from "./counter.js";
import CounterApp from "./counter.js";

CounterApp.increment();
CounterApp.increment();
CounterApp.increment();
CounterApp.decrement();
CounterApp.log(Counter.count);

const Counter = {
  // new, local Counter
  // ...
}

Even if we were using named export, we can use an alias.

Revealing Module Pattern

The revealing module pattern is a variation of the module pattern. In Revealing Module Pattern, instead of defining public members of the module only in the returned value of a factory function, one defines all variables and methods within the function and just “reveals” what one wishes to make public within the return statement.

By doing so,

🟢 the code becomes easier to maintain and scale. To be precise, this pattern promotes us to observe the OCP Principle(Open-Closed Principle). According to the definition of the principle, "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification." The rule is better observed in the revealing module pattern than the normal one since we don't touch or move the existing code when we want to change its access level from private to public or vice versa.

For example, we don't have to copy and paste code in and out of the return statement. Additionally, there is no more need to switch between function declaration/expression and object literal syntax.

Nevertheless, there are tradeoffs.

❌ Code gets longer than normal module pattern since one ends up repeating public members.

Now, let's refactor our Counter app to adopt the revealing module pattern.

Closure

ESM

Extending Module

Imagine we want to customize Counter when we use it in index.js. For example, we want to add additional functionality that logs when the counter initiates.

🟢 Here, OCP Principle is well observed. We didn't have to change the code in counter.js, where Counter is declared. Still, we successfully added new functionality by extending the module in index.js, where it is used.

✅ Adaptation

How we break UI into smaller components in React is one of the most common use cases of the module pattern

✅ Categorization

Module design pattern overlaps with many other design patterns we will cover later on, such as Singleton Pattern, and Proxy Pattern. Those patterns are built on top of Module Design Pattern

✅ Language Integration

ESM

It was only recently that JavaScript had its standard module system. Before, developers limitedly modularized apps with the help of native features of language(e.g. HTML `<script type="module"/>`, JavaScript Closure) or libraries(e.g. AMD, UMD). The growing need for modules led to the standard ECMAScript module.

WeakMap()

According to Osmani, in his book Learning JavaScript Design Patterns,

it should be noted that there isn't really an explicitly true sense of "privacy" inside JavaScript because unlike some traditional languages, it doesn't have access modifiers. Variables can't technically be declared as being public nor private and so we use function scope to simulate this concept. Within the Module pattern, variables or methods declared are only available inside the module itself thanks to closure. A workaround to implement privacy of variables in returned objects uses WeakMap().

In this regard. WeakMap() is also one of the examples where design patterns' solutions to recurring problems are adopted by a programming language as a standard.

References

Learning JavaScript Design Patterns, Osmani, Addy. O'Reilly
https://en.wikipedia.org/wiki/Open–closed_principle

Subscribe to go-kahlo-lee

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe