Skip to main content
a z C o d e l a n d

Module Design Pattern

Created: July 3th, 2024

Updated: July 7th, 2024

Explanation


NOTE

In this article, the term encapsulate is used to refer to the organization of code under a namespace. It does not mean making code private.


The Module Design Pattern encapsulates related state and logic within a module. If the module has any exports, named or default, then those exports form the public interface of the module, and the remaining non-exported code forms the private interface, which is only accessible within the module. External code can interact with the module’s encapsulated code through the controlled, exported public interface.

The reason the module design pattern in JavaScript is able to provide a public and private interface is due to the way the JavaScript module system works. The import/export mechanism allows for exposing only selected parts of the module for import by other modules, creating a public interface while the rest of the module remains private.

When a module is imported, all of its top level code is executed. During this phase, the module’s private and public interfaces are defined. If the exported public interface contains functions, then a closure gets created that retains access to the private interface even after the module’s initial execution on first import.

Wikipedia article says:

Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality.

A module interface expresses the elements that are provided and required by the module. The elements defined in the interface are detectable by other modules. The implementation contains the working code that corresponds to the elements declared in the interface.

Analogy

We can think of a vending machine as a module that encapsulates a variety of snacks and provides a public interface and a private interface. To select and pay for snacks, we interact with the keypads and the payment slot, which can be thought of as the public interface. On the other hand, the private interface is only available to the vending machine for dispensing and replenishing snacks.

Code Example

In this code example, we have a module named bankAccount.mjs, and the module exports a default bankAccount, object that forms the public interface of the module.

The encapsulated state includes accountTypes and accountBalances, and the encapsulated logic includes the functions calculateInterestPrivate, getAccountTypes, getAccountBalance, deposit, withdraw, and calculateInterest.

Notice that the calculateInterest function in the public interface invokes the calculateInterestPrivate function from the private interface. When using the module design pattern, this is a way to hide implementation details of the module from external users while still making the functionality of the private interface available through the public interface.

ES6 Syntax

  // bankAccount.mjs

  // All these are private interfaces
  let accountTypes = ['checking', 'savings'];
  let accountBalances = {
    checking: 100,
    savings: 50
  } 

  function calculateInterestPrivate(amount, timePeriod) {
    // Private interest rate calculation implementation
  }


  // Public interface
  const bankAccount = {
    getAccountTypes: function() { 
      return accountTypes
    },

    getAccountBalance: function(accountType){
      return accountBalances[accountType]
    },


    deposit: function(amount, accountType){
      accountBalances[accountType] += amount;
    },

    withdraw: function(amount, accountType){
      if(accountTypes.includes(accountType)){
        if(accountBalances[accountType] >= amount){
          accountBalances[accountType] -= amount;
          console.log(`Withdrew ${amount} from ${accountType}`);
        }
        else {
          console.log(`You don't have enough money in ${accountType}`);
          console.log(`Your balance is ${accountBalances[accountType]}`);
        }
      } else {
        console.log(`Invalid account type`);
        console.log(`Valid account types are ${accountTypes}`);
      }
    },

    calculateInterest: function(amount, timePeriod){
      calculateInterestPrivate(amount, timePeriod)
    }
  };

  export default bankAccount

Advantages

  • Encapsulation: The ability to organize related code into one file improves the readability of that code and the overall codebase.

  • Public & Private interfaces: The ability to hide implementation details and expose only certain parts is beneficial.

  • Namespace management: When importing a module, the import name given to the module becomes the namespace by which the module’s functionality is accessed. This namespace, used as a prefix before identifiers of the imported module, avoids name collisions with identifiers of the same name in other modules.

  • Ability to Update Public Members: Public variables and functions on the exported object can be reassigned and redefined by external module users to make it fit their use cases. It’s important to note that these updates do affect the original module and therefore any other modules that import it.


Disadvantages

  • Visibility Change Overhead: If you change the visibility of a member (state or logic) from private to public or vice versa, you need to:

    • Update all instances of where that member was accessed, as the way to access the member will change based on its visibility.

    • If adding a private member to the public interface, we need to remove it from the private interface scope and add it to the public interface scope, and vice versa.

  • No Access To Private Members: Depending on the context, you might be working with an external module whose source code you can’t modify. Private members become a limitation in such scenarios because if you want to add additional functionality to the module, you won’t be able to access the private members.

  • Limitation in Testing Granularity: Since only the public interface is exposed, testing of private members is only possible
    through unit testing of the public members.

The Javascript Modules Journey

With the introduction of ES6, JavaScript got a built-in module system that allowed the import and export of specific logic and state between JavaScript modules using the import and export keywords. But it wasn’t always like this, let’s take a quick look.

Before the introduction of ES6, JavaScript did not have a built-in module system for importing and exporting JavaScript modules within other JavaScript modules. In a browser environment, to use code from another module, developers had to correctly arrange their script tags in HTML files so that variables and functions imported from earlier script tag modules would get added to the global scope before later imported modules could use them. This was not good practice, as it polluted the global scope and could lead to identifier conflicts between different modules using the same variable and function names.

In a Node.js environment, developers used module.exports to export variables and functions from a module. If a module needed to export multiple identifiers, an object containing those identifiers as properties would be exported. The require() function was then used to import that module. However, this syntax was only understood by the Node.js runtime and not by browsers, so modules using this syntax could not be used directly in the browser.

In both environments, all the code of the imported module would be added to the global scope. To get around this, developers use an IIFE to return the code that they want exposed to the global scope. Everything else within the IIFE scope remains private because functions create their own scope. The returned code, usually
an object, would become the public interface of the module that other modules could interact with. Below are some code examples to illustrate this.

An example of how defining everything at the top level of a module gets added to the global scope, which is window in the browser and global in Node.js.

  // moduleOne.mjs

  var carModel = 'Toyota';
  var carColor = 'Blue';
  // we want this to be private but it's exposed anyway
  var vinNumber = '123456789'; 
  
  // we want this to be private but it's exposed anyway
  function getVinNumber() { 
    return vinNumber;
  }

  function drive() {
    console.log("Driving");
  }

  function stop() {
    console.log("Stopping");
  }
  // moduleTwo.mjs
  // We're accessing code from moduleOne.mjs without 
  // importing moduleOne.mjs because, once moduleOne.mjs 
  // is imported using the script tag in the html file, 
  // the code from it will be added to the global scope.
  console.log(window.carModel); // or console.log(carModel);
  window.drive(); //  or drive();
  window.stop(); //  or jog();
  <!DOCTYPE html>
  <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    </head>
    <body></body>
    <script src="moduleOne.mjs"></script>  <!-- import moduleOne.mjs first -->
    <script src="moduleTwo.mjs"></script>  <!-- import moduleTwo.mjs second -->
  </html>

Output:

Disadvantage #1



An example of using an IIFE to create a private scope (interface). The returned object by the IIFE becomes the public interface. This object is assigned to the carDetails variable and exposed to the global scope when the module is imported.

  // moduleOne.mjs
  var carDetails = (function() {
    // private interface
    var carModel = 'Toyota';
    var carColor = 'Blue';
    var vinNumber = '123456789'; 
    
    function getVinNumber() { 
      return vinNumber;
    }

    function drive() {
      console.log("Driving");
    }

    function stop() {
      console.log("Stopping");
    }

    // public interface
    return {
      carModel,
      carColor,
      drive,
      stop
    }
  })();
  // moduleTwo.mjs
  // only the identifier 'carDetails' is added to the global scope
  // and therefore we need to use it as the namespace to access
  // the variables and functions returned frommoduleOne.mjs
  console.log(carDetails.carModel);
  carDetails.drive(); 
  carDetails.stop(); 
  <!DOCTYPE html>
  <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    </head>
    <body></body>
    <script src="moduleOne.mjs"></script>  <!-- import moduleOne.mjs first -->
    <script src="moduleTwo.mjs"></script>  <!-- import moduleTwo.mjs second -->
  </html>

The reason this works is threefold:

  1. Top level code within a module is executed only once, when the module is initially imported.

  2. Functions create their own scope, so that allows for a private and public interface to be created.

  3. An IIFE is invoked only once, in this case when the modile is imported, and because its an anonymous IIFE, it cannot be invoked again. Even if it was a named IIFE, if the IIFE was not exposed on the public interface, it could not be invoked again. But if it was a named IIFE and exposed, it could be invoked again. An example of what I mean:

      // moduleOne.js
      var carDetails = (function createCarDetails() {
        // private interface code here
        return {
          createCarDetails
        }
      })();

    In this case, createCarDetails is exposed on the the public interface and can be invoked again.

    // moduleTwo.js
    var carDetails.createCarDetails()

    I don’t know why anyone would do this, but I wanted to show it because it’s possible.


    Output:

    Disadvantage #1



If you’re interested in knowing more about the JavaScript module history, I found this article helpful.


I wasn’t programming during that phase of JavaScripts development and therefore did not experience this firsthand in a real project, so if my brief explanation contains errors, please correct me.

Variations

The Revealing Module Design Pattern

The Revealing Module Design Pattern variation differs from the main design pattern by defining all the state and logic in the private interface and exporting only references to them in the public interface.

Code Example

  // bankAccount.mjs

  // All these are private interfaces
  let accountTypes = ['checking', 'savings'];
  let accountBalances = {
    checking: 100,
    savings: 50
  } 

  function calculateInterestPrivate(amount, timePeriod) {
    // Private interest rate calculation implementation
  }

  function getAccountTypes() { 
      return accountTypes
  }

  function getAccountBalance(accountType){
      return accountBalances[accountType]
  }

  function deposit(amount, accountType){
      accountBalances[accountType] += amount;
  }

  function withdraw(amount, accountType){
    if(accountTypes.includes(accountType)){
      if(accountBalances[accountType] >= amount){
        accountBalances[accountType] -= amount;
        console.log(`Withdrew ${amount} from ${accountType}`);
      }
      else {
        console.log(`You don't have enough money in ${accountType}`);
        console.log(`Your balance is ${accountBalances[accountType]}`);
      }
    } else {
      console.log(`Invalid account type`);
      console.log(`Valid account types are ${accountTypes}`);
    }
  }
  
  function calculateInterest(amount, timePeriod){
    calculateInterestPrivate(amount, timePeriod)
  }

  // Public interface
  export default {
    getAccountTypes,
    getAccountBalance,
    deposit,
    withdraw,
    calculateInterest,
  };

Advantages

NOTE: All the advantages of the original Module Design Pattern apply to the Revealing Module Pattern as well as:

  • Simplified Visibility Management: In the Revealing Module Design Pattern, all the state and logic are always defined in the private interface scope, so if you want to expose a private member to the public interface, you simply add it to the returned object (public interface). If you want to make a public member private, you can simply remove it from the returned object or comment it out. This is simpler than the approach in the original Module Design Pattern, where changing the visibility of a member required updating all instances of where that member was accessed.

Disadvantages

NOTE: Besides “Visibility Change Overhead”, all the disadvantages of the original Module Design Pattern apply to The Revealing Module Design Pattern.

The reason “Visibility Change Overhead” doesn’t apply is because all the state and logic are always defined in the private interface scope. Therefore, its scope always remains private, and accessing it always remains consistent, whether the member is exposed through the public interface or not. But the following disadvantages apply:


Disadvantage #1

Reassigning a public variable on the public interface externally will only update that variable within the scope of the exported interface object. The reassignment does not reassign the public variable within the private scope of the module. Therefore, if any function within the module refers to that variable, it will retain the original value assigned to it due to the closure created at the time of the function’s definition, which occurs upon the module’s initial import.


The following code example illustrates this:

  1. We define publicVariable = 10 within moduleOne.js.
  2. We define publicFunction within moduleOne.js that logs publicVariable.
  3. We export publicVariable and publicFunction as part of the public interface of the module.
  4. Within moduleTwo.mjs, we reassign publicVariable = 88.
  5. We log module.publicVariable, which outputs 88.
  6. Then, we invoke moduleOne.publicFunction(), which outputs 10.
  // moduleOne.mjs

  let publicVariable = 10

  function publicFunction() {
    console.log("publicVariable:", publicVariable)
  }

  export default {
    publicVariable, 
    publicFunction
  }
  // moduleTwo.mjs

  import moduleOne from './moduleOne.mjs';

  // Logs 'publicVariable: 10'
  console.log('publicVariable:', moduleOne.publicVariable);

  // Logs "publicVariable: 10"
  moduleOne.publicFunction();

  moduleOne.publicVariable = 88; 

  // Logs "publicVariable: 88"
  console.log('publicVariable:', moduleOne.publicVariable);

  // Still logs "publicVariable: 10"
  // This is because publicFunction within moduleOne.mjs is still 
  // referring to the original value of publicVariable from when the 
  // closure was first created.
  moduleOne.publicFunction(); 

Output:

Disadvantage #1


Solution

To solve this issue, we can expose a setter function as a member of the public interface that allows us to update the public variable from within the module. This will work because the setter function has access to the identifier ‘publicVariable’ enclosed within the closure.

  // moduleOne.mjs

  let publicVariable = 10

  function publicFunction() {
    console.log("publicVariable:", publicVariable)
  }

  // Define a setter for the publicVariable
  function setPublicVariable(value){
    publicVariable = value;
  }

  export default {
    publicVariable, 
    publicFunction,
    // Add the setter function to the public interface
    setPublicVariable 
  }
  // moduleTwo.mjs

  import moduleOne from './moduleOne.mjs';

  // Logs "publicVariable: 10"
  moduleOne.publicFunction();

  // Use setPublicVariable to set the publicVariable
  moduleOne.setPublicVariable(88);

  // Logs "publicVariable: 88"
  moduleOne.publicFunction(); 

Output:

Disadvantage #1



Disadvantage #2

Redefining a function on the public interface externally does not redefine that same function within the private scope of the imported module. Therefore, if any private function within the module references that public function, it will retain the original implementation due to the closure created at the time of the private function’s definition, which occurs upon the module’s initial import.


The following code example illustrates this:

  // moduleOne.mjs
  
  // Define a private function that references a public function
  function privateFunction() {
    publicFunction();
  }
  
  function publicFunction() {
    console.log("publicFunction: original implementation");
  }
  
  // Call the private function after 3 seconds
  setTimeout(() => {
    console.log('-----------setTimeout-------------');
    privateFunction();
  }, 3000);
  
  export default {
    publicFunction
  };
  // moduleTwo.mjs

  import moduleOne from './moduleOne.mjs';
  
  // Logs the original message
  moduleOne.publicFunction();
  
  // Redefine publicFunction
  moduleOne.publicFunction = function() {
    console.log("publicFunction: redefined implementation");
  };
  
  // Logs the new message
  moduleOne.publicFunction();
  
  // After two seconds the `setTimeout` function will call the privateFunction
  // which calls publicFunction, which will still log the original message.
  // This is because privateFunction still retains a reference to the original
  // implementation of publicFunction from when the closure was first created.

Output:

Disadvantage #2


Solution

In this scenario, I can think of one solution, although it is not very practical. The solution involves three steps:

  • Define the public function using a function expression instead of a function declaration.
  • Define a setter function that assigns the public function identifier to a new function and expose it as a member of the public interface.
  • Define a getter function that returns the public function and expose it as a member on the public interface.

This approach allows us to assign a new function to the publicFunction identifier from within the module. However, it requires using the getter function every time we set a new function so that we can get the new public function.


The following code example illustrates this:

  // moduleOne.mjs
  
  // Define a private function that references a public function
  function privateFunction() {
    publicFunction();
  }
  // Define publicFunction using a function expression
  let publicFunction = () => { 
    console.log("publicFunction: original implementation");
  };
  
  // Define a setter function for the publicFunction
  function setPublicFunction(newPublicFunction) {
    publicFunction = newPublicFunction;
  };
  
  // Define a getter function for the publicFunction
  function getPublicFunction() {
    return publicFunction;
  }
  
  setTimeout(() => {
    console.log('-----------setTimeout-------------');
    privateFunction();
  }, 3000);
  
  export default {
    publicFunction,
    setPublicFunction,
    getPublicFunction
  };
  // moduleTwo.mjs
  
  import moduleOne from './moduleOne.mjs';
  
  // Use the getter function to get the public function
  let publicFunction = moduleOne.getPublicFunction();
  // Logs the original message
  publicFunction();
  
  const newPublicFunction = function() { 
    console.log("publicFunction: redefined implementation");
  };
  // Use the setter function to set a new public function
  moduleOne.setPublicFunction(newPublicFunction); 
  
  
  // Again, use the getter function to get the new public function
  publicFunction = moduleOne.getPublicFunction();
  // logs the new message
  publicFunction();
  
  // After two seconds the `setTimeout` function will call the privateFunction
  // which calls publicFunction, which will log the new message.

Output:

Disadvantage #2

Wrapping Up

The “Learning JavaScript Design Patterns” closes off the chapter with the following:

“A disadvantage of this pattern is that if a private function refers to a public function, that public function can’t be overridden if a patch is necessary. This is because the private function will continue to refer to the private implementation, and the pattern doesn’t apply to public members, only to functions.

Public object members, which refer to private variables, are also subject to the no-patch rule.”

I couldn’t make sense of the following: “Public object members, which refer to private variables, are also subject to the no-patch rule.” and therefore I wasn’t able to illustrate it within the code examples. If someone else understands this, please let me know.

I found the language in the chapter slightly confusing, as someone else that I spoke with did too. The chapter uses the words “overriding” and “patching”, and I inferred them to mean the same and used interchangably but decided to not use them in my article because I found them confusing, as “overriding” is usually mentioned in the context of overriding class methods and not methods of an object And “patching” doesn’t seem to be a thing in the javascript world. I am sure these words were was used in a general sense, but I’ve opted not to use them in my article.

I enjoyed learning all that I’ve documented in this article. I also learned that JavaScript modules are singletons! They aren’t implemented using the Singleton design pattern (from my little research), but after the initial import, the JavaScript runtime caches the module, and the same instance is returned every time the module is imported again.

If I am wrong about any of the code examples, please correct me. Thank you for reading.

Resources


© 2024 NazCodeland