On October 17, 2012, Bryan Helmkamp, founder of Code Climate, wrote a blog post outlining 7 patterns to refactor fat ActiveRecord models in Ruby on Rails. Here at Crush & Lovely, this post is a core reference for all Rails developers on how to separate concerns, write modular, concise and expressive code, and make testing exceedingly simple.
When a process has side effects that need to be executed only in certain situations, this functionality can be layered onto an existing operation using Decorators. A Decorator takes an object and wraps auxiliary functionality around it, letting you add on what you need when you need it and keeping the core procedure untouched.
Let’s imagine a Service Object that generates report cards for each student in a class. A teacher can generate these report cards at any point, but there are some situations where those report cards should be mailed to the students’ parent.
One way to address this would be to have two separate Service Objects,
GenerateReportCardsAndEmailParent, but this creates duplication and is hard to maintain when there are many cases with many conditional steps.
Another way would be to put together callbacks, such as:
This isn’t bad, but it relies on the return value from the Service Object to be usable for the subsequent process. Additionally, the same process may need to happen in both procedures, such as generating the HTML for the report card.
This problem calls for a Decorator, which targets a specific method and layers on top of it, allowing us to tap into an existing operation and add auxiliary functionality. So, for this example, our Service object looks like this:
We could create a Decorator object that accepts an instance of the Service Object as its argument and returns that Service Object with the specified method wrapped with the added functionality.
One key goal of the Decorator pattern is that the returned object is the same object as the input object, both in terms of identity and API, but with an altered property. So the following should be true if the object is decorated properly.
With this pattern in practice, we can layer on any number of Decorators that decorate any number of methods on the original Service Object for much win.
When testing a Decorator, it is wise to test that the method is both decorating the original object properly and that the auxiliary method or methods are performing the right actions. A reasonably comprehensive test suite for the
EmailReportCardToParent Decorator above could look like this: