Skip to content

Instantly share code, notes, and snippets.

@patrickkunka
Last active January 8, 2019 10:33
Show Gist options
  • Save patrickkunka/2019d6fe45abf9d5c5f8e764d05539d3 to your computer and use it in GitHub Desktop.
Save patrickkunka/2019d6fe45abf9d5c5f8e764d05539d3 to your computer and use it in GitHub Desktop.
Facade Patterns
/**
* Facade #1: Public/Private Members
*
* In this example, a "Facade" is placed between the consumer
* and the implementation in order to cherry-pick which public
* members we wish to expose to the API.
*/
class Implementation {
constructor() {
/**
* @private
*/
this.myPrivateProp1 = false;
/**
* @private
*/
this.myPrivateProp2 = -1;
/**
* @public
*/
this.myPublicProp1 = '';
/**
* @public
*/
this.myPublicProp2 = null;
Object.seal(this);
}
/**
* @private
*/
myPrivateMethod1() {
// Do something private
}
/**
* @private
*/
myPrivateMethod2() {
// Do something private
}
/**
* @public
*/
myPublicMethod1() {
// Do something public
}
/**
* @public
*/
myPublicMethod2() {
// Do something public
}
}
class Facade {
constructor() {
// A private instance of the implemetation is created
// within the facade's closure
const _ = new Implementation();
// Members are then exposed one by one:
// Methods are lexically bound to the implementation instance
this.myPublicMethod1 = _.myPublicMethod1.bind(_);
this.myPublicMethod2 = _.myPublicMethod2.bind(_);
// Properties can be made read-only by only exposing
// "getter" functions
Object.defineProperties(this, {
myPublicProp1: {
get: () => _.myPublicProp1
},
myPublicProp2: {
get: () => _.myPublicProp2
}
});
// NB: Rather than using the ES6 `get()/set()` syntax on the
// prototype, we must define getters within the constructor closure
// using `Object.defineProperties()` to provide access to the private
// implentation instance variable (`_`)
// The facade is always stateless and can be frozen for an extra
// layer of robustness.
Object.freeze(this);
// If monkey patching or spies are expected, Object.seal(this)
// will suffice
}
}
// Only the facade is exported
export default Facade;
/**
* Facade #2: Factories and Facades
*
* Hiding the implementation can hinder development and
* debugging when we actually need to interrogate the state
* of something private.
*
* By passing a `debug` option, we can have our factory
* return the full implementation when needed.
*/
import Facade from './Facade';
import Implementation from './Implementation';
export default function factory(config={}) {
if (config.debug === true) {
return new Implementation(config);
}
return new Facade(config);
}
/**
* Facade #3: Interfaces
*
* Facades can be used like "interfaces" in other languages, to
* establish an API contract that consumer code must implement. This
* is particularly useful when we open up our software to integration
* with consumer-provided "plugins".
*
* In this example, all consumer provided plugins must implement the
* `IPlugin` interface.
*/
/** --------- Consumer Code ---------- */
class PluginImplementation {
// An arbitrary implementation of a "plugin" for some
// vendor provided software
}
// As the public API is defined by `IPlugin` (see below), we simply need to
// extend it to create a facade for the plugin. We could also expose additional
// public members at this point.
class PluginFacade extends IPlugin {}
// Access to the implementation class is provided via a static
// `Implementation` property on the Facade
PluginFacade.Implementation = PluginImplementation;
export default PluginFacade;
/** --------- Vendor Code ---------- */
class IPlugin {
constructor() {
// The implemenation class is accessed via a static property
// of the constructor (the plugin facade).
const plugin = new this.constructor.Implementation();
this.myPublicMethod1 = plugin.myPublicMethod1.bind(plugin);
this.myPublicMethod2 = plugin.myPublicMethod2.bind(plugin);
Object.defineProperties(this, {
myPublicProp1: {
get: () => plugin.myPublicProp1
},
myPublicProp2: {
get: () => plugin.myPublicProp2
}
});
Object.seal(this);
}
}
/**
* A factory function forming the primary public entry point
* of the vendor code's API, as demonstrated in previous gists.
*/
function factory() { ... }
/**
* A static method for registering plugins.
*
* @param {function} Plugin
* @return {void}
*/
factory.use = (Plugin) => {
const plugin = new Plugin();
// At this point, we check if the consumer provided plugin
// implements `IPlugin`, and if not, we reject the registration.
if (!(plugin instanceof IPlugin)) {
throw new TypeError('Plugins must implement IPlugin');
}
// Register plugin ...
}
// As well as the factory itself, we'll want to expose IPlugin
// in order for consumer code to implement it.
export IPlugin;
export default factory;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment