Some of the scariest domain models around arose from the need to accommodate expanding levels of re-use. For instance, consider a student filling out a quiz. The same quiz might be answered by multiple students. It might be spawned from a template which is re-used across multiple classes. And that template, in turn, might be sourced from an example quiz shared across multiple schools.
The number of classes needed to model this kind of multi-level reuse can increase geometrically if you’re not careful. In this episode, you’ll learn a pattern which will enable you to collapse arbitrarily deep re-use hierarchies into a single, “flat” set of domain concepts.
Video transcript & code
Imagine we are writing teaching software for schools. Specifically, we are developing software for delivering quizzes to students.
Quiz has a title and some instructions. It also has a list of questions associated with it. Questions have attributes of their own, notably the question text and any instructions the teacher might have added to clarify the question.
Of course, we want multiple students to be able to take the same quiz. So we have a related class called
QuizResponse has a student and a collection of answers associated with it. Answer objects contain a reference to a question, along with the field for the answer that the student has provided.
After developing the software for a while, we decide to turn it into a software as a service offering that we can sell to many different schools. As one of the features, we want to be able to offer predefined quizzes which schoolteachers can select and add into their own school library of quizzes.
To enable this, we add a new class: a
QuizTemplate. A quiz template has a title and a description. It also has a list of
QuestionTemplates. A question template is similar to a question, but it has no need for specific teacher-provided instructions.
With the addition of quiz templates, we also do some normalization. Since quiz templates have titles, and quizzes have a reference to the quiz template they spawned from, we remove the seemingly redundant title field from quizzes. Likewise, we remove the question text from question objects. They can simply delegate their question text back to the question template.
Later on, we add yet another feature: quiz sections. A section is a block of questions that can be moved around as a unit. It can also be shared between multiple quizzes.
Inevitably, adding quiz sections also drives us to add section templates, and answer sections.
There is a design smell going on here that you might recognize from back in Episode #430, when we talked about "parallel hierarchies". Every time we've added a level of reuse, we've had to multiply the number of domain models in our design by the number of levels of abstraction.
By itself, this is already confusing. And it makes the code base difficult to change.
But there are other, less obvious problems with this design as well. In order to normalized data and keep things "DRY", we've made it so that question text and quiz titles can only be edited at the template level. Soon, we start hearing from teachers who want to make use of the template quizzes, but customize them with their own titles and question text.
An even more insidious issue crops up once we start updating template question text in response to teacher feedback. Question answers have references to questions, and questions refer back to question templates. Which means that a student might have answered one version of question, but their teacher sees a different version by the time they grade the response.
At this point we realize we need to complicate the model even more by adding some kind of versioning mechanism.
(By the way, this example isn't just something that I invented. It's based on actual project that I worked on. The main thing I changed is that I've drastically simplified it.)
There is an alternative to this design. It's called the prototype pattern. Let's switch from diagrams to code to explore this alternative solution.
We define a quiz class which has attributes for title, description, teacher instructions, a flag optionally marking a quiz as a template, a student, and a list of questions.
We customize how object of this class are copied, by defining an
initialize_copy method as we saw in Episode #486. Inside, we ensure that new copies of template quizzes will not themselves have the template flag set to true. We make sure that all other attributes are copies of the originals, instead of references back to them. This includes copying each question.
class Quiz attr_accessor :title, :description, :teacher_instructions, :is_template, :student, :questions def initialize_copy(original) super @is_template = false @title = @title.dup @description = @description.dup @teacher_instructions = @teacher_instructions.dup @student = @student.dup @questions = Array(@questions).map(&:dup) end end
Next we add the question class. This class has attributes for the question text, special instructions, and a question answer.
Here again, we customize the way these objects are copied so that new duplicates don't share objects with their originals.
class Question attr_accessor :text, :special_instructions, :question_answer def initialize_copy(original) @text = @text.dup @special_instructions = @special_instructions.dup @question_answer = @question_answer.dup end end
In order to keep his example short, I'm leaving out the "questions section" feature. But now you seen the definition of quizzes and questions, you can probably imagine how a question section class would be defined.
Let's put these classes through their paces.
We'll start by defining a new quiz template, by instantiating a quiz object and setting it's template flag to true.
We'll give it a title and a description.
Then we'll add some questions to the template.
template = Quiz.new template.is_template = true template.title = "Movie quiz" template.description = "A quiz about movies" template.questions =  template.questions << Question.new( text: "What's the answer to life, the universe and everything?") template.questions << Question.new( text: "In 'Mission: Impossible', what's the password to the vault?")
In order to create a concrete quiz from a quiz template, we simply duplicate the template. The new object has copies of all of the original attributes, except that it is not marked as a template.
quiz = template.dup quiz.title # => "Movie quiz" quiz.description # => "A quiz about movies" quiz.questions # => [#<Question:0x00000001ddbcc8 # @question_answer=nil, # @special_instructions=nil, # @text="What's the answer to life, the universe and everything?">, # #<Question:0x00000001ddbb38 # @question_answer=nil, # @special_instructions=nil, # @text="In 'Mission: Impossible', what's the password to the vault?">] quiz.is_template # => false
We can customize attributes of the quiz for particular school, without having any effect on the template. While we're at it, we can also add school-specific guidance to the questions.
quiz.description = "About movies that we love" quiz.description # => "About movies that we love" template.description # => "A quiz about movies" quiz.questions.first.special_instructions = "Include calculations."
In effect, we have used the quiz template as a prototype object for this quiz.
A given school has a library of quizzes.
Let's add this new quiz to the school library.
Now, how do we use this object to administer a quiz to a specific student?
That's easy: we just duplicate it again.
We fill in the student field for the quiz.
And as the student enters answers into the program UI, we fill in each question's answer field.
library =  library << quiz response = quiz.dup response.student = "Avdi" response.questions.question_answer = "42" response.questions.question_answer = "789551"
Before, we used a quiz template as a prototype for quiz. Now, we've used the quiz as a prototype for a quiz response.
Because the quiz response has unique copies of all of its attributes, we don't have the problem where updating a template question causes answers to fall out of sync with questions. Instead we have a record of exactly the question that the student actually answered.
template.questions.text = "What was the departing message from the dolphins?" response.questions.text # => "What's the answer to life, the universe and everything?" response.questions.question_answer # => "42"
And of course our domain model is much, much simpler than our original version.
As with any design choice, there are trade-offs with this approach. By making all attributes be unique copies, we've eliminated any possibility of easily changing some attribute at the template level, such as quiz instructions, and having that change propagates throughout all concrete quizzes that have been spawned from it. Teachers who want an updated version of a template quiz will have to re-copy it to get the updates.
By reducing sharing, we've also increased the data storage requirements for our application.
But all in all, I think these are worthwhile trade-offs for this particular application. Our design is a lot simpler, and a lot more flexible now. If we needed to add fourth layer of reuse, let's say for groups of schools all united under a single umbrella organization, we easily could. Without having to introduce any new classes.
And that's the Prototype Pattern. I hope you'll find it useful in your own designs. Happy hacking!