Adding Functionality to Ruby Classes With Decorators
In my last article, I presented some code that wrapped up accessing a customer’s Stripe data and added a caching layer on top. I wanted to take some time to dig in to that code and see how we can make it better.
Decorators give us a tool to add additional functionality to a class while still keeping the public API consistent. From the perspective of the client, this is a win-win! Not only do they get the added behavior, but they don’t need to call different methods to do so.
Our original class accessed data from Stripe AND cached the response for some time period. I accentuated “AND” because it’s generally the word to be on alert for when considering whether functionality can be teased apart in to separate responsibilities.
The question becomes, can we make one class that accesses Stripe data, and another that’s only responsible for caching it?
Of course we can!
Let’s start with the most basic form of accessing our Stripe customer data with the Stripe gem:
Extract an Adapter
Because we’re interfacing with a third-party system (Stripe), it makes sense for to create a local adapter to access the Stripe methods. It’s probably not likely we’re going to switch out the official Stripe gem for another one that access the same data, but a better argument might be that we could switch billing systems entirely in the future. And if we make a more generic adapter to our third-party billing system, we would only need to update our adapter when that time comes.
While the adapter optimization may seem like overkill here, we’ll see how that generic adapter helps us implement our caching layer shortly.
Let’s start by removing the notion that it’s Stripe and all and call it
Billing. Here we can expose the methods needed from the
There we have it. A simple
Billing class that wraps the methods that we used in the first place — no change in functionality. But certainly more organized and isolated.
Let’s now use this new class in the accounts controller from earlier:
Not too bad! At this point we’ve provide the exact same functionality we had before, but we have a class that sits in the middle between the controller and Stripe gem – an adapter if you will.
Create a Decorator
Now that we have our adapter set up, let’s look at how we can add caching behavior to improve the performance of our accounts page.
The most of basic form of a decorator is to pass in the object we’re decorating (
Billing), and define the same methods of the billing, but add the additional functionality on top of them.
Let’s create a base form of
BillingWithCache that does nothing more than call the host methods:
So while we haven’t added any additional functionality, we have created the ability for this class to be used in place of our existing
Billing class because it responds to the same API (
Integrating this new class with
AccountsController looks like:
As you can see, we only had to change one line — the line where we decorated the original billing class:
I know what you’re thinking, “But it doesn’t actually cache anything!”. You’re right! Let’s dig in to the
BillingWithCache class and add that.
Adding Caching Functionality
In order to cache data using
Rails.cache, we’re going to need a cache key of some kind. Fortunately, the original
Billing class provides a reader for
billing_id that will allow us to make this unique to that user.
In this case,
item can refer to things like
"upcoming_invoice". This gives us a method we can use internally with
BillingWithCache to provide a cache key unique to the both the user and the type of data we’re caching.
Adding in the calls to actually cache the data:
The code above caches the call to each of these methods for 15 minutes. We could go further and move that to an argument with a default value, but I’ll leave as an exercise for another time.
Separating your application and third-party services helps keeps your applications flexible — offering the freedom to switch to another service when one no longer fits the bill.
Another benefit of an adapter is you have the freedom to name the class and methods whatever you like. The base gem for a service might not have the best names, or it may be that the names don’t make sense when dragged in to your application’s domain. This is a small but important point as applications get larger and its code more complex. The more variable/method names you need to think about when you poke around the code, the harder it’ll be to remember what was going on. Not to mention the pain new developers will have if they acquire the code. Whether it’s you or the next developer, the time you invest in creating great names will be greatly appreciated.
Using decorators in this way makes it easier for clients of the code to avoid change, but keep your applications flexible. The
Billing class above was relatively simple — intentionally so. If the class being decorated has more than a few methods, it might be worth incorporating
SimpleDelegator to ensure the methods that don’t need additional functionality still continue to respond appropriately.