Revisiting the "Revealing Module pattern"
Eckehard
Posted on April 27, 2024
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()
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
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")
...
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
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
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
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()
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 }}
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
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!
Posted on April 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.