The technique of composition over inheritience is more than simply encapsulating objects into larger entities; its really about defining models as being made up of resuable behaviors. It makes sense then in Behavaior Driven Design we apply the technique not just when writing the implementations of our models but also when creating the specifications themselves. Instead of many files repeating the same functionality in large blocks of spec code, we end up with specs that look like:
This is pleasantly tighter, DRYer, and tighter code. So how did we “compose” this spec and behaviors? TL;DR »
Imagine we are developing a media management application. Within this system, an assignment can be created for an image into multiple galleries. This is simple enough to model.
Additionally, each of these models can be marked with metadata, which comes in three flavors:
hstoreand are also searchable.
There is one additonal important detail; not all types of metadata would be available on every type of model:
Given the above requirements, it is apparent that at some point composing with modules will come into play and we could leap to that point. But in practice, we would be following our story-based agile process, and so may end up fully developing the
Image model first.
We start by writing out a spec, using Factory-Girl to handle instance generation.
And then implement the model to pass the spec. Note the custom code below to handle the Postgres array and hstore based tags and hashbag:
With working specs and implementation in hand for all of our metadata, we can move on to adding the same but different functionality to the remaining models. And as good devs, we HAVE to start with specs that define the behavior. We could simply use the ugly technique of “copy-paste” the spec code of
Image into the specs of the
Gallery. But this begs the wrath of The Maintenance Gods and their acolytes, your fellow devs!
So we won’t, as we additionally follow the principle of DRY; Don’t Repeat Yourself. This is where RSpec shared examples come into play. A shared example is a collection of context and examples you can declare with a name:
The shared example can then be invoked within the context of your specs with
include_examples or (my preference)
You can even pass in parameters into a shared example:
Using this feature we can refactor the behavior of each of the three different types of metadata into their own shared examples. Looking at the code in the original specs, we need to change two things to better support testing against any model; not calling the methods on the
Image class and using the appropriate factory when instantiating instances to test against. We do this by passing the model we are testing and the factory to use as parameters:
Now we can rewrite the spec for
Image to use these shared examples:
Even better we can now easily write the specs of the other two models:
We have effectively “composed” our specs from a set of behaviors! We also have not said anything about HOW this functionality will be achieved, maintaining the BDD mantra of testing behavior NOT implementation.
Now how to get these new specs to pass? Again the most ugly way would be to simply copy and paste but remember; those gods and devs sure are wrathful!
In all seriousness, this is where code reuse becomes important and also illustrates where composition will trump inheritence. Yes, the desired functionality can be achieved with inheritence. We could introduce a parent
MetadataBase class that all our models inherit from. Or decide that
Image descends from
Gallery which descends from
Assignment with each level adding the additional metadata implementation needed their parent does not provide. But neither correctly represents our domain; the first example exposes functionality on
Gallery that really shouldn’t be there while adding class bloat, and the other solution is just plain wrong!
The better solution is to encapsulate the behavior of each specific kind of metadata into seperate modules:
We can then compose our models by including the modules of behavior they actually have.
This is extremely elegant on so many levels. It adheres to the single responsibility principle with each module solely focused around the logic of the metadata it represents. It DRYs up the implementation. And the resulting code practically reads like the domain model we outlined at the top of this post:
Our specs could stand to be DRYed up even further. The repetitive passing in of the class we are testing and its factory now standout like a sore thumb after all our efforts. What to do?
By default the
subject of a spec of a class is an instance of that class created via a call to
new. Given that, we create a shared context that derives the model and factory from the current subject.
We can then include the shared context within each shared example and drop the need to have them passed in.
Finally, our specs drop repeatedly passing the same set of arguments to the shared examples, reading cleaner than ever:
There are further “optimizations” we could do to the code above. For example, we could also write specs for each module itself, testing all paths within it, and only compose the model specs from “coarser” variations that just verify the general functionality; this approach would speed up running the entire test suite. But these and other exercises are left for the reader to pursue.
Most important is to realize that composition over inheritence can be elegantly implemented in our codebase from the very start, resulting in well-designed and easily maintainable domain, specs and implementation.
shared_exampleto define the behaviors that are shared between objects.
Modules that you will
includein your implementation.
Rudy’s fascination with computer programming began at age 10 when he mistakenly picked up the Micro-Adventure book Space Attack; he thought it was going to be about Star Wars. That happy accident led him to graduate from McGill University in Computer Science and start a 12 year career in software development playing with a wide range of technology; everything from web applications to cryptology to NoSQL.