From my experience, learning JavaScript has been like opening Pandora's box. There are so many topics to study, so many niche functions, that I often lose myself out of curiosity. Sometimes it feels like my time is well spent, and other times it feels like I'm giving in to an inner desire to procrastinate with distractions. Harder still, is finding ways to implement any new knowledge into everyday practice. So I gave it a shot with the module pattern!
I work in several different code bases at my job. One of our largest code bases is a behemoth of a project file, and parsing through some of the front-end can be a little tough at times. Not because any singular person wrote bad code, but because the project was started at a time of change for the department, and certain standards weren't put in place. Standards involving more subtle things like the use of global variables. In an effort to provide a solution for these issues, I decided to research how other companies structure their front ends to keep the code base easier to maintain. First, let's start by listing out the issues we're trying to solve:
- Over-reliance on global variables.
- Redundant and cumbersome naming conventions for declared functions/variables.
- No consistency in styling across the project's javascript files.
While I came across several unique and interesting solutions, the one that stuck the most with me, was the module pattern. I loved it's simplicity in design, and it seemed like the perfect solution for the code-base I was working with at the time.
The Basics
The module pattern is just an organizational structure for your code. The basic premise is that you have one or more global objects that house your application's modules. What does that actually look like? Let's put down some code.
In the spirit of remaining on brand, let's pretend we're making an Animal Crossing themed application called 'Nookbook'. First, we create a new global variable/reference called Nookbook
and set it to an Immediately-invoked Function Expression (IIFE). This post won't necessarily focus on how IIFEs work, but if you'd like to read up on them, you can do so on MDN.
const Nookbook = (() => {
const _modules = {};
const _createModule = (_moduleName) => {
((N) => {
_modules.moduleName = {};
N[moduleName] = { get N() {return _modules.moduleName; } };
})(Nookbook);
};
return {
createModule(moduleName) {
_createModule(moduleName);
}
};
})();
The module pattern works by storing everything in a series of contextual hierarchies that take form through the use of objects. Our Nookbook app could have several modules that one might imagine an Animal Crossing app to have. Such as a 'Marketplace' module, or perhaps a 'Profile' module that contains functionality surrounding user profiles. In those cases, we could create what we refer to as a namespace for those modules by using our createModule
function. Notice that it simply calls the _createModule
function declared within our IIFE's scope. The typical naming convention for variables declared within an IIFE is to prefix them with underscores in order to differentiate what's scoped to the IIFE and what's not. This is important, since IIFE's are anonymously scoped, their inner properties cannot be accessed unless we interact with them through the return object's methods. To create a module:
Nookbook.createModule('Marketplace');
// This is what our IIFE looks like after running the above fn.
const Nookbook = (() => {
const _modules = {
Marketplace: {}
};
const _createModule = (_moduleName) => {
...
};
return {
createModule(moduleName) {
_createModule(moduleName);
},
get Marketplace() {
return _modules.Marketplace;
}
};
})();
Notice we created an object called Marketplace
that we're storing in our _modules
object. It also adds a method to the return object of Nookbook
. The method uses the get
syntax to allow us to access the newly created object directly. This line is what creates that getter:
N[moduleName] = { get N() { return _modules.moduleName; }
Here, N
is just the alias we gave our Nookbook IIFE. All we are doing is creating a getter for our marketplace object -- the function simply returns the module's object. Now if we want to add functionality to our marketplace, we can simply declare functions in the standard way:
Nookbook.Marketplace.addItem = (itemName, askingPrice) => {
// ... code here
};
// To call the function:
Nookbook.Marketplace.addItem('Ironwood Kitchenette', 150000);
It's as simple as that!
Benefits
So what exactly are the benefits of structuring your applications around this design pattern? Introducing any design structure, by default introduces standards that will make your code more uniform. In this case, the paths of your functions now contain contextual information. Not only is our code more uniform, it also categorizes and houses information in a more meaningful way:
// Standard function declaration.
function addUserProfile() { ... };
function updateProfileInformation() { ... };
// Object notation is easier to read and provides context.
Nookbook.Profile.add = () => { ... };
Nookbook.Profile.update = () => { ... };
Often times, knowing a function is contained within the Profile
module is enough context to understand the functions intent. This means we can start to simplify naming conventions and actually make code more intuitive to read.
Let's keep diving further. Say we want to separate out module-specific constants for things that don't change often -- like file paths. Instead of relying on global variables, we can simply create an object to hold our constants for each module.
// We begin by creating an empty object to hold our constants.
Nookbook.Profile.CONST = {};
// Then we can organize our constants however we like.
Nookbook.Profile.CONST.PATHS = {
MAIN: '../Profile/main.html',
FRIENDS: '../Profile/friends.html'
};
// Here's an alternative way of declaring what we wrote above in a more concise way.
Nookbook.Profile.CONST = {
PATHS: {
MAIN: '../Profile/main.html',
FRIENDS: '../Profile/friends.html'
}
};
This creates an easy to remember location for all of our constant variables. If you design your own naming standards, then you begin to develop more consistency in the long term! In my case, I set the standard that every module has a CONST
object holding all of it's constants. Now, no matter which module I'm working in, I always know where all of my constants are declared. Next, let's create some functions that behave in a 'global' manner.
const Nookbook = (() => {
const _modules = {};
const _createModule = (_moduleName) => {
...
};
const _loadPage = (_pageName) => {
// code that makes a server call for desired file
};
return {
createModule(moduleName) {
_createModule(moduleName);
},
loadPage(pageName) {
_loadPage(pageName);
}
};
})();
In the above example we added a function called loadPage
that we're pretending has code that makes a server call for an HTML file. By creating this function in the main Nookbook
IIFE, we can think of it as a global function, because it's not contained within any one specific module, and every module has access to it:
Nookbook.Profile.loadFriends = () => {
Nookbook.loadPage(Nookbook.Profile.CONST.PATHS.FRIENDS);
};
We now begin to see how nicely all of this begins to fit together. We call on our new loadPage() function in our module, and we call on our object holding our constants for the page's file path. Everything is incredibly easy to read through, if perhaps verging on being a little verbose.
Drawbacks
Personally, I haven't encountered all too many drawbacks to the module pattern except that it can be complicated to integrate into an existing code-bases. It can also get a little verbose for applications that are incredibly large. If you have modules with several sub-modules, the contextual paths may get a little tedious to work with:
Nookbook.Profile.Wishlist.add = (itemName) => { ... };
Having to type Nookbook.Profile.Wishlist
for each function I want to declare for the wishlist sub-module is a little annoying. Fortunately, you could just create local references, such as:
const NPW = Nookbook.Profile.Wishlist;
NPW.add = () => { ... };
The only problem with a reference like this, is that they become global, and thus began to slowly defeat the original purpose of using the module pattern -- at least in my case. I have found that you can often just design the code in a way that relies on more modules and less sub-modules, but it's still a limiting factor. However, since the original goal was to simply lower global variable usage, having these references isn't a big deal. The problem lies with the fact that if your app is being worked on by multiple developers, you need to develop standards for where these global references are declared as early as possible. You wouldn't want developers accidentally declaring references with the same name, but to different modules. Here are two imaginary modules with sub-modules where this could be an issue:
const NPS = Nookbook.Profile.Settings;
const NPS = Nookbook.Pattern.Storage;
If you don't have standards in place to account for this, you could potentially start to run into issues!
Conclusions
I'm still seeing how far I can take this design structure, so I'll keep posting more as I find cool/unique ways to implement and use the module pattern. For now, all I can say, is that it's already beginning to help organize our code and cut down on headaches with overlapping functionality and redundant function names.
If you have any questions, please feel free to ask. If you spotted anything wrong in this post, please let me know so that I may correct it! Also, as I am still learning, I would greatly appreciate hearing your experience and discoveries working with the module pattern!
Update: If you'd like to read more, here is the second post in this series!
Top comments (0)