The Facade Pattern
The facade pattern is a structural design pattern that abstracts complex underlying systems to expose a streamlined, use-case-specific interface. By wrapping intricate implementation details, it reduces code duplication, improves readability, and simplifies testing.
In frontend development, facades often apear as standalone functions, objects, or modules. For applications with modular architectures, facades can be nested to layer abstractions, with top-level facades orchestrating sub-modules while hiding internal complexity.
Core Benefits
- Reduces boilerplate code by encapsulating repeated low-level operations
- Improves code readability by using method names aligned with business concepts
- Simplifies testing by proviidng a single, stable entry point to complex systems
- Decouples application code from third-party libraries, making replacements easier
A simple facade example wraps a third-party math library to focus on a specific use case:
function generateAdjustedRandom(base, range) {
const normalizedBase = base - range / 3;
const squaredRange = Math.pow(range, 2);
const randomOffset = 2.5 * Math.random();
const defaultPrecision = 2;
return MathLib.computeScaledValue(normalizedBase, squaredRange, randomOffset, defaultPrecision);
}
jQuery and Facades
jQuery itself is a collection of facades. Its core APIs wrap raw DOM, XMLHttpRequest, and event handling, providing consistent, concise methods across browsers.
DOM Traversal
Before jQuery, developers relied on limited, inconsistent DOM methods like getElementById and getElementsByTagName. jQuery's $() function (backed by the Sizzle selector engine) provides a unified facade for selecting elements with CSS selectors, handling cross-browser inconsistencies internally. Even modern querySelectorAll benefits from jQuery's optimizations and edge-case handling.
Property Access
jQuery's $.fn.prop() and $.fn.attr() methods act as facades for standardizing property/attribute access across browsers. They handle differences in naming (e.g., class vs className) and provide getter/setter behavior in a single method:
jQuery.extend({
propUtil: function(element, name, value) {
if (element.nodeType !== 1 || jQuery.isXMLDoc(element)) return;
const fixedName = jQuery.propMap[name] || name;
const hook = jQuery.propHooks[fixedName];
if (value !== undefined) {
if (hook && hook.set) {
const result = hook.set(element, value, fixedName);
if (result !== undefined) return result;
}
return (element[fixedName] = value);
}
if (hook && hook.get) {
const result = hook.get(element, fixedName);
if (result !== null) return result;
}
return element[fixedName];
},
propMap: { "for": "htmlFor", "class": "className" },
propHooks: {
tabIndex: {
get: function(el) {
const attr = jQuery.find.attr(el, "tabindex");
return attr ? parseInt(attr, 10) : -1;
}
}
}
});
Practical Application: Random Item Picker
To demonstrate facades in action, we'll build a simple random item picker that uses unique IDs and decouples from third-party libraries.
First, the main module:
(function() {
window.randomItemPicker = window.randomItemPicker || {};
const itemIds = [];
let $pickerContainer;
const itemCount = 40;
randomItemPicker.init = function() {
itemIds.length = 0;
$pickerContainer = $('#picker-container').empty();
randomItemPicker.addItems(itemCount);
$('#pick-btn').on('click', randomItemPicker.selectRandom);
};
randomItemPicker.addItems = function(count) {
for (let i = 0; i < count; i++) {
const id = randomItemPicker.uniqueIdGenerator.getNext();
itemIds.push(id);
$pickerContainer.append(randomItemPicker.itemRenderer.createHtml(id));
}
};
randomItemPicker.selectRandom = function() {
const index = Math.floor(Math.random() * itemIds.length);
const selectedValue = $pickerContainer.find('#' + itemIds[index]).text();
alert('Selected: ' + selectedValue);
return selectedValue;
};
$(document).ready(randomItemPicker.init);
})();
Next, the item renderer facade:
(function() {
randomItemPicker.itemRenderer = randomItemPicker.itemRenderer || {};
randomItemPicker.itemRenderer.createHtml = function(id) {
const itemValue = Math.floor(Math.random() * 2000);
return '<div id="' + id + '" class="picker-item">' + itemValue + '</div>';
};
})();
Then, the unique ID generator facade, which wraps a third-party library:
(function() {
randomItemPicker.uniqueIdGenerator = randomItemPicker.uniqueIdGenerator || {};
randomItemPicker.uniqueIdGenerator.getNext = function() {
return 'pick-' + simpleGuid.generate();
};
})();
If we later switch to a different ID library, only the uniqueIdGenerator module needs changes:
(function() {
randomItemPicker.uniqueIdGenerator = randomItemPicker.uniqueIdGenerator || {};
randomItemPicker.uniqueIdGenerator.getNext = function() {
return uuid.v4();
};
})();
Creational Patterns: Factory and Builder
Creational pattrens abstract object creation, decoupling application code from specific classes or initialization steps.
Factory Pattern
The factory pattern defines an interface for creating objects, letting subclasses or implementations decide which class to instantiate. It's ideal for cases where object creation depends on runtime conditions or requires cross-browser compatibility.
jQuery's AJAX Factory
jQuery uses the factory pattern internally for creating XMLHttpRequest (XHR) objects. The jQuery.ajaxSettings.xhr function acts as a factory, returning the appropriate XHR implementation for the current browser (e.g., XMLHttpRequest for modern browsers, ActiveXObject for older IE):
jQuery.ajaxSettings.xhr = window.XMLHttpRequest !== undefined ?
function() {
try { return new window.XMLHttpRequest(); } catch (e) {}
} :
function() {
try { return new window.ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
};
Practical Application: Data-Driven Form Factory
We'll build a factory that generates HTML form elements based on a configuration array. The factory accepts a type, name, and label, returning the appropriate HTML string with sensible defaults.
First, the HTML:
<h1>Data-Driven Form</h1>
<form id="dynamic-form"></form>
<script src="jquery.js"></script>
<script src="form-factory.js"></script>
Next, the factory implementation:
(function() {
'use strict';
window.formFactory = window.formFactory || {};
formFactory.createElement = function(config) {
const label = config.label || config.name;
let html = '';
switch (config.type) {
case 'text':
html = '<div><label><span>' + label + ':</span><br>' +
'<input type="text" maxlength="250" name="' + config.name + '">' +
'</label></div>';
break;
case 'email':
html = '<div><label><span>' + label + ':</span><br>' +
'<input type="email" required name="' + config.name + '">' +
'</label></div>';
break;
case 'number':
html = '<div><label><span>' + label + ':</span><br>' +
'<input type="number" min="0" max="1000000" name="' + config.name + '">' +
'</label></div>';
break;
case 'textarea':
html = '<div><label><span>' + label + ':</span><br>' +
'<textarea cols="35" rows="4" maxlength="1000" name="' + config.name + '"></textarea>' +
'</label></div>';
break;
case 'checkbox':
html = '<div><label><span>' + label + ':</span>' +
'<input type="checkbox" name="' + config.name + '">' +
'</label></div>';
break;
case 'notice':
html = '<p>' + config.name + '</p>';
break;
case 'button':
html = '<button name="' + config.name + '">' + label + '</button>';
break;
}
return html;
};
formFactory.configs = [
{ type: 'text', name: 'firstName', label: 'First Name' },
{ type: 'text', name: 'lastName', label: 'Last Name' },
{ type: 'email', name: 'email', label: 'Email Address' },
{ type: 'number', name: 'experience', label: 'Years of Experience' },
{ type: 'textarea', name: 'bio', label: 'Short Bio' },
{ type: 'checkbox', name: 'newsletter', label: 'Subscribe to Newsletter' },
{ type: 'notice', name: 'By submitting, you agree to our terms.' },
{ type: 'button', name: 'submit', label: 'Submit' }
];
formFactory.init = function() {
const $form = $('#dynamic-form');
formFactory.configs.forEach(function(config) {
const elementHtml = formFactory.createElement(config);
if (elementHtml) $form.append(elementHtml);
});
};
$(document).ready(formFactory.init);
})();
Builder Pattern
The builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It's ideal for objects that require many optional parameters or step-by-step configuration.
jQuery's Element Builder
jQuery uses the builder pattern when creating new DOM elements with $('<div>'). The returned jQuery object acts as a builder, allowing chained method calls to configure the element before attaching it to the DOM:
$('<input>')
.attr({ type: 'number', min: '0', max: '200' })
.prop('required', true)
.val(42)
.css('width', '100px')
.wrap('<div class="form-field">')
.parent()
.prepend('<label>Quantity: </label>')
.appendTo('#dynamic-form');
Practical Application: Quiz Question Builder
We'll build a builder for creating quiz questions with optional multiple answers. The builder uses chained methods to configure the question before generating the final HTML.
First, the HTML:
<h1>Data-Driven Quiz</h1>
<form id="quiz-form"></form>
<script src="jquery.js"></script>
<script src="quiz-builder.js"></script>
Next, the CSS:
.quiz-question { margin-bottom: 20px; }
.quiz-options { list-style: none; padding: 0; }
.quiz-option { margin: 8px 0; }
Then, the builder implementation:
(function() {
'use strict';
window.quizBuilder = window.quizBuilder || {};
function QuestionBuilder() {
this.title = 'Untitled Question';
this.options = [];
this.allowMultiple = false;
}
quizBuilder.QuestionBuilder = QuestionBuilder;
QuestionBuilder.prototype.setTitle = function(title) {
this.title = title;
return this;
};
QuestionBuilder.prototype.setAllowMultiple = function(allow) {
this.allowMultiple = allow;
return this;
};
QuestionBuilder.prototype.addOption = function(optionText) {
this.options.push(optionText);
return this;
};
QuestionBuilder.prototype.build = function() {
const $question = $('<article class="quiz-question">');
$('<header>').text(this.title).appendTo($question);
const questionId = 'q_' + (jQuery.guid++);
const $optionsList = $('<ul class="quiz-options">');
this.options.forEach(function(option, index) {
const $input = $('<input>')
.attr({
type: this.allowMultiple ? 'checkbox' : 'radio',
value: index,
name: questionId
});
const $optionItem = $('<li class="quiz-option">');
$('<label>').append($input, $('<span>').text(option)).appendTo($optionItem);
$optionsList.append($optionItem);
}, this);
$question.append($optionsList);
return $question;
};
quizBuilder.questions = [
{
title: 'What is the primary goal of the facade pattern?',
options: [
'To add new functionality to existing classes',
'To abstract complex systems into a simplified interface',
'To enforce strict type checking',
'To optimize database queries'
]
},
{
title: 'Which jQuery method acts as a builder for new DOM elements?',
options: [
'$() with a CSS selector',
'$() with an HTML string',
'$.ajax()',
'$.extend()'
]
},
{
title: 'Which of these are creational patterns? (Select all that apply)',
allowMultiple: true,
options: ['Singleton', 'Observer', 'Factory', 'Decorator']
}
];
quizBuilder.init = function() {
const $form = $('#quiz-form');
quizBuilder.questions.forEach(function(config) {
const builder = new quizBuilder.QuestionBuilder();
builder
.setTitle(config.title)
.setAllowMultiple(config.allowMultiple);
config.options.forEach(function(option) {
builder.addOption(option);
});
$form.append(builder.build());
});
};
$(document).ready(quizBuilder.init);
})();
Asynchronous Control Flow Patterns
Asynchronous programming is core to frontend development, enabling non-blocking operations like AJAX requests, animations, and user interactions.
Callbacks
Callbacks are functions passed as arguments to other functions, to be executed later when an operation completes. They're simple but can lead to "callback hell" with deeply nested code.
Simple Callback Example
function generateRandomAsync(max, callback) {
const delay = 1500 + Math.random() * 1000;
setTimeout(function() {
const result = Math.random() * max;
callback(result);
}, delay);
}
generateRandomAsync(20, function(num) {
console.log('Generated:', num);
});
Sequential Execution with Callbacks
function getThreeNumbers(callback, errorCallback) {
const results = [];
generateRandomAsync(20, function(num1) {
results.push(num1);
generateRandomAsync(20, function(num2) {
results.push(num2);
$.getJSON('random-number-api', function(data) {
results.push(data.value);
callback(results);
}).fail(errorCallback);
});
});
}
Avoiding Callback Hell
To flatten nested callbacks, use named functions:
function getThreeNumbers(callback, errorCallback) {
const results = [];
generateRandomAsync(20, function(num1) {
results.push(num1);
fetchSecondNumber();
});
function fetchSecondNumber() {
generateRandomAsync(20, function(num2) {
results.push(num2);
fetchThirdNumber();
});
}
function fetchThirdNumber() {
$.getJSON('random-number-api', function(data) {
results.push(data.value);
callback(results);
}).fail(errorCallback);
}
}
Parallel Execution with Callbacks
function getNumbersParallel(callback, errorCallback) {
const results = [];
let completed = 0;
const total = 3;
function collectResult(index) {
return function(result) {
results[index] = result;
completed++;
if (completed === total) callback(results);
};
}
generateRandomAsync(20, collectResult(0));
generateRandomAsync(20, collectResult(1));
$.getJSON('random-number-api', collectResult(2)).fail(errorCallback);
}
Promises
Promises provide a more structured way to handle asynchronous operations, representing a value that may be available now, later, or never. They support chaining and parallel execution, eliminating callback hell.
jQuery Deferred and Promise
jQuery's $.Deferred() creates a deferred object, which can be resolved or rejected. The promise() method returns a read-only promise object to consumers:
function generateRandomPromise(max) {
const deferred = $.Deferred();
const delay = 1500 + Math.random() * 1000;
setTimeout(function() {
const result = Math.random() * max;
deferred.resolve(result);
}, delay);
return deferred.promise();
}
generateRandomPromise(20).then(function(num) {
console.log('Generated:', num);
});
ES6 Promises
ES6 native promises follow the Promises/A+ specification, with a simpler constructor:
function generateRandomES6(max) {
return new Promise(function(resolve, reject) {
const delay = 1500 + Math.random() * 1000;
setTimeout(function() {
const result = Math.random() * max;
resolve(result);
}, delay);
});
}
generateRandomES6(20).then(function(num) {
console.log('Generated:', num);
});
Chaining Promises
Promises can be chained to execute operations sequentially:
generateRandomPromise(20)
.then(function(num) {
console.log('First:', num);
return num * 2;
})
.then(function(doubled) {
console.log('Doubled:', doubled);
return generateRandomPromise(doubled);
})
.then(function(secondNum) {
console.log('Second:', secondNum);
});
Parallel Execution with Promises
jQuery's $.when() and ES6's Promise.all() combine multiple promises into one:
// jQuery
$.when(
generateRandomPromise(20),
generateRandomPromise(30),
$.getJSON('random-number-api')
).then(function(num1, num2, apiData) {
console.log('All results:', num1, num2, apiData.value);
});
// ES6
Promise.all([
generateRandomES6(20),
generateRandomES6(30),
fetch('random-number-api').then(res => res.json())
]).then(function(results) {
console.log('All results:', results[0], results[1], results[2].value);
});
Mock Object Pattern
Mock objects simulate the behavior of real, complex objects, enabling parallel development, testing, and decoupling from external dependencies.
Key Use Cases
- Developing modules that depend on unimplemented components
- Testing edge cases (e.g., network errors, rare events)
- Decoupling frontend development from backend APIs
Practical Application: Video Service Mock
We'll build a mock video service for a dashboard that displays YouTube videos, decoupling frontend development from the real YouTube API.
First, the HTML:
<section class="dashboard-categories">
<select id="category-selector"></select>
<div class="category-buttons"></div>
<div class="clear"></div>
</section>
<div class="video-boxes"></div>
<script src="jquery.js"></script>
<script src="video-service-mock.js"></script>
<script src="dashboard.js"></script>
Next, the mock video service:
(function() {
'use strict';
window.videoService = window.videoService || {};
window.videoService.mockData = window.videoService.mockData || {};
// Mock data
videoService.mockData.searches = [
{
keywords: 'jquery conference',
data: {
items: [
{
id: { videoId: 'abc123' },
snippet: {
title: 'jQuery Conference 2023: Modern Frontend Patterns',
thumbnails: {
default: { url: 'https://example.com/thumb1.jpg' },
medium: { url: 'https://example.com/thumb1-m.jpg' }
}
}
},
{
id: { videoId: 'def456' },
snippet: {
title: 'jQuery UI Deep Dive',
thumbnails: {
default: { url: 'https://example.com/thumb2.jpg' },
medium: { url: 'https://example.com/thumb2-m.jpg' }
}
}
}
]
}
},
{
keywords: 'javascript design patterns',
data: {
items: [
{
id: { videoId: 'ghi789' },
snippet: {
title: 'Design Patterns for JavaScript Developers',
thumbnails: {
default: { url: 'https://example.com/thumb3.jpg' },
medium: { url: 'https://example.com/thumb3-m.jpg' }
}
}
}
]
}
}
];
// Build all videos array for single-video lookups
const allVideos = [];
videoService.mockData.searches.forEach(function(search) {
allVideos.push.apply(allVideos, search.data.items);
});
videoService.mockData.allVideos = allVideos;
// Mock service methods
videoService.searchVideos = function(keywords) {
return $.Deferred(function(deferred) {
const searches = videoService.mockData.searches;
for (let i = 0; i < searches.length; i++) {
if (searches[i].keywords.toLowerCase() === keywords.toLowerCase()) {
deferred.resolve(searches[i].data);
return;
}
}
deferred.reject('No results found');
}).promise();
};
videoService.getVideo = function(title) {
return $.Deferred(function(deferred) {
const videos = videoService.mockData.allVideos;
for (let i = 0; i < videos.length; i++) {
if (videos[i].snippet.title === title) {
deferred.resolve(videos[i]);
return;
}
}
deferred.reject('Video not found');
}).promise();
};
const videoBaseUrl = 'https://www.youtube.com/watch?v=';
videoService.getVideoUrl = function(videoId) {
return videoBaseUrl + videoId;
};
})();
Then, the dashboard module:
(function() {
'use strict';
window.videoDashboard = window.videoDashboard || {};
videoDashboard.categories = [
{ title: 'jQuery Conferences', keywords: 'jquery conference' },
{ title: 'JS Design Patterns', keywords: 'javascript design patterns' }
];
videoDashboard.initCategories = function() {
const $selector = $('#category-selector');
const $buttons = $('.category-buttons');
videoDashboard.categories.forEach(function(category, index) {
$('<option>').val(index).text(category.title).appendTo($selector);
});
$selector.on('change', function() {
const index = $(this).val();
const category = videoDashboard.categories[index];
videoDashboard.loadVideos(category.keywords);
});
// Load initial videos
videoDashboard.loadVideos(videoDashboard.categories[0].keywords);
};
videoDashboard.loadVideos = function(keywords) {
const $boxes = $('.video-boxes').empty();
videoService.searchVideos(keywords).then(function(data) {
data.items.forEach(function(video) {
videoDashboard.openVideoBox(video.snippet.title);
});
}).fail(function(error) {
$boxes.text('Error loading videos: ' + error);
});
};
videoDashboard.openVideoBox = function(title) {
const $box = $('<div class="video-box">')
.append($('<header>').text(title).append($('<button class="close-btn">×</button>')))
.append($('<div class="content">Loading...</div>'))
.appendTo('.video-boxes');
videoService.getVideo(title).then(function(video) {
const $link = $('<a>').attr('href', videoService.getVideoUrl(video.id.videoId));
$link.append($('<img>').attr('src', video.snippet.thumbnails.medium.url));
$box.find('.content').empty().append($link);
}).fail(function() {
$box.find('.content').text('Error loading video');
});
$box.find('.close-btn').on('click', function() {
$box.remove();
});
};
$(document).ready(videoDashboard.initCategories);
})();
Client-Side Templating
Client-side templating libraries generate HTML dynamically from data and templates, separating presentation from logic and reducing string concatenation.
Underscore.js Templates
Underscore.js provides _.template(), which uses <% %> for execution, <%= %> for interpolation, and <%- %> for HTML-escaped interpolation.
Basic Example
const templateFn = _.template('<h1><%- title %></h1>');
const html = templateFn({ title: 'Underscore.js Template Example' });
Iteration with Underscore
const templateSource = '<% _.each(categories, function(cat, i) { %>' +
'<option value="<%= i %>"><%- cat.title %></option>' +
'<% }); %>';
const optionsHtml = _.template(templateSource)({ categories: videoDashboard.categories });
$('#category-selector').append(optionsHtml);
Embedded Templates in HTML
Store templates in <script type="text/template"> tags to separate them from JavaScript:
<script id="video-box-template" type="text/template">
<div class="video-box">
<header>
<button class="close-btn">×</button>
<%- title %>
</header>
<div class="content">Loading...</div>
</div>
</script>
Create a template provider with caching:
const templateCache = {};
function getTemplate(name) {
if (templateCache[name]) return templateCache[name];
const template = $('#' + name).html();
templateCache[name] = _.template(template);
return templateCache[name];
}
videoDashboard.openVideoBox = function(title) {
const template = getTemplate('video-box-template');
const $box = $(template({ title: title })).appendTo('.video-boxes');
// Rest of implementation...
};
Async Template Loading
Load templates via AJAX for better scalability:
const templateCache = {};
function getTemplateAsync(name) {
if (templateCache[name]) return $.Deferred().resolve(templateCache[name]).promise();
return $.ajax({
url: name + '.html',
mimeType: 'text/html'
}).then(function(template) {
templateCache[name] = _.template(template);
return templateCache[name];
});
}
videoDashboard.openVideoBox = function(title) {
getTemplateAsync('video-box').then(function(template) {
const $box = $(template({ title: title })).appendTo('.video-boxes');
// Rest of implementation...
});
};
Handlebars.js Templates
Handlebars.js provides logic-less templates with {{ }} for interpolation (HTML-escaped by default), {{{ }}} for unescaped interpolation, and built-in helpers like {{#each}} and {{#if}}.
Basic Example
const templateFn = Handlebars.compile('<h1>{{ title }}</h1>');
const html = templateFn({ title: 'Handlebars.js Template Example' });
Iteration with Handlebars
const templateSource = '{{#each categories}}' +
'<option value="{{@index}}">{{ title }}</option>' +
'{{/each}}';
const optionsHtml = Handlebars.compile(templateSource)({ categories: videoDashboard.categories });
$('#category-selector').append(optionsHtml);
Embedded Templates in HTML
Store templates in <script type="text/x-handlebars-template"> tags:
<script id="video-box-template" type="text/x-handlebars-template">
<div class="video-box">
<header>
<button class="close-btn">×</button>
{{ title }}
</header>
<div class="content">Loading...</div>
</div>
</script>
Create a template provider with caching:
const templateCache = {};
function getTemplate(name) {
if (templateCache[name]) return templateCache[name];
const template = $('#' + name).html();
templateCache[name] = Handlebars.compile(template);
return templateCache[name];
}
Precompiled Templates
Precompile Handlebars templates for better performance. Install Handlebars via npm, then run:
handlebars video-box-template.handlebars -f video-box-template.js
Use the precompiled template:
function getTemplate(name) {
return Handlebars.templates[name];
}