DEV Community

loading...

Strategy and Decorator Design Patterns

micahshute profile image Micah Shute ・6 min read

Why do I use a Decorator or Strategy?

Or any design pattern, for that matter? In general, design patterns are created and used to promote object composition over inheritance, allow for (black-box) code reuse, better enforce the single responsibility principle, and reduce the size and unwieldiness of your code's classes in general.
Like any design pattern, Decorators and Strategies have specific scenarios when they are there are beneficial to use. These two patterns can have similar use cases, and it can be easy to confuse when you should use one or the other. According to [1], both Decorators and Strategies can be employed when your goal is an "alternative to extending functionality by subclassing". Strategies can also be used when you have classes with algorithmic dependencies, and Decorators can be used to easily alter the functionality of a class which itself is not easily changed.

So what actually are they and how do you use them?
Let's get specific. Decorators and Strategies are just extra classes I write to alter/extend the functionality of a class I have already written. The way they alter this functionality is different, and therefore they are useful in different scenarios. First, let's look at the class diagrams [1]:

Decorator

Decorator Class Diagram

What is this saying? If you haven't looked at too many class diagrams, or you are only familiar with dynamically typed languages, it can be confusing. Each box with a bold title represents a class. In Ruby, we won't actually be programming all of the classes we would in Java or C++, for example.

All of the boxes which do not say Concrete are the architecture of our inter-class APIs that we are designing. In statically typed languages, we have to define these in code as Interfaces, Prototypes, Abstract Classes, etc. depending on the language. In dynamically typed languages, they are defined in the documentation and in our minds as the engineers of the class ecosystem. There are ups and down to both statically and dynamically typed languages; statically typed classes error check for you, help you be more precise and cleaner in your code, and ensure you are designing your system correctly, while dynamically typed languages require less code and can be a lot simpler to create and read IF designed well and properly documented. If designed badly, they can be much harder to read and debug.
But now getting back to the Decorator Class Diagram; basically, this diagram tells us that we have a class ConcreteComponent and we want to extend its functionality. This class has the functions and properties as defined by Component. So, we make another class with the same API as ConcreteComponent (i.e. it conforms to the Component design) except that it has one extra reference: a Component (which is going to be the original class you wanted to extend the functionality of or any decorator it is already wearing), and we call this new design Decorator. We can make as many different types of Decorator classes, as long as it adheres to this design.

Above you can see that the Component can do an Operation(). So, our Decorator also has to be able to do an Operation(). Since our Decorator has a reference to Component, it just looks at how Component does Operation(), mimics that output, and then changes what it needs to and returns this new-and-improved output from its Operation() method. That is what the dotted lines are showing you above.

Pseudo-Code:

class NumberComponent
   func operation(x){
      return x
   }
end

class BinaryNumberDecorator
   constructor(component){
      this.c = component 
   }  
   func operation(x){
      return this.c.operation(x).toBinary()
   }
end

c = new NumberComponent()
d = new BinaryNumberDecorator(c)
x = 2
c.operation(x) // outputs what your original class returns
d.operation(x) // extends functionality of NumberComponent using the same function call

I'll show more complex, working ruby code below.

Strategy

Strategy Class Diagram

Here, the Context is the class I want to extend the functionality of. So, I make a Strategy class that can perform that function. It is interchangeable because each ConcreteStrategy conforms to the same Strategy interface. Note that unlike the Decorator, the Strategy does not need to share an interface with Context. This design pattern is convenient when the functionality I want is complex and can be implemented with different algorithms. Let's say I am creating a class which needs to perform the Fast Fourier Transform (FFT). Since the FFT can be calculated in different ways, and I may want to switch to a faster/better algorithm in the future, I can implement a Strategy to perform the calculation.

Pseudo-Code:

class DataContext
   constructor(data, fftStrategy = new Radix2Strategy()){
      this.data = data
      this.fftStrategy = fftStrategy
   }
   func fft(){
      return this.fftStrategy.fft(this.data)
   }
end

class BruteForceFFTStrategy
   func fft(data){
      ...perform brute force fft
      return calculated_fft
   }
end

class Radix2Strategy
   func fft(data){
      ...perform radix 2 strategy
      return calculated_fft
   }
end

Intuition

Decorators are kind of like Russian nesting dolls. You take an object you want and nest it inside of your class which uses the inner functionality and adds onto it to makes it more functional. So actually, decorators are more like in MIB when the little Arquillian is controlling the humanoid body inside the head. He is like the original Component and the body is like a Decorator. He could then decorate himself further by having his human decoration operate some sort of exoskeleton. At that point, the little alien and his exterior would have the same methods, like moveArm() or push(), but the decorated exoskeleton would have a much different output (i.e. he could push harder). As such, the decorator has to have the same interfaces as the underlying class. It eats the original Component and allows the world to interact with it the same way it would interact with the original Component.
Strategies, on the other hand, are kind of like replaceable cards in a robot. Imagine you pulled off the back plate and there were slots with digital cards inserted for each function such as eating, walking, talking, etc. You could take any of them out and replace them when an algorithm was updated or you wanted to change the behavior of the robot.
In general, if you are wondering whether to use a Strategy or Decorator design pattern, a rule of thumb is to keep the base class simple in a decorator. If the Component is too complicated, you will have to mirror too many methods in each decorator, and it will be better to use a strategy.
Also, decorators are widely used when you want to dynamically add functionality to an object, not necessarily an entire class (and perhaps withdraw this functionality at a later time). Strategies are commonly used to perform a function which is very complicated and can be performed in different ways (perhaps with different time/space complexities).

Code Example

Decorators:


class IceCream

    def initialize(price: 1.0)
        @price = price
    end

    def price=(amt)
        @price = amt
    end

    def price
        return @price
    end

    def ingredients
        "Ice Cream"
    end

end


class WithJimmies
    def initialize(item)
        @item = item
    end
    def price
        @item.price + 0.5
    end

    def ingredients
        @item.ingredients + ", Jimmies"
    end
end

class WithChocolateSyrup
    def initialize(item)
        @item = item
    end
   def price
        @item.price + 0.2
    end

    def ingredients
        @item.ingredients + ", Chocolate Syrup"
    end
end

class WithOreos
    def initialize(item)
        @item = item
    end
    def price
        @item.price + 1.0
    end

    def ingredients
        @item.ingredients + ", Oreos"
    end
end


class FroYo

    def initialize(price: 1.5)
        @price = price
    end

    def price=(amt)
        @price = amt
    end

    def price
        return @price
    end

    def ingredients
        "Froyo"
    end
end


treat = IceCream.new
treat = WithJimmies.new(treat)
treat = WithOreos.new(treat)
treat = WithChocolateSyrup.new(treat)
puts treat.ingredients # "Ice Cream, Jimmies, Oreos, Chocolate Syrup"
puts treat.price # 2.7
another_treat = FroYo.new
another_treat = WithJimmies.new(another_treat)
puts treat.ingredients # "Froyo, Jimmies"
# Froyo and Ice Cream (Components) and all of the Decorators have #price and #ingredients
# methods, making them conform to the same interface.

Strategies:

class ChocolateSyrup
    def price
        0.2
    end 
    def ingredients
        "Chocolate Syrup"
    end
end


class Jimmies
    def price
        0.5
    end
    def ingredients
        "Jimmies"
    end
end

class Oreos
    def price
        1.0
    end
    def ingredients
        "Oreos"
    end
end

class IceCream

    attr_accessor :toppings, :price_strategy

    def initialize(toppings: , price_strategy: CalculatePriceStandardStrategy )
        @toppings = toppings
        @price_strategy = price_strategy.new
    end

    def price
        self.price_strategy.calculate(self.toppings)
    end

    def ingredients
        self.toppings.length > 0 ? "Ice Cream, " + self.toppings.map(&:ingredients).join(", ") : "Ice Cream"
    end

end

class CalculatePriceStandardStrategy
    def calculate(toppings)
        return 1.0 + toppings.reduce(0){ |cum, curr| cum + curr.price }
    end
end

class CalculatePriceRewardsMemberStrategy
    def calculate(toppings)
        return 0.5 + 0.8 * (toppings.reduce(0){ |cum, curr| cum + curr.price })
    end
end


toppings = [ChocolateSyrup.new, Jimmies.new, Oreos.new]
strategy = CalculatePriceStandardStrategy
treat = IceCream.new(toppings: toppings, price_strategy: strategy)
puts treat.ingredients # Ice Cream, Chocolate Syrup, Jimmies, Oreos
puts treat.price # 2.7
treat.price_strategy = CalculatePriceRewardsMemberStrategy.new
puts treat.price # 1.86

That covers the basics. You can see more in-depth descriptions and discussion in the reference below.

References:

[1] DESIGN PATTERNS Elements of Reusable Object-Oriented Software, Gamma, Helm, Johnson, Vlissides

Discussion (0)

Forem Open with the Forem app