Yianna Kokalas

Design Patterns: Strategy

07 Jan 2017

The strategy pattern is made up of a couple of idioms, “Composition over inheritence” and “Promises”. The strategy pattern can be used alone or part of a bigger pattern. It’s good for making code modular and reuseable without the use of inheritence. There are pitfalls to using inheritence all the time when sharing behavior such as the diamond problem. With inheritence the child class can inherit behavior that it did not intend on inheriting. If you forget to override it, then you will get an unexpected result. The strategy pattern will enforce some diversity in behavior that can be changed at runtime with no unexpected side affects.

class Musician
  def dance
    put "does a dance"
  end
end

class Singer < Musician
end

class Drummer < Musician
end

Now both the Singer and Drummer can dance because they inherit the dance behavior from the parent class of Musician. What happens when a person can’t or shouldn’t dance for this song? We could use polymorphism and override this behavior.

class Musician
  def dance
    put "does a dance"
  end
end

class Singer < Musician
end

class Drummer < Musician
  def dance
    put "can't dance, won't dance"
  end
end

We have now overidden dance but as developers we are left with the task of remembering if a Drummer could or couldn’t dance. Sometimes, we may want a drummer to be able to dance. The solution becomes to make another class and override the method in there. That doesn’t sound very DRY. There’s an easier way to share common behavior and declare which behavior an instantiated child object can use. To solve this problem we are going to use composition over inheritence. An example of composition over inheritence would be if instead of inheriting behavior we chose to compose behavior from an object. So we can encapsulate specific behavior in a class, instantiate it and use the method dance, e.g., CanDanceBehavior.new.dance will allow us to dance. Best of all, we can set the dance behavior at instantiation and at runtime. Let’s start with a default namespace for our dance behavior called DanceBehavior.

class Musician
  attr_accessor :dance_behavior

  def initialize(dance_behavior=nil)
    @dance_behavior = dance_behavior || DanceBehavior.new
  end

  def dance
    dance_behavior.dance
  end
end

class Singer < Musician
  def introduce
    puts "I am a singer!"
  end
end

class Drummer < Musician
  def introduce
    puts "I am a drummer!"
  end
end

Now, let’s define a parent class DanceBehavior which will make sure all modules that belong to the family of dance behavior promise to implement a dance method. This way, we can use any type of dance behavior on any musician at any given time without unwanted side affects. Our DanceBehavior class is a parent class for all of our dance behavior and it’s responsibility is to ensure all children objects promise to implement specified methods by raising a NotImplementedError error. It’s also a good idea to rescue this error so we don’t stop execution of our program.

class DanceBehavior
  def dance
    error_message = "\nERROR: Missing behavior for DanceBehavior.perform_dance"
    begin
      raise NotImplementedError, error_message
    rescue NotImplementedError => error
      puts error.message
      puts error.backtrace.join("\n")
    end
  end
end

class CanDanceBehavior < DanceBehavior
  def dance
    puts "::does a dance::"
  end
end

class CannotDanceBehavior < DanceBehavior
  def dance
    puts "Can't dance won't dance"
  end
end

If we run the following code we will get a NotImplemented error message for a drummer and after setting the behavior the error goes away.

drummer = Drummer.new
drummer.dance
drummer.dance_behavior = CannotDanceBehavior.new
drummer.introduce
drummer.dance

singer = Singer.new(CanDanceBehavior.new)
singer.introduce
singer.dance
ERROR: Missing behavior for DanceBehavior.dance
example.rb:5:in `dance'
example.rb:37:in `dance'
example.rb:54:in `<main>'
I am a drummer!
Can't dance won't dance
I am a singer!
::does a dance::

By using a little bit of inheritence, composition and promises. We have created modularity and continuity across different types of objects that all do the same thing but in different ways, or in another project as a Gem.

If you find yourself creating extra classes to encapsulate behavior, consider using the strategy pattern. Using these practices will make sure that as your code base grows it stays maintainable.

Github Repository musical_performance

Tweet me @yonk_nyc if you like this post.

Tweet