Chubby models are still fat with Concerns. DCI focuses on how things work together

by Jim

You’ve seen complicated codebases. You’ve dug through other people’s code without a clue to what’s going on or how it all fits together. Perhaps it was your own code or perhaps someone else’s but you know how difficult it can be to figure out how this or that aspect of a system implements some required feature.

Often, code organization is a key aspect of aiding communication. Today on the 37signals blog, David Heinemeier Hansson wrote about how ActiveSupport::Concern can really help clean up your code. Rails 4 will even have new concerns directories in the load path by default, making it even easier to use this feature.

He points out that Basecamp has almost 40 concerns that organize features required for multiple models. Giving an example, he says “This concern can then be mixed into all the models that are taggable and you’ll have a single place to update the logic and reason about it.”

What’s great about this is that single place. To understand something thats “Taggable” or “Searchable” you only need to find that concern and you have all that you need. Making changes is just as easy; it’s all right there.

Addressing some of the objections to these fat models, he says:

It’s true that this will lead to a proliferation of methods on some objects, but that has never bothered me. I care about how I interact with my code base through the source. That concerns happen to mix it all together into a big model under the hood is irrelevant to the understanding of the domain model.

I agree with the first part of this. Most of the time an object that has many unrelated methods isn’t much of a concern. But when we need to drop into a debugger and look at what the object is and can do, then we are met with far more than we need. Inspecting the runtime object becomes difficult because of the overwhelming features and responsibilities that it has.

Almost 100% of the time, however, we interact with code through the source. This is spot on. We read and write code. But the fact that concerns happen to mix it all together under the hood is not irrelevant to understanding a domain model. It’s important how many of these mixins are used and how complicated an individual class can become.

But even more important is how it works. Not how things are mixed in, but how your program works. What is going on?

Why do you care that a model is Taggable? What purpose does it serve and what other actor in the system will have a need for it?

Using concerns can be helpful, but if it’s used by many models it may effectively become a fragile base class problem where changes to it have unintended ripple effects through those models that inherit its features.

ActiveSupport::Concerns and Roles in DCI are not the same. Roles in DCI are ONLY defined within a context. Your context represents a use case for your system and is larger than a single procedure. A use case covers success paths, failure paths, and alternate paths to the execution.

I had one reader of Clean Ruby, Drew Ulmer, write to me and say:

As far as putting roles directly into contexts, I can’t tell you how much I love this idea. It’s fantastic to have all of the logic and model behavior for a specific activity in one place and not have to jump around files. It keeps the models focused around how they are defined and not around what we use them for or how they behave; it keeps the behavior focused and easy to reason about; and it defines a very clear API that must be implemented for a context.

I passed this along to my mailing list and after reading it, I got this response from Anthony Burton saying:

To be honest, reading this I kept thinking “that’s just the way things were supposed to be”. Nevertheless, I always get so frustrated trying to dig through a project (new or old, right?), and trying to trace the flow of execution and how things fit together. This made me think of a project I worked on a few years back (which thankfully had a substantial logging capability) where I wrote a little perl script that would parse logs files and show how things were linked together. It was a very popular utility with the other devs and I was always curious, 1) why no one had written something like it before me; and 2) why the code wasn’t just organized differently.

This is a valuable lesson for communicating what your software is supposed to do. You not only need to read the code, but you need to understand how different pieces work together. DCI is a great way to organize code and achieve better communication.

Here’s a dumb example of what your context might look like:

class SwappingDetails
  def initialize(source, recipient)
    @source, @recipient = source.extend(Source), recipient.extend(Recipent)
  end

  def swap
    # trigger the appropriate methods
  end

  def other_trigger
    # trigger alternate behavior
  end

  module Source
    # some related methods here
  end

  module Recipent
    # some related methods here
  end
end

Those modules represent the methods required for an object playing the role. With concerns, you must understand more about the classes of objects ahead of time whereas within a DCI context you’d focus only on roles and their interaction. When using concerns, you understand less about what will trigger the action, what other objects are necessary, and what roles those other objects play.

Concerns can be useful, but just like anything else they can be done poorly. Thinking in terms of interacting roles is not the same as abstracting related behaviors into a module.

Regardless of what you do in your application, communicating it’s purpose is key to managing change. The better others can understand how something works, the easier it will be to make necessary changes.

Clean Up Your Code

If you liked this post and want more like it, get periodic tips in your inbox by leaving your email address below.

To get more information about cleaning up your objects, classes, and code, then check out Clean Ruby, an ebook which will describe ways to keep your code clean, maintainable, and focused on business value. Make your OOP more obvious, easier to understand, easier to test, and easier to maintain.



Comments

Kendall B. said on Sunday, January 27, 2013:

I've tried to use DCI for several months now, and I can't seem to get past one thing:

I want business logic (my contexts) to exist outside my Rails apps (as gems) – primarily to make our company service oriented. Persistence and querying is always the problem in DCI.

With the context objects having been written in a gem, but implemented in a Rails controller, what is the best way to give it persistence capabilities? Dependency Injection?

I've seen people discourage instantiating role players within contexts – instead passing them in as arguments – but I can't see how else I might have a "BuildHouse" context that can persist information about the house, etc.

This most applies to queries. Say, you have a report: Do you query around for multiple actors, and then pass them to a context, only for the context to "extend" them with roles such that you can now render information about them on a web page? Or is it best to pass in the "User" class as in a repository pattern?

In theory, contexts are great, but in practice, this problem has tied me in knots, and led me to inconsistently applying patterns just to get the job done…

Find more in the archives

1999 - 2014 © Saturn Flyer LLC 2321 S. Buchanan St. Arlington, VA 22206

Call Jim Gay at 571 403 0338