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.
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:
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:
Top level code within a module is executed only once, when the module is initially imported.
Functions create their own scope, so that allows for a private and public interface to be created.
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:
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:
- We define
publicVariable = 10
withinmoduleOne.js
. - We define
publicFunction
withinmoduleOne.js
that logspublicVariable
. - We export
publicVariable
andpublicFunction
as part of the public interface of the module. - Within
moduleTwo.mjs
, we reassignpublicVariable = 88
. - We log
module.publicVariable
, which outputs88
. - Then, we invoke
moduleOne.publicFunction()
, which outputs10
.
// 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:
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 #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:
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:
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
- âLearning JavaScript Design Patternsâ Second Edition by Addy Osmani, specifically Chapter 7: JavaScript Design Patterns.
- A History of JavaScript Modules and Bundling, For the Post-ES6 Developer