Marketdown: Exploring DCI
I recently read a book titled Clean Ruby by Jim Gay. It wasn’t too long, but I found the content quite interesting.
In it, he describes a way to write cleaner Ruby code using the Data-Context-Interaction (DCI) pattern.
Thin Controller; Fat Model
In a typical Rails app, which ascribes to “Thin Controller; Fat Model”, each
model has many responsibilities. Instead of having one enormous model file, we
can group related methods or behavior, and extract them into modules/concerns.
We then include
them on the model.
The downside of this approach is that every instance of that model class has every one of these methods on them, all of the time. Regardless of what controller action was invoked, each instance has all these methods, most of which are likely not used during this single action.
Service Objects
To slim down our models, one option is to use service objects.
This is an approach I like. You can break your code into smaller pieces based on responsibility and use patterns like decorators, presenters, observers, and others. Data-Context-Interaction seems like an extension of service objects.
Explaining DCI
The “service” in this case is really the Context. The context is the thing we’re
trying to accomplish. If we’re purchasing a book, then the context is
Purchasing
.
A context may have one or more objects, the Data, which interact to accomplish
some task. The data play “roles” in this context. To purchase a book, we can
imagine two roles: Purchaser
and Book
.
The interaction between the data in this context is what glues things together.
Here, that interaction could be complete_purchase
.
Gems for DCI
Jim created the Surrounded and Surrounded-Rails gems to allow us to use DCI in our Ruby/Rails applications.
Note: I did have issues where the surrounded-rails didn’t seem to do what it promised by automatically include Surrounded in the models.
My Demo
Since I’ve been interested in writings and markdown lately, I decided to create a small Rails 4 app which would allow people to sign in and create a book, which other people could then purchase.
I called it Marketdown. You can view the live demo as well as the code.
You can test out the demo yourself:
- Type in a username, and sign in
- No passwords here, it’s really barebones
- Create a book yourself
- You can entire Markdown, which will then be rendered as HTML when people view your book
- HTML should be escaped, to hopefully prevent some maliciousness
- Purchase a book someone else has created
- Don’t worry, there’s no credit cards or anything.
Once you purchase a book, the site indicates that.
That’s about the extent of this demo.
Digging In
The most interesting part of the demo is how purchasing a book is handled. The
context, as we mentioned above, is Purchasing
.
In the site, we have users. And a user who wants to purchase a book plays the
purchaser
role. The book they’re purchasing, no surprise, plays the book
role.
The benefit of roles is that a generic class like User
can be used, but given
a more meaningful name in this context, based on the role.
We “trigger” the interaction between these two roles with a method called
#complete_purchase
.
The trigger contains the business logic behind purchasing.
- A user has to be logged in
- An author can’t purchase their own book
- A user can’t purchase the same book twice
The context is where this business logic lives. It doesn’t have to be stuck in the controller, on in the model. The controller just invokes the context.
Another huge advantage of DCI and the Surrounded gem is that we can add behavior to each role, only in this context. The methods are added to the role’s instance, scoped to this context. The instance starts playing the role, so it gains some additional behavior. When the instance is done playing the role, it loses the behavior.
This seems like a great way to limit behavior to specific instances, to help keep your models clean. You only add behavior to instances just when you need it. Your models retain their persistence, validation, and other ActiveRecord magic all the time, but they receive additional behavior based on the roles they play.
When a user is signing up, neither the User
class nor the user instance need
the #owns_book?
method. They’re outside the Purchasing
context, so they
don’t have it.
In another context, Authoring
, an author
can
#publish_book
,
because they need that behavior. But outside this context, the user doesn’t need
that ability.
Discussion
After talking with my coworker, Zac, about this pattern, there is a downside to DCI.
We’re used to creating classes, which give behavior to instances of that class. It’s standard OO. But it would be surprising for someone to look at the above demo, without understanding the concept behind DCI, and understand what’s going on. It violates the principle of least astonishment.
Perhaps it’s just that this pattern is new and unfamiliar. With a bit of communication and understanding what problems it solves, that could become clearer.
But it is another abstraction we’ve added to our design and mental model of the software. Perhaps the ideas are worthwhile, but is the cognitive overhead of the pattern more valuable than the cost to learn, understand, and apply it? Zac gave several links which had interesting discussion about abstraction.
Concluding
I’ve only just become aware of the DCI pattern, and Marketdown is my first foray into applying it. I’ve not had any production experience with it. It does pique my interest though.
I like the concept of breaking an application’s features into contexts, which isolate behavior to their roles, and allow them to interact to accomplish something larger.
I would like to see what the software’s design looks like when DCI is applied to real problems on a larger scale. I’ll keep an eye our for other projects that might use this. And I’ll see if I can apply it to a future side project of mine.
Writing this post alone has helped me better understand the concepts, and I hope it’s useful to someone else. Thanks for reading!