The Original FileMaker Community
Business Templates - Demo Apps - Video Tutorials -Samples - Help - 46000 Member Forum

The Leading Filemaker Developer Tools

The Decorator Pattern – beezwax blog

The decorator pattern is one of my favorite patterns. It is simple, extensible and powerful. It feels like it follows the essence of object oriented programming beautifully. Sadly though, it is also easy to be misused or misunderstood. So, in this post I will show you the essence of the decorator pattern, illustrated with a few examples.

Let’s start with a problem

Say you have to make a system for a pizza place. The Pizza class might look something like this:

class Pizza {
  // ...
  get price () {
    return 30
  }
}

So far, so good. Our next requirement though, is handling toppings. Ham is $3 and eggs are $2. An initial implementation might look like this:

class Pizza {
  // ...
  get price() {
    const base = 30
    if (this.wantsHam) base += 3
    if (this.wantsEggs) base += 2
  }
}

It certainly works for this particular case, but there are a few issues with this code:

  • If we add more toppings, we need to add more conditionals. And conditionals add complexity.
  • For each conditional, we have to add a positive and negative test case, which is easy to miss.

Sometimes, conditionals aren’t bad, but most of the time, they are pointing at bad object-oriented design. So, how can we get rid of the conditionals? A naive approach would be to use inheritance:

class Pizza { ... }
class HamPizza { ... }
class EggPizza { ... }
class HamAndEggPizza { ... }

But it’s easy to see this will quickly get out of hand as soon as we add more toppings. Adding classes for each combination of toppings simply doesn’t scale well.

Luckly for us, there is another way: Composition (remember: prefer composition over inheritance!)

class Pizza {
  get price () { return 30 }
}

class HamPizza {
  constructor (pizza) {
    this.pizza = pizza
  }

  get price () {
    return this.pizza.price + 3
  }
}

class EggPizza {
  constructor (pizza) {
    this.pizza = pizza
  }

  get price () {
    return this.pizza.price + 2
  }
}

const hamAndEggPizza = new EggPizza(new HamPizza(new Pizza()))

Wow, what happened there? HamPizza and EggPizza take a pizza object and decorate its price method, adding their own logic to it.

So, for the hamAndEggPizza object, when we call .price it will go all the way to the pizza base class, return its value (30), then go back in the call chain to HamPizza, add 3, then go back to EggPizza, add 2 and return. It ends up looking like this: 30 + 3 + 2.

That was quite the mouthful! A graph makes it way easier to understand:

The good thing about this pattern is that consumers of the Pizza objects don’t care if it’s a Pizza, a HamPizza or whatever, really, as long as it implements the proper interface (#price).

It certainly looks funny when composing all of our objects together (and that can be addressed in several ways), but notice the massive flexibility we get from this:

  • Linear scaling: Just one class per topping
  • More powerful: We can compose as many classes as we need in run-time, we can even make things like “double ham”
  • Trivial to test
  • Simpler: No more conditionals, logic is easier to follow

A real world example: Configuration Options

That’s all fine and dandy, but in a real app, pizza prices would just use a database and some logic to calculate prices. Let me show you instead a real example.

Imagine we have a Parser class with a #parse instance method. We don’t need to worry about how that method works.

A new requirement comes in: We need to cache that method, but only if we pass true to the cached option. We could add an if statement to #parse, but let’s try out this new pattern instead:

class Parser {
  // ...
  parse (input) { ... }
}

class CachedParser {
  constructor (parser) {
    this.parser = parser
    this.cache = {}
  }

  parse (input) {
    if (!this.cache[input]) this.cache[input] = this.parser.parse(input)
    return this.cache[input]
  }
}

function parserBuilder (options = {}) {
  let parser = new Parser()
  if (options.cached) parser = new CachedParser(parser)
  return parser
}

It might seem like an overkill for a simple functionality like this to be its own class, except it gives us all the advantages we talked about earlier, like less complexity and easier testing. Also, it’s way easier to extend. Note that the parser itself doesn’t even need to worry about the options object, because it’s handled in a builder.

Consider now we need to add logging. Well, that’s easy!

class LoggedParser {
  constructor (parser, logger) {
    this.parser = parser
    this.logger = logger
  }

  parse (input) {
    this.logger.log(`Parsing: ${input}`)
    this.parser.parse(input)
  }
}

function parserBuilder (options = {}) {
  let parser = new Parser()
  if (options.cached) parser = new CachedParser(parser)
  if (options.logger) parser = new LoggedParser(parser, new MyLogger())
  return parser
}

We new have a lot of flexibility, we can either have a cached parser, a logged parser, or both. This way of adding features is a breeze to scale and test. When used properly, it just feels great.

Misuses

As you can see, the decorator pattern shines when it enhances the logic of the object it decorates, be it parse in the example above or price in the first one.

While the decorator can add other methods to the objects, that’s not the essence of the decorator pattern.

Particularly in the Ruby community, it’s common to see gems like Drapper, which is used only for adding methods to an existing object at runtime. And many new developers see that and they think that’s the decorator pattern, when in fact it’s more like the facade pattern.

Besides helping us solve problems, patterns are useful for communicating between developers, so it’s quite important that we develop a somewhat similar understanding of what terms like “decorator pattern” or “facade pattern” mean, to avoid confusion down the road.

Hope you found this post useful. Cheers!

— Fede

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

Privacy & Cookies Policy