Refactoring Ruby: From Subclass to Registry
From developing a solution to gradually improving it by listening to the pain
My team was given a particularly laborious task: implement responsive images throughout a legacy Rails application. Back then, the app was sending disproportionally large image files over to web browsers regardless of client device and viewport characteristics. Users had to bear the burden of prolonged load times and an overall bad experience which contributed towards decreasing conversion rates.
To solve this problem, we provisioned an on-the-fly image server to perform image manipulations in real time and wired it up to a CDN layer. Our first task was to assess whether the concept would work on a small portion of the product, which was far away from the home page and thereby did not have as much traffic.
The first version
With all the basic infrastructure in place, we looked for gems to help us implement responsive images but none proved to be suitable for our use case. We then started developing our own solution to fulfill the following requirements:
- One
Image
(AR model) maps to one responsive image (one-to-one). - Responsive images can have different formats: HTML, CSS and JSON.
- Formats can be shared among internal and external apps for preloading, JS integration and other purposes.
If you are wondering, a responsive image consists of a set of images where each one applies to a different screen size or device-pixel-ratio.
After some work, a ResponsiveImage
class similar to the following was born:
As you may have figured, this class aims to provide functionality for subclasses to define and gather image versions:
Take line 2 for example: it declares an image of 173x130 pixels which is meant to be displayed on viewport widths between 0 and 599 pixels. Also, the inherited
hook automatically declares an :original
version on all subclasses. Among other utilities, each version is meant to generate a distinct URL to the image server requesting a crop corresponding to the declared dimensions:
# Returns a URL for the :sm version
responsive_image.url(:sm)
In the end, a ResponsiveImage
object is nothing but a proxy for obtaining different representations, which functionality gets delegated away to specialized objects:
Finally, here is an example usage:
<%= FreebieMainImage.new(freebie.main_image).to_picture %>
This ERB code outputs a picture tag HTML containing display rules and URLs for all versions declared in FreebieMainImage
.
A touch of convenience
That implementation proved to be a great fit for our problem, but using it was not without friction. Among other issues, we needed to reuse the same responsive image over a single endpoint, and instantiating objects more than once did not feel right.
Using the controller for this purpose would have been a bad move since it would encourage bloating all others:
We needed an abstraction to make this recurring code easier to work with. After thinking for a while, we came to the conclusion that a factory to group responsive images by model seemed like an appropriate solution.
Here’s the sketch: given our Freebie
model has one main_image
, our collection automatically figures the right presenter for it (FreebieMainImage
) and delivers the result through a hash-like interface.
Let’s apply some programming by wishful thinking and picture how our imaginary code ought to be used:
Finally, here’s the implementation:
As you can see, we are using a Railsy convention to find the responsive class of an image: if the model name is Freebie
and the image name is main_image
, we then look for a class named FreebieMainImage
.
The following method at FreebiePresenter
makes the code even more accessible and avoids further pollution of controllers:
Finally, here’s how to use it in the views:
<%= @freebie.images[:main_image].to_picture %>
Advantages of the first version
The launch gone smooth and the code was technically adequate, because:
- Concerns were kept separate and we did not bloat the model with responsive configuration. That would be more akin to a classic Rails solution.
- The
ResponsiveImage
class was not coupled to any particularImage
. - The
version
method proved to be a great and nifty macro. - Although palliative,
ResponsiveImageCollection
fulfilled the needs of the first launch: it mapped resources one-to-one but it did not couple the core system in any way.
Good enough, ship it!
The second version
The time came around to implement the system in a wider range of pages, right after our successful POC got deployed. Additionally, adapting the core was necessary to take into account a newfound complexity:
- One
Image
maps to manyResponsiveImage
and vice-versa (many-to-many).
We did some planning and went on to work on these new changes.
Realizing the symptoms
During our first iteration, we repeatedly found the need to declare responsive subclasses:
Then an obvious realization emerged: creating a class for each and every responsive image would get exhausting pretty quickly.
We did not anticipate the pain because at first there were just a few configurations to deal with. This is OK: we released the minimal amount of code to solve our problem, but the solution had to evolve to accommodate newfound requirements and eliminate rough edges.
After asking a few questions to ourselves, an interesting fact turned up: the role of our subclasses was to gather and centralize data.
Wait a minute, isn’t inheritance mostly about behavior?
That’s right: inheritance is best for specializing behavior where a “Y is a X” kind of relationship exists. Of course, our subclasses did not meet those requirements: instead they acted as configuration hubs supplying data over to inherited methods.
Never should the role of inheritance be *just* to share methods and implementation details. This kind of misuse usually points toward design improvements.
As a side effect, our approach generated complexities not apparent at first sight such as looking for artifacts by class name. This is OK when dealing with SOLID-oriented code, but it did not happen to be our case.
The solution
Our problem seemed more suited to a registry that provides configuration to a factory. Didn’t get it? So bear with me.
The first step to refactor our solution was to start moving versions
from class-level to instance-level, though not removing the class method yet:
Great, everything worked just as before and it was possible to instantiate responsive images on-the-fly without the bureaucracy of subclasses:
Since we started moving versions
into an instance method, the next step was to change all call-sites from class method to instance method (we will not present this step here).
However, we were still using subclasses:
FreebieMainImage.new(freebie.main_image)
Eliminating these artifacts proved to be a bit more difficult. First of all, we created a registry to allow getting rid of subclass-centric configuration:
Then we moved versions of all subclasses onto it. For instance, the following code can be placed on a Rails initializer:
As you can see, the add
method receives a configuration ID and an array of version information which gets mapped away to Struct
objects.
Next, we changed ResponsiveImageCollection
to search for config keys instead of subclasses:
Finally, we were able to definitely delete ResponsiveImage.version
and ResponsiveImage.inherited
methods. Our class got much simpler:
But we were not over yet.
One last coupling
Refactoring our solution was relatively easy, but there were still problems. What if we wanted to reuse the same responsive configuration twice? The short answer is we couldn’t, because our collection class was using a hardcoded naming scheme to look up configurations.
For instance, if we had a Project
model and wanted to declare versions for its main_image
, we would have to use a project_main_image
key:
This was inflexible and did not scale up, not to mention the naming scheme was obscure and hard to remember.
What if we had a way to explicitly map images to their responsive counterparts? In that case, we would be able to specify and repeat any combination and quantity of elements.
Firstly, we created a CollectionConfig
class similar to ResponsiveConfig
:
Then we configured our collection mappings like so:
And finally, we changed our collection class to interpret these mappings:
Awesome! In doing that we were finally able to reuse the main_image
config on a random_image
(or any other image for the matter):
Using the system was still very easy, not to mention the ERB call sites did not change a tiny bit:
<%= freebie.images[:main_image].to_picture %>
Of course, our collection class could have been made more flexible. For example, we could have gotten rid of the model coupling: why does a collection need to be coupled to a model anyway?
However, do we need to? Nope, it’s a feature! Other kinds of collections can easily be created, and the underlying system is flexible enough to allow for just that.
Wrap up
The code in here was considerably changed to fit into main theme of this blog post. Also, we did not go over the real complexity faced while implementing the solution.
Nevertheless, the key ideas here are:
- If the amount of change you need to perform on a task is huge, tackle it in small chunks and start small.
- Don’t try to guess the future: write just the amount of code your solution needs. Listen to the pain and allow your system evolve naturally.
- Inheritance is not bad, but it’s also not suited for many problems.
- Beware when subclasses are used with the sole purpose of sharing data. Be attentive to their best usages and keep asking questions to yourself.
Thanks for reading and have a great day.