DEV Community

Cover image for How to organize your JS code: Part 2
Muhammad Ahmad
Muhammad Ahmad

Posted on

How to organize your JS code: Part 2

Modules

The module pattern has essentially the same goal as the class pattern, which is to group data and behavior together into logical units.
Also like classes, modules can “include” or “access” the data and behaviors of other modules, for cooperation sake.
But modules have some important differences from classes. Most notably, the syntax is entirely different.

Classic Modules

ES6 added a module syntax form to native JS syntax, which we’ll look at in a moment. But from the early days of JS, modules was an important and common pattern that was leveraged in countless JS programs, even without a dedicated syntax.
The key hallmarks of a classic module are an outer function (that runs at least once), which returns an “instance” of the module with one or more functions exposed that can operate on the module instance’s internal (hidden) data.

Because a module of this form is just a function, and calling it produces an “instance” of the module, another description for these functions is “module factories”.
Consider the classic module form of the earlier Publication, Book, and BlogPost classes:

function Publication(title, author, pubDate) {
    var publicAPI = {
        print() {
            console.log(`
 Title: ${ title }
 By: ${ author }
 ${ pubDate }
 `);
        }
    };
    return publicAPI;
}

function Book(bookDetails) {
    var pub = Publication(
        bookDetails.title,
        bookDetails.author,
        bookDetails.publishedOn
    );
    var publicAPI = {
        print() {
            pub.print();
            console.log(`
 Publisher: ${ bookDetails.publisher }
 ISBN: ${ bookDetails.ISBN }
 `);
        }
    };
    return publicAPI;
}

function BlogPost(title, author, pubDate, URL) {
    var pub = Publication(title, author, pubDate);
    var publicAPI = {
        print() {
            pub.print();
            console.log(URL);
        }
    };
    return publicAPI;
}
Enter fullscreen mode Exit fullscreen mode

Comparing these forms to the class forms, there are more similarities than differences.
The class form stores methods and data on an object instance, which must be accessed with the this. prefix. With modules, the methods and data are accessed as identifier variables in scope, without any this. prefix.

With class, the “API” of an instance is implicit in the class definition—also, all data and methods are public. With the module factory function, you explicitly create and return an object with any publicly exposed methods, and any data or other unreferenced methods remain private inside the factory function.

There are other variations to this factory function form that are quite common across JS, even in 2020; you may run across these forms in different JS programs: AMD (Asynchronous Module Definition), UMD (Universal Module Definition), and CommonJS (classic Node.js-style modules). The variations, however, are minor (yet not quite compatible).
Still, all of these forms rely on the same basic principles.
Consider also the usage (aka, “instantiation”) of these module factory functions:

var Article = Book({
    title: "How to organize JS",
    author: "Muhammad Ahmad",
    publishedOn: "August 2021",
    publisher: "dev.to",
});
Article.print();
// Title: How to organize JS
// By: Muhammad Ahmad
// August 2021
// Publisher: dev.to
Enter fullscreen mode Exit fullscreen mode

The only observable difference here is the lack of using new, calling the module factories as normal functions.

ES Modules

ES modules (ESM), introduced to the JS language in ES6, are meant to serve much the same spirit and purpose as the existing classic modules just described, especially taking into account important variations and use cases from AMD, UMD, and CommonJS.
The implementation approach does, however, differ significantly.

  1. There’s no wrapping function to define a module. The wrapping context is a file. ESMs are always file-based; one file, one module

  2. You don’t interact with a module’s “API” explicitly, but rather use the export keyword to add a variable or method to its public API definition.
    If something is defined in a module but not exported, then it stays hidden (just as with classic modules).

  3. And maybe most noticeably different from previously discussed patterns, you don’t “instantiate” an ES module, you just import it to use its single instance. ESMs are, in effect, “singletons,” in that there’s only one instance ever created, at first import in your program, and all other imports just receive a reference to that same single instance. If your module needs to support multiple instantiations, you have to provide a classic module-style factory function on your ESM definition for that purpose.

In our running example, we do assume multiple-instantiation, so these following snippets will mix both ESM and classic modules.

Consider the file publication.js:

function printDetails(title, author, pubDate) {
    console.log(`
 Title: ${ title }
 By: ${ author }
 ${ pubDate }
 `);
}
export function create(title, author, pubDate) {
    var publicAPI = {
        print() {
            printDetails(title, author, pubDate);
        }
    };
    return publicAPI;
}
Enter fullscreen mode Exit fullscreen mode

To import and use this module, from another ES module like blogpost.js:

import {
    create as createPub
} from "publication.js";

function printDetails(pub, URL) {
    pub.print();
    console.log(URL);
}
export function create(title, author, pubDate, URL) {
    var pub = createPub(title, author, pubDate);
    var publicAPI = {
        print() {
            printDetails(pub, URL);
        }
    };
    return publicAPI;
}
Enter fullscreen mode Exit fullscreen mode

And finally, to use this module, we import into another ES module like main.js:

import { create as newBlogPost } from "blogpost.js";

var organizeJsP2 = newBlogPost(
    "How to organize your JS code: Part 2",
    "Muhammad Ahmad",
    "October 27, 2014",
    "https://dev.to/0xf10yd/how-to-organize-your-js-code-part-2-1401"
);
forAgainstLet.print();
// How to organize your JS code: Part 2
// By: Muhammad Ahmad
// August 18, 2021
// https://dev.to/0xf10yd/how-to-organize-your-js-code-part-2-1401
Enter fullscreen mode Exit fullscreen mode

The as newBlogPost clause in the import statement is optional; if omitted, a top-level function just named create(..) would be imported. In this case, I’m renaming it for readability sake; its more generic factory name of create(..) becomes more semantically descriptive of its purpose as newBlogPost(..).

As shown, ES modules can use classic modules internally if they need to support multiple-instantiation. Alternatively, we could have exposed a class from our module instead of a create(..) factory function, with generally the same outcome. However, since you’re already using ESM at that point, I’d recommend sticking with classic modules instead of class

If your module only needs a single instance, you can skip the extra layers of complexity: export its public methods directly.

Discussion (0)