This is the first installment in the SOLID JavaScript series which will explore the SOLID design principles within the context of the JavaScript language.  In this first installment, we’ll take a look at what principles comprise the SOLID design principles and discuss the first of these principles: The Single Responsibility Principle.

 

The SOLID Design Principles and JavaScript

SOLID is a mnemonic acronym referring to a collection of design principles which evolved out of the objected-oriented community and were popularized by the writings of Robert C. Martin.    These principles are as follows:

  • The Single Responsibility Principle
  • The Open/Closed Principle
  • The Liskov Substitution Principle
  • The Interface Segregation Principle
  • The Dependency Inversion Principle

These principles are often discussed within the context of classical, statically typed, objected-oriented languages, and while JavaScript is a prototype-based, dynamically typed language blending concepts from both object-oriented and functional paradigms, programmers can still reap the benefits from the application of these principles to JavaScript.  This article will cover the first of these principles: The Single Responsibility Principle.

The Single Responsibility Principle

The Single Responsibility Principle relates to the functional relatedness of the elements of a module.  The principle states:

A class should have only one reason to change

This description can be a little misleading as it would seem to suggest that an object should only do one thing. What is meant by this assertion, however, is that an object should have a cohesive set of behaviors, together comprising a single responsibility that, if changed, would require the modification of the object’s definition.  More simply, an object’s definition should only have to be modified due to changes to a single responsibility within the system.

Adherence to the Single Responsibility Principle helps to improve maintainability by limiting the responsibilities of an object to those which change for related reasons.  When an object encapsulates multiple responsibilities, changes to the object for one of the responsibilities can negatively impact the others.  By decoupling such responsibilities, we can create code that is more resilient to change.

But how do we identify whether a given set of behaviors constitutes a single responsibility?  Would grouping all string manipulation into a single object be a single responsibility?  How about grouping together all the service calls made within an application?  Without an established approach to making these determinations, adhering to the Single Responsibility can be a bit perplexing.

Object Role Stereotypes

One approach which can aid in the organization of behavior within a system is the use of Object Role Stereotypes.  Object Role Stereotypes are a set of general, pre-established roles which commonly occur across object-oriented architectures.  By establishing a set of role stereotypes, developers can provide themselves with a set of templates which they can use as they go through the mental exercise of decomposing behavior into cohesive components.

The concept of Object Role Stereotypes is discussed in the book Object Design: Roles, Responsibilies, and Collaborations by Rebecca Wirfs-Brock and Alan McKean.  The book presents the following stereotypes::

  • Information holder – an object designed to know certain information and provide that information to other objects.
  • Structurer – an object that maintains relationships between objects and information about those relationships.
  • Service provider – an object that performs specific work and offers services to others on demand.
  • Controller – an object designed to make decisions and control a complex task.
  • Coordinator – an object that doesn’t make many decisions but, in a rote or mechanical way, delegates work to other objects.
  • Interfacer – an object that transforms information or requests between distinct parts of a system.

While not prescriptive, this set of role stereotypes provides an excellent mental framework for aiding in the software design process.  Once you have an established  set of role stereotypes to work within, you’ll find it easier to group behaviors into cohesive groups of responsibilities related to the object’s intended role.

Single Responsibility Principle Example

To illustrate the application of the Single Responsibility Principle, let’s consider the following example JavsScript code which facilitates the movement of product items into a shopping cart:

function Product(id, description) {
    this.getId = function() {
        return id;
    };
    this.getDescription = function() {
        return description;
    };
}

function Cart(eventAggregator) {
    var items = [];

    this.addItem = function(item) {
        items.push(item);
    };
}

var products = [
    new Product(1, "Star Wars Lego Ship"),
    new Product(2, "Barbie Doll"),
    new Product(3, "Remote Control Airplane")],
    cart = new Cart();

(function() {
    function addToCart() {
        var productId = $(this).attr('id');
        var product = $.grep(products, function(x) {
            return x.getId() == productId;
        })[0];
        cart.addItem(product);

        var newItem = $('<li></li>')
            .html(product.getDescription())
            .attr('id-cart', product.getId())
            .appendTo("#cart");
    }

    products.forEach(function(product) {
        var newItem = $('<li></li>')
            .html(product.getDescription())
            .attr('id', product.getId())
            .dblclick(addToCart)
            .appendTo("#products");
    });
})();
/pre>

While not overly complex, this example illustrates a number of unrelated responsibilities which are grouped together within a single anonymous function. Let’s consider each responsibility:

First, we have behavior defined to handle populating the Cart model when an item is double-clicked.

Second, we have behavior defined to add items to the cart view when an item is double-clicked.

Third, we have behavior defined to populate the products view with the initial set of products.

Let’s break these three responsibilities out into their own objects:

 

function Event(name) {
    this._handlers = [];
    this.name = name;
}
Event.prototype.addHandler = function(handler) {
    this._handlers.push(handler);
};
Event.prototype.removeHandler = function(handler) {
    for (var i = 0; i < handlers.length; i++) {
        if (this._handlers[i] == handler) {
            this._handlers.splice(i, 1);
            break;
        }
    }
};
Event.prototype.fire = function(eventArgs) {
    this._handlers.forEach(function(h) {
        h(eventArgs);
    });
};

var eventAggregator = (function() {
    var events = [];

    function getEvent(eventName) {
        return $.grep(events, function(event) {
            return event.name === eventName;
        })[0];
    }

    return {
        publish: function(eventName, eventArgs) {
            var event = getEvent(eventName);

            if (!event) {
                event = new Event(eventName);
                events.push(event);
            }
            event.fire(eventArgs);
        },

        subscribe: function(eventName, handler) {
            var event = getEvent(eventName);

            if (!event) {
                event = new Event(eventName);
                events.push(event);
            }

            event.addHandler(handler);
        }
    };
})();

function Cart() {
    var items = [];

    this.addItem = function(item) {
        items.push(item);
        eventAggregator.publish("itemAdded", item);
    };
}

var cartView = (function() {
    eventAggregator.subscribe("itemAdded", function(eventArgs) {
        var newItem = $('<li></li>')
            .html(eventArgs.getDescription())
            .attr('id-cart', eventArgs.getId())
            .appendTo("#cart");
    });
})();

var cartController = (function(cart) {
    eventAggregator.subscribe("productSelected", function(eventArgs) {
        cart.addItem(eventArgs.product);
    });
})(new Cart());

function Product(id, description) {
    this.getId = function() {
        return id;
    };
    this.getDescription = function() {
        return description;
    };
}

var products = [
    new Product(1, "Star Wars Lego Ship"),
    new Product(2, "Barbie Doll"),
    new Product(3, "Remote Control Airplane")];

var productView = (function() {
    function onProductSelected() {
        var productId = $(this).attr('id');
        var product = $.grep(products, function(x) {
            return x.getId() == productId;
        })[0];
        eventAggregator.publish("productSelected", {
            product: product
        });
    }

    products.forEach(function(product) {
        var newItem = $('<li></li>')
            .html(product.getDescription())
            .attr('id', product.getId())
            .dblclick(onProductSelected)
            .appendTo("#products");
    });
})();

In our revised design, we’ve removed our anonymous function and replace it with objects to coordinate each of the separate set of responsibilities.  A cartView was introduced to coordinate the population of the cart display, a cartController was introduced to coordinate the population of the cart model, and a productView was introduced to coordinate the population of the products display.  We also introduced an Event Aggregator to facilitate communication between the objects in a loosely-coupled way.  While this design results in a larger number of objects, each object now focuses on fulfilling a specific role within the overall orchestration with minimal coupling between the objects.

Next time, we’ll discuss the next principle in the SOLID acronym: The Open/Closed Principle.

Tagged with:  
  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1000

  • http://developtodesign.com Rob Levin

    Hi Derek. Enjoyed your article on SOLID principles, and using in JavaScript. I looked at the two examples and, on the one hand, I could see that in the second example, the objects are clearly more “cohesive”; however, the second example (unless you skip right to the bottom where objects are being constructed) is much longer and more complex. Obviously, this design leans heavily on PubSub/Observer (whatever you prefer to call it). I got that but I’d suggest you discuss this pattern being used before introducing the second example.

    Also, just playing devils advocate, there is the issue of over engineering and I think folks need to be mindful of how likely it is that a piece of code is going to grow to need all of these components. Sometimes ‘yagni’. It’s sort of like preoptimization but from a design perspective. I generally code things up quite procedurally at first; this helps me to understand the nuts and bolts of the problem. Then I go back (immediately) and decide whether a refactor is needed. So I’d love to hear your process from going from first draft to SOLID compliant modules .. perhaps food for thought for subsequent articles?

    Thanks again for the thoughtful article.

    • Anonymous

      Thanks for the feedback, Rob.  Concerning the topic of complexity, I touched on this topic toward the end of the article, though I decided not to go into depth in order to keep the article brief and on topic.  This is also the reason I chose to avoid introducing any 3rd party libraries beyond JQuery and as well as foregoing any explanation of the Event Aggregator pattern.

      Concerning what heuristic I would recommend for knowing when these principles should be applied, I’ll refer you to my Effective Tests series over at Los Techies wherein I go into depth on this topic.  In brief, Test-Driven Development offers an excellent heuristic for guiding the introduction of this level of complexity.

      For the purposes of this series, my plan is to keep the examples fairly simple in order to keep the focus on demonstrating the principles.

  • http://twitter.com/rubikzube rubikzube

    After spending some time thinking about this excellent example, I am a bit uncertain of the value provided by separate Cart and Product Repository objects at this stage of the code’s evolution.  

    I think that a case could be made for the Product as an unit of data that can be passed between the CartController and the ProductController via the EventAggregator without the data storage objects… but I look forward to the next article to see how the code evolves next.Great job.

    • Anonymous

      Thanks for the feedback, Zubin.  When applying these principles, It’s good to be mindful of the trade offs between the application of the Single Responsibility Principle and the effort involved in decomposing responsibilities into discrete components.  When considering this example, however, keep in mind that the purpose of this article is to illustrate the SRP within the context of the JavaScript language.  To do this effectively, I’ve tried to strike a balance between demonstrating the principle and keeping the example simple.  Reactions invariably come down on both sides, with some pointing out there are concerns yet to be factored out and others pointing out the example isn’t complex enough to warrant any decomposition.  Both sides have valid points depending upon the context.

  • http://twitter.com/SubliminalEd Ed Castaneda

    I loved this.  I don’t think the second example is more complex.  There is more code and structure yes but once you get used to that, it’s easier to focus on specifics versus the first example.  I think what might help is namespace/folder structure?

  • http://twitter.com/codingobviously Alexander

    The second example increases the amount of code, but in the first example things are actually harder to find.

    A quick look at the anonymous function in the second example and you already know who’s doing what. That’s why breaking responsibilities into their own little modules is worth it.

  • http://diminishing.org Michael Guterl

    It seems to me that ProductsController is overloaded with responsibility that could belong to the ProductRepository, specifically finding the product by id.  What do you think?

    • Anonymous

      Good observation, Michael.  Indeed, the logic for retrieving a single product from the full set isn’t an inherent responsibility of a controller.  In fact, there’s actually two responsibilities represented here: specification and caching.  In a real application involving a greater level of complexity, you’d likely want to factor out both of these concerns.

  • Pingback: JavaScript Weekly #57 | island205

  • http://www.facebook.com/willcatcher Vlad Nicula

    I’ve implemented something similar for an application with 3 different layouts performing the same task with Ajax. 

    Each “stage” as I called it, registered some callbacks to a global object – I called it “AppCore” -. AppCore handled the triggering, binding and unbinding of callbacks. 

    This way, an action in one of the stages would be propagated to all others :) … however, AppCore contained other functionality, so it was not respecting the single responsibility principle.

    Great article Derek, now let me ask you a question. How would you go about designing a set of UI components respecting this principle? I might have a Hide-enabled object, a Lockable object, a Clickable object and a Draggable object. A button would be a Hide-enabled, Lockable and Clickable object while a picture frame would be a Hide-enabled, Clickable Draggable object. I’m thinking about mixins but I never understood them.

  • Pingback: SOLID JavaScript: The Interface Segregation Principle

  • Lroal

    I dont like the eventaggregator cause its a global singleton relying on magic strings. I would rather rename the Cart to CartPresenter and CartController to CartView, then let the CartPresenter call CartView.addItem(). Same principle for Product. Just plain old model view presenter.