Skip to content

Instantly share code, notes, and snippets.

@practicingruby
Created December 19, 2012 22:29
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save practicingruby/4341122 to your computer and use it in GitHub Desktop.
Save practicingruby/4341122 to your computer and use it in GitHub Desktop.
Models, Roles, Decorators, and Interactions -- A modest proposal for a toned done version of DCI that isn't as janky as Concerns.

Models, Roles, Decorators, and Interactions

A modest alternative to DCI that might be worth further thought

One of the problems with advancing the discussion on DCI is that we lack a comparable alternative pattern that has the same goals, but favors a low ceremony approach. The closest thing we have to that is Rails concerns, but they are more like distant relatives of the DCI concepts rather than first cousins, and that makes comparisions between the two approaches not especially fruitful.

I am considering the idea of experimenting with my own paradigm that captures the intent and purity of DCI, but with the convenience of concerns. Please note that this is just the starting point of a conversation, it is NOT a promise of comercially available cold fusion or a cure for cancer. It's just a gist with an idea on it I'd like to hear your thoughts on.

What if we had a top-level topology that was split into Models, Roles, Decorators, and Interactions?

Models

A model is responsible for data access and persistence. It does not have logic of its own, apart from validations, and basic transformations that make its internal state more palatable for some other part of a system. Bonus points if these models are immutable, but it's not essential as long as consistency and safe access is maintained.

So for example, we might have a Person and Comment model, as shown below:

class Person < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :author, :class_name => "Person"
end

We might have some simple methods on these objects that make queries easier, but they wouldn't have any business logic in them. Models can be made up of other models via object composition, as long as those constraints were mainted.

Roles

A role is an object (potentially composite) that understands how to take specific actions that manipulate models, either directly or indirectly. It's not necessarily stateless, but its job is to collaborate with other objects, not to encapsulate data of its own. In that sense, a role is a transient thing that is meant to be constructed dynamically to complete an action, and then thrown out.

So in that sense, we might havve a Commenter role:

class Commenter
  def initialize(person)
    @person = person
  end
  
  def recent_comments
    @person.comments.where("created_at > ?", 3.days.ago)
  end
  
  def post_comment(text)
    @person.comments.create(:body => text)
  end
  
  def update_comment(new_text)
    raise "Comment not owned by this person" unless comment.author == @person
    
    comment.update_attribute(:body, new_text)
  end
end

This is a plain Ruby object that could perhaps live in app/roles. Like models, you can certainly build composite roles as needed, as long as their purpose is maintained.

Decorators

A decorator is an object that takes a model and transforms it into something suitable for consumption by some other part of the system. This is used sometimes for presentation purposes, other times it is used for adapting data so that it be passed from one kind of API to another. In any case, it is a simple transformation of a model into a different representation: nothing more, nothing less.

So for this example, we might have something like this:

class CommentDecorator
  def initialize(comment)
    @comment = comment
  end
  
  def to_json
    {:author => comment.author.name, 
     :body   => comment.body,
     :date   => comment.created_at }.to_json 
  end
  
  def to_html
    # use your imagination here
  end
end

Again, composition is fine here: CommentDecorator could be a trivial facade that rests on top of lots of specific decorators for individual formats, if it'd help make it more modular and maintainable.

Interactions

Here's where things get sticky. An interaction is not something that can be succinctly expressed in a general way, because it is by definition the code that forms the glue between the delivery mechanism (e.g. the Rails framework + the user's browser) and the application itself (the thing that understands the concept of a person, comment, and commenter). Because a generalization of interactions would be so abstract it would hardly mean anything at all, we must rely on common sense here. Complex interactions could be wrapped up in a network of objects, but we could work towards keeping interactions strictly procedural by pushing most of our substantial code into the Models, Decorators, and Roles.

That said, we could probably come up with plenty of sensible tactics and strategies for dealing with this part of our system, as long as we don't try to come up with the Grand Unified Theory of everything. By knowing what kind of application you want to build, and what kind of delivery mechanism you are using, it would probably be possible to build gems or libraries that help make certain kinds of interactions easier. However, rather than formalizing this concept, I'd prefer to leave it adhoc for now.

Sugar

I personally like writing explicit class definitions that are plain old Ruby. But we could probably justify sprucing up these constructs within the context of particular frameworks. So I'd have no problem with some rails magic that'd give us something like this for roles and decorators:

class Commenter < ActiveSupport::Role
  concerns :person # this is a reuse of the word only, not the semantics.
  
  def recent_comments
    person.comments.where("created_at > ?", 3.days.ago)
  end
  
  # ...
end

# usage
Commenter.new(some_person)

class CommentDecorator < ActiveSupport::Decorator
  decorates :comment
  
  # the view method and maybe a few other small helpers provided by decorator class
  
  def to_html
    view.content_tag(:p, comment.body)
  end
end

# usage
CommentDecorator.new(comment).to_html

I could care less whether this sugar used class inheritance or mixins, or if by some magic could be supported by composition (perhaps with a callback mechanism?). It's just an olive branch for those who love their sexy magic, but with the assumption that we can build it on top of a solid foundation.

What do you think?

It's just an idea, and since I just thought of it now, my code examples are as thin as the very DCI articles I've been critical of. I do plan to try out these ideas in a full scale application, but I'd like to get a sanity check on them first. They're not so much anything special or novel, they're just a collection of existing practices that I'm trying to group together so that we can discuss their interrelations. I'd love to hear what you think!

@lwoodson
Copy link

Part of my complaint with DCI is that I think its naming and concepts are overly abstract, probably even poorly chosen and open to all sorts of misinterpretation. 'Context' ooohkay, whats a context? A business context, usually for carrying out a one or more business operations spanning multiple models. Okay, why not call it business operation or operation or services (http://martinfowler.com/eaaCatalog/serviceLayer.html)? 'Data', hmm, okay, sounds like a model. Why not call it model? Was there something in MVC that required models to be fat so that we need a different term to describe a skinny model? Not sure, okay, whatever. "Interaction", uh, yeah. Whats that? I'm not sure I know even now how to explain it. Lets say its the act of changing the way that a model may be interacted with in a given context. So a context-dictated change in the interface of an object. But I've yet to see any examples of such a change outside of the concept of a role and I can't really think of any. This company is a purchaser in this scenario and a seller in this scenario. A person is a felon in this scenario or a cop in this scenario. Why not simply call these roles?

In that regard, I like the spirit of your suggestion. Not sure if I agree with how all of that is accomplished.
Me personally, I wish it was called MRO or MRS (Model - Role - Operations|Services) and that as far as structuring within a rails project, that there were app/roles and app/operations or app/services directories.

@mech
Copy link

mech commented Jan 16, 2013

I think both has its place. Concern is good for use-cases like Taggable where every models has shared and similarly "change-together" piece of code.

When you comes to slightly complex business logic (which if you know business, its rules are ad-hoc and not tied down), you can use the DCI context to have its own isolated role-playing goodness.

Both has its place I feel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment