EPAM Anywhere: "Less popular" JavaScript Design Patterns
Technology / 4 min

"Less popular" JavaScript Design Patterns

Javascript_design_patterns.pngJavascript_design_patterns.png

As software engineers, we strive to write maintainable, reusable, and eloquent code that might live forever in large applications. The code we create must solve real problems. We are certainly not trying to create redundant, unnecessary, or “just for fun” code. At the same time, we frequently face problems that already have well-known solutions that have been defined and discussed by the Global community or even by our own teams millions of times. Those solutions for such problems are called “Design patterns”.

Design patterns in JavaScript

There are a number of existing design patterns in software design, some of them are used more often, some of them less frequently. Examples of popular JavaScript design patterns include factory, singleton, strategy, decorator, and observer patterns. In this article, we’re not going to cover all of the design patterns in JavaScript. Instead, let’s consider some of the less well-known but potentially useful JS patterns such as command, builder, and special case, as well as real examples from our production experience.

Command pattern

Just imagine there’s a TV in your flat, and you have a remote control for it. When you (“client”) press any button (for instance, “Volume Up”) on the remote control (“invoker”), it sends a signal to your TV (“receiver”) and the command executes with a particular result. That’s basically how command pattern is used in action.

In our real-world case, this design pattern in JavaScript was useful for processing messages from a message broker. It helped considerably with decoupling the execution logic for each message, and made it easier to add new commands if necessary.

The implementation was approximately the following:

class Command { 
    execute() {} 
} 
 
class VolumeUpCommand extends Command { 
    execute() { 
        console.log('volume up'); 
    } 
} 
 
class VolumeDownCommand extends Command { 
    execute() { 
        console.log('volume down'); 
    } 
} 
 
const Messages = { 
    VOLUME_UP: 'volumeUp', 
    VOLUME_DOWN: 'volumeDown', 
} 
 
const Commands = { 
    [Messages.VOLUME_UP]: VolumeUpCommand, 
    [Messages.VOLUME_DOWN]: VolumeDownCommand, 
}; 
 
class Executor { 
    handleMessage(payload) { 
        const { type } = payload; 
 
        if (!Commands[type]) { 
            console.warn('unknown command'); 
            return; 
        } 
 
        const command = new Commands[type](); 
        command.execute(payload); 
    } 
} 
 
const executor = new Executor(); 
 
executor.handleMessage({ type: 'volumeUp' }); 
executor.handleMessage({ type: 'volumeDown' }); 
executor.handleMessage({ type: 'unknown' });

In some implementations, command pattern might include a rollback method and journaling of each executed command.

Builder pattern

Have you ever ordered something from McDonald’s? Generally speaking, after you place a complex order, your order will be built from several components, for instance, a drink, a burger, and French fries, step-by-step. You don’t know how it was built or cooked, you just wait for your ready-to-go meal. Similarly, a builder encapsulates an entire process.

The real-case scenario was building a report. The report had several major components, from various sources, and after all the data was built, the report was to be sent to a reporting service.

The high-level implementation of the described JS pattern:

class Report { 
    constructor(consumer, products, preferences) { 
        this.consumer = consumer; 
        this.products = products; 
        this.preferences = preferences; 
    } 
} 
 
class ReportBuilder { 
    constructor() { 
        this.consumer = null; 
        this.products = null; 
        this.preferences = null; 
    } 
 
    setConsumer(consumer) { 
        this.consumer = consumer; 
    } 
 
    setProducts(products) { 
        this.products = products; 
    } 
 
    setPreferences(preferences) { 
        this.preferences = preferences; 
    } 
 
    build() { 
        return new Report(this.consumer, this.products, this.preferences); 
    } 
} 
 
const builder = new ReportBuilder(); 
 
builder.setConsumer({ name: 'Joe' }); 
builder.setProducts([{ product: 'French fries' }, { product: 'Burger' }]); 
builder.setPreferences({ value: 'no ketchup' }); 
 
console.log(builder.build());


This pattern helped achieve a clear separation between the construction and representation of an object, and better control over the construction process itself.

Special case pattern

From time to time, while defining classes and objects that will handle some data, we do not want to change the behavior of the code if something goes wrong. Instead, we will manage that unique situation safely to avoid immediate failure or error. This is when Martin Fowler’s Special case JavaScript pattern comes in.

The Special Case pattern is a refinement of the strategy pattern. Generally speaking, it’s a default strategy with the same interface as it’s siblings and is what the caller expects.

As an example, we had several types of content on our application, some of them should have been processed, some of them not. The processor was defined by the strategy based on content type and, by default, it returned a nullable/default processor.

Let’s have a look at a code sample of this JavaScript pattern 

class HTMLProcessor
    parse() { 
        console.log('parsing html'); 
    } 
} 
 
class XMLProcessor { 
    parse() { 
        console.log('parsing xml'); 
    } 
} 
 
class DefaultProcessor { 
    parse() { 
        console.log('content type is not supported'); 
    } 
} 
 
const SupportedContentTypes = { 
    HTML: 'html', 
    XML: 'xml' 
} 
 
function getProcessor(contentType) { 
    switch(contentType) { 
        case SupportedContentTypes.HTML: 
            return new HTMLProcessor(); 
        case SupportedContentTypes.XML: 
            return new XMLProcessor(); 
        default: 
            return new DefaultProcessor(); 
    } 
} 
 
const processor = getProcessor('html'); 
console.log(processor.parse()) 
 
const processor1 = getProcessor('txt'); 
console.log(processor1.parse())


The pattern helped us not be afraid of getting the wrong input, and enabled us to use the processor’s public interface safely without any kind of null exceptions or unnecessary checks.

Closing thoughts

Plenty of JavaScript design patterns are well-defined and explained in articles like this one. However, please avoid any tendency toward overuse. As they say, “If all you have is a hammer, everything looks like a nail.” You don’t want to fall into the trap of overuse, so use these JS patterns only when necessary, and when it might solve a real problem in your code.

Thanks for reading!

Written by

Rufat.jpgRufat.jpg

Rufat KhaslarovChief Software Engineer I

I'm a Software Engineer with 10 years of commercial experience! Starting from the very first work day, I'm really into simplifying the complex, effective troubleshooting, failing fast, and learning from each small task. Therefore, I've played various roles as a system back-end engineer, system administrator, penetration tester, and finally a full-stack JS engineer. At the same time, I'm really keen on product and people management, I'm an engineering manager and technical product manager as well.

Are you searching for a remote job in IT?

We're inviting Software Developers, QA Engineers, DevOps Specialists, Business Analysts, Designers, Data Analysts, and other IT specialists to join our community and work from the comfort of your home. 

View jobs