Friday, June 11, 2010

Switching to the Strategy Pattern in JavaScript

A Typical Switch Statement Scenario

Recently I’ve been working on some highly dynamic User Interfaces and at one point in the project I found my first reflex on a certain task to use a switch statement in JavaScript.

Now, my previous training in studying the Design Patterns has told me that the switch statement is bad. The design pattern drilled into me to resolve the switch statement is the the Strategy Pattern, however, I’ve never used the Strategy Pattern in JavaScript before.

The following example is a simple demo application where I start with a switch statement and then refactor the code later in the blog post to use the Strategy Pattern.

A Simple Super Hero Demo Application

SuperHeroShadow

The sample application is a Super Hero creator. You provide a Super Hero name and then you select your Super Hero power. The power can be a variety of things ranging from Flying, Invisibility, to nothing at all. Some of the powers have additional questions (metadata) about that particular power.

Once the Super Hero is created, the application should build up a object with the appropriate power type and supporting metadata. For now, I’m just outputting the object in the Firebug Lite console using console.dir().

First Pass at a Switch Statement Solution

Here is the above application written using a switch statement to build up the Power type and metadata. The solution is fairly simple in nature. I am utilizing the Revealing Module Pattern.

Note: You can view, run, and edit the following code… http://jsfiddle.net/elijahmanor/KzAMX/

var superHeroModule = (function () {
    var public = {};
    
    public.init = function() {
        $("input:radio[name='Power']").click(function() {
            var selectedPower = this.value;
            
            $("#powers .power").hide();
            $("#powers ." + selectedPower).show();
        });
        
        $("#create").click(function() {
            var superHero = public.scrapeSuperHero();
            console.dir(superHero);
        });
    };
    
    public.scrapeSuperHero = function() {
        var superHero = {};
        
        superHero.Name = $("#Name").val();
        superHero.Power = public.scrapePower();
        
        return superHero;
    };
    
    public.scrapePower = function() {
        var power = {};

        var selectedPower = $("input:radio[name='Power']:checked").val();
        switch (selectedPower) {
            case "Flying" : 
                power.type = "Flying";
                power.speed = $("#flyingSpeed").val();
                break;
            case "Invisibility" :
                power.type = "Invisibility";
                break;
            case "Strength" :
                power.type = "Strength";
                power.lift = $("#strengthLift").val();
                power.strongerThan = $("#strengthStrongerThan").val();
                break;
            case "Vision" : 
                power.type = "Vision";
                power.distance = $("#visionDistance").val();
                power.seeInDark = $("#visionDark").is(":checked");
                power.xrayVision = $("#visionXray").is(":checked");
                break;
        }
                
        return power;
    };
    
    return public;
} ());

superHeroModule.init();

cooltext439925164

Pros of this Solution

  • Simple Implementation
  • All the code is in one place to handle building up the Super Hero power metadata

Cons of this Solution

For those of you aware of Bob Martin’s (@unclebobmartin) SOLID Principles, the “O” represents the Open/Closed Principle which states that…

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

The switch solution violates the Open/Closed Principle in that every time a new Super Hero power is added to the list that the same piece of code will need to be modified. This is problematic in that new bugs can easily be introduced as new features are added or as defects are resolved.

Let’s look at a better way to solve the solution.

Refactoring Using the Strategy Pattern

A lof of the code will remain the same in the refactored solution. We mainly want to pull out the switch statement into something that is more maintainable and less error prone for future enhancment.

Note: You can view, run, and edit the following code… http://jsfiddle.net/elijahmanor/K8CkZ/

var superHeroModule = (function () {
    var public = {};
    
    public.init = function() {
        $("input:radio[name='Power']").click(function() {
            var selectedPower = this.value;
            
            $("#powers .power").hide();
            $("#powers ." + selectedPower).show();
        });
        
        $("#create").click(function() {
            var superHero = public.scrapeSuperHero();
            console.dir(superHero);
        });
    };
    
    public.scrapeSuperHero = function() {
        var superHero = {};
        
        superHero.Name = $("#Name").val();
        superHero.Power = public.scrapePower();
        
        return superHero;
    };
    
    public.scrapePower = function() {
        var power = {};
        
        var selectedPower = $("input:radio[name='Power']:checked").val();
        var scrapePowerFunction = "public.scrapePower" + selectedPower;
        power = eval(scrapePowerFunction)();
        
        return power;
    };
    
    public.scrapePowerFlying = function() {
        var power = {};
        
        power.type = "Flying";
        power.speed = $("#flyingSpeed").val();
        
        return power;
    };
    
    public.scrapePowerInvisibility = function() {
        var power = {};
        
        power.type = "Invisibility";
        
        return power;
    };
    
    public.scrapePowerStrength = function() {
        var power = {};

        power.type = "Strength";
        power.lift = $("#strengthLift").val();
        power.strongerThan = $("#strengthStrongerThan").val();        
        
        return power;    
    };
    
    public.scrapePowerVision = function() {
        var power = {};
        
        power.type = "Vision";
        power.distance = $("#visionDistance").val();
        power.seeInDark = $("#visionDark").is(":checked");
        power.xrayVision = $("#visionXray").is(":checked");
        
        return power;
    };
    
    return public;
} ());

superHeroModule.init();

cooltext439925164

Pros of this Solution

  • Abides by the Open/Closed Principle by opening the code for extension, but closing it for modification.

Cons of this Solution

  • The solution is more complex than the previous switch statement implementation
  • You might have noticed the use of the eval function. Douglas Crockford labeled the eval function as a Bad Part of JavaScript for some very good reasons, however, in this case I think the benefits it brings in this situation outweighs the risks. With every hard and fast rule, there comes a place and time to examine the benefits and risks.

Conclusion

I found that separating the switch statement logic into the above Strategy Pattern has made my current project much more maintainable and less error prone when adding new features.

How have you addressed similar situation? Have you implemented a different solution to the switch statement problem? Please share with me, I’d love to know!

No comments:

Post a Comment