DEV Community

Cover image for Revisiting the "Revealing Module pattern"
Eckehard
Eckehard

Posted on

Revisiting the "Revealing Module pattern"

(Cover Image source)

Maybe you've heard of the "Revealing module pattern" [RMP], which is a way to create protected code modules in Javascript. Unlike JS class objects, code inside the modules cannot be altered from the outside, which can be a huge benefit to protect your code from it´s worst enemy: you yourself! There are quite some explanations on the pattern on the net (even on dev.to), but I like to show some extensions to make the patter more useful.

The RMP uses the fact that local functions and variables created inside a function cannot be reached from the outside. The function body forms a local scope, that is insulated from the rest of the program. To make elements inside the function body available, the main function returns an object containing the all referencs that should be accessible. See an example here:

function Person(myName) {
  let name = myName

  function log(txt) { console.log(txt) }
  function public_talk() { log(name + " is talking") }
  function public_dance() { log(name + " is dancing") }

  // interface
  return {
    talk: public_talk,
    dance: public_dance
  }
}

let father = Person("Peter")

father.talk()
father.dance()
Enter fullscreen mode Exit fullscreen mode

Person returns an object contaning just the two functions talk and dance, so all the "internals" are hiden to the outside world. In contrast to JS classes, the code inside an RMP looks pretty normal, in fact the RMP-body looks and works exactly like a small program inside the rest of the code. But there are some downsides too. Most of all, they lack any form of inheritance, so RMP-modules are not open for extensions. But let´s see, what we can do on this...

JS Tricks and shortcuts

The examples below use some JS-"tricks" that are absolute standard, but which you may or may not be familiar with, so here is a short introduction:

// Destructuring
let {a, b} = myFunction()
-> if myFunction returns an object {a:1, b:2, c:3}, a and b are assigned to local variables.  

// Arrow-functions
function value(){return x}
value = () => x
-> Shorter, if you just need to return a value

// ES6-shorthands
let a=1, b=2
let ob = {a:a, b:b}
-> instead, you can just write ob = {a,b}

// getter and setter
let a=1, b=2
let ob = {
  get a(){ return a}, 
  set a(x){ a = x },
  b
}
-> setter can be used like a normal proerty ob.a = 5, but invokes a function call
Enter fullscreen mode Exit fullscreen mode

This "tricks" are important to know to understand the following code examples.

Improving the Revealing Module Pattern

First, let see if we can make our example a bit smarter:

function Person(name) {
  function log(txt) { console.log(txt) }
  function talk() { log(name + " is talking") }
  function dance() { log(name + " is dancing") }

  // interface
  return {
    talk,
    dance
  }
}

let father = Person("Peter")
...
Enter fullscreen mode Exit fullscreen mode

We do not need a variable to keep the name, as name is already a local variable in the function scope. So Parameters do not need to be stored and can be altered too. And we do not need separate names for our functions using ES6-shorthands.

Removing limitations

But what about changing values from the outside? Ok, you can build a function to to the job. But there are better ways. Let´s try to expose a "variable":

function Person(name) {
  function log(txt) { console.log(txt) }
  function talk() { log(name + " is talking") }
  function dance() { log(name + " is dancing") }

  // interface
  return {
    name,
    talk,
    dance
  }
}

let father = Person("Peter")
father.talk() // --> Peter is talking
father.name = "Paul"
father.talk() // --> Peter is talking
Enter fullscreen mode Exit fullscreen mode

This does not work, as the object just returns a copy of our value. Even if we change the value of name inernally, this would not be reflected in the result. But we can use getters and setter to get what we want:

...
// interface
  return {
    get name(){return name},
    set name(x){name = x},
    talk,
    dance
  }
  father.talk() // --> Peter is talking
  father.name = "Paul"
  father.talk() // --> Paul is talking
Enter fullscreen mode Exit fullscreen mode

Hint: Be careful with getters! They work only in the initial context. So, you can use father.name = "Newname". But if you destructure father, you will receive a string, not a getter:

let {name} = father
name = "Paul" // --> does not change the internal variable
father.name = "Paul" // --> does change the internal variable
Enter fullscreen mode Exit fullscreen mode

How to inherit?

This is my very special pattern to implement a simple form of inheritance. Often it comes handy to be able to extend or change an existing "module" without changing the initial code. Hier is my proposal:

function Person(name) {
  function log(txt) { console.log(txt) }
  function talk() { log(name + " is talking") }
  function dance() { log(name + " is dancing") }

  // interface
  return {
    private: {
      log,
      get name(){return name}
    },
    talk,
    dance
  }
}

function Woman(_name) {
  // Inherit
  let Super = Person(_name)
  let { name, log } = Super.private

  function talk() { log(name + " is a talking woman"); }
  function jump() { log(name + " is jumping") }

  return Object.assign(Super, { talk, jump }) // Override talk
}


let father = Person("Peter")
let mother = Woman("Claire")

father.talk()
father.dance()

mother.talk()
mother.dance()
mother.jump()
Enter fullscreen mode Exit fullscreen mode

Some comments on the code:

Super: This object keeps all references from the "parent" module. You can add new functions to the class by extending the object. Initially i tried this:

return{...Super, { talk, jump }}
Enter fullscreen mode Exit fullscreen mode

This works, but breaks the getters and setters. So, it is advised to use Object.assign() to extend the parent class.

private: This retrns references that are not intended for external use. You do not need to use this, but putting some references inside a sub-object reminds me on the task.

destructuring: You can simply use Super.private.log(), but destructuring make the functions available in the local scope. It´s simply easier to read. But be careful: Destructuring name returns a string. If you want to invoke the getter, you need to use Super.private.name instead.

polymorphism: There is a pretty simple way to even change parent frunctions from inside a child class. Just use a setter to make parent functions mutable:

...Parent: 
// interface
  return {
    private: {
      get log(){log},
      set log(x){log = x}
    }
  }

...Child:
  function mylog(){}
  Super.private.log = mylog
Enter fullscreen mode Exit fullscreen mode

Ok, the RMP will not provide anything a full featured class system may contain, but it has a lot of advantage:

  • You can use the same code inside and outside the module
  • Variables and functions are protected by desing until you manually expose them to the public
  • The RMP is simple Javascript, no hidden gems. So it will most likely work on a wide range of browsers without any polyfilling

The RMP can be a powerful part in your toolbox. With some extensions, it can be even more flexible.

Here is a working example to see all patterns in action

Happy coding!

Top comments (5)

Collapse
 
lionelrowe profile image
lionel-rowe

If you just want a module (doesn't need to be instantiated, stateless or very limited state), you can use a native module, which implements fully private functions/variables. Anything not exported can't ever be accessed outside the module file it lives in.

function private() {
    console.log('ok')
}
export function public() {
    private()
}
Enter fullscreen mode Exit fullscreen mode

If you want a class (needs to be instantiated), you can use a native class with private properties and methods. Again, these can't be accessed outside the class they're declared in.

class Class {
    #private() {
        console.log('ok')
    }
    public() {
        this.#private()
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
efpage profile image
Eckehard

Referring do caniUse private class fields are supported by most desktop browsers since 2021, chrome for Android adopted the features just this month (May 2024), so it is still risky to use.

Modules are a bit of a false. They provide only a one way insulation. global variables defined in a module will not be accessible from the outside, but modules do not pevent external scopes to leak in:

Assume you have a "module" script.js:

export function test(){
    console.log(a)
}
Enter fullscreen mode Exit fullscreen mode

Now, this is jour HTML file:

...
<body>
  <script>
    let a = "Hello World"
  </script>
  <script type="module">
    import { test } from "./script.js";
    test() // -> Hello World
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

As you see, you cannot prevent global scopes to leak into modules. Therefore it is a good advice to not use any global variables at all to prevent confusion.

Collapse
 
lionelrowe profile image
lionel-rowe

They provide only a one way insulation. modules do not pevent external scopes to leak in

That's not a limitation of modules though, it's a limitation of JS as a language — JS lacks a way of preventing external scopes from leaking in. Your examples simulating modules/classes with function closures have exactly the same privacy characteristics. If you want to avoid global state, just avoid accessing variables that aren't defined in the current scope (a linter will help a lot with this).

Thread Thread
 
efpage profile image
Eckehard • Edited

Sure, this is a limitation of the language. But accessing variables unintentional is a very common source of severe and hard to track errors. And the chance increases with your project size. There are many reasons this kind of errors happen:

  • Changing code during refactoring, but leaving some code unchanged
  • Misspelling identifiers
  • Reusing buggy code

To make it even worse, ALL Javascript ID´s are reflected in the global scope, so even this works:

  <p id="a">This is a text</p>
  <script type="module">
    import { test } from "./script.js";
    test() //-> p#a
  </script>
Enter fullscreen mode Exit fullscreen mode

Script.js can even change your page content:

export function test(){
    console.log(a)
    a.textContent = "The world is dying"
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
artydev profile image
artydev • Edited

I am a fan of this pattern, thank you for this tip :-)
Object.assign is not sufficiently used.