Revisiting the "Revealing Module pattern"

efpage

Eckehard

Posted on April 27, 2024

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!

💖 💪 🙅 🚩
efpage
Eckehard

Posted on April 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related