4 Simple Steps - Extending Ruby Objects - The Tip of the Iceberg with DCI

by Jim

You’ve got a few Rails applications under your belt, perhaps several. But something is wrong. As you go, your development slows and your classes become bloated and harder to understand.

Keep your program simple

While you’re doing your best to follow the concept of keeping your controllers skinny and your models fat, your models are getting really fat.

The answer is simple: cut the fat.

Your models don’t need to have every method they will ever need defined in the class. The reality is that the objects that your application handles only need those methods when they need them and not at any other time. Read that again if you must, because it’s true.

Step 1: Evaluate your code and look for a place to separate concerns

This is the point where you look at your code and try to realize what object needs what and when.

For example, if users of your system need to approve a friend request they only need to do it in the context of viewing that request. Your User class or @current_user object doesn’t need this ability at any other time.

Step 2: Prepare your test suite

If you want to make your code simpler and easier to understand, write tests. You must do this first.

Even if you’re only intending to change one small thing (just one tiny piece), write a test for that. You need a baseline.

Step 3: Create the Object Roles

Take your friend approval (or whatever it is) method or methods and put them in a module.

You might want to drop this into some namespace such as ObjectRole::FriendApprover or if you know your name won’t clash with anything else, just go with FriendApprover.

Here’s a sample of what this might look like:

module FriendApprover
  def approve(friend_request)
    friend_request.approved = true

  def increment_friends
    friend_count += 1

  def notify_new_buddy(buddy_id)
    BuddyMailer.notify_buddy(buddy_id, "We're officially friends!")

It doesn’t really matter what my sample code is, you get the picture: take the methods from your User class that do the approval and put them in your FriendApprover module.

The unit tests you had for these methods can now be simplified and applied to the module. The test just needs to check that some object agrees to the contract that the methods expect.

Step 4: Extend your user

Extend your user. Thats little “u” user. Your class doesn’t need this module, your object does.

Open up your controller where you usually call current_user.approve(friend_request) and change it to:

current_user.extend FriendApprover

That’s it.

What you’ve just done

You’ve made your code more obvious.

It’s only in this context that a user needs to perform this action and this change has limited the scope of those methods to a very concrete area.

But what about…

Yes, there’s more to it. Of course there’s more you can do, but with this simple concept you can do a lot of cleanup of both your code, and your ability to reason about your code.

What is DCI?

For now, I’ll leave the description of what DCI is to this article but I’ll be writing more about the concepts in Data Context and Interaction.

Clean Up Your Code and get a Free chapter of Clean Ruby

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

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.


trans said on Thursday, September 29, 2011:

Technically, for DCI as I understand it, the FriendApprover should be "unextended" when finished being used. Of course we can't do that in Ruby and it would probably pretty inefficient even if we could.

Rather than use mixins, I might be better to use delegation.

Jim Gay said on Thursday, September 29, 2011:

@trans That's true. I didn't mention it here, but this assumes that you're working with something like Rails where the objects are thrown away after the request so there's no need to unmix anything.

Kris said on Thursday, September 29, 2011:

The FriendApprover module would also work well as a separate class which accepted two user id's and befriended them.

Jim Gay said on Thursday, September 29, 2011:

@Kris true, the FriendApprover could be another class, but in reality there is no friend approver for your relationships. If someone wants access to your information then you give them access.

By using a module the current_user is the one doing the approval, and this is really the only object that should.

trans said on Friday, September 30, 2011:

Another thought. Are DCI and Jim Weirich's GUToSD at odds? (http://vimeo.com/10837903)

Ben Crouse said on Friday, September 30, 2011:

Have you run any performance test with this? Any bad affect on Ruby's internal method lookup/caching?

Jim Gay said on Friday, September 30, 2011:

@Ben, I've not run performance tests on this. As far as I understand, the VM will clear the method cache any time this happens.
At first glance, that may seem expensive, but I'm going to do more research on it. With well separated code, the impact of that goes down since we wouldn't be relying on most of those methods to be there anyway.
I'll write more about this.

Jim Gay said on Friday, September 30, 2011:

@trans, I haven't seen that video. I'll take a look.

David Kovsky said on Friday, September 30, 2011:

I like the idea of extracting related methods into modules. What bothers me here is that the controller has to know to extend current_user with FriendApprover before calling current_user.approve. I don't see the case for this JIT object extension yet.

It seems to me the controllers would quickly become a hotbed of module knowledge. And model-level tests wouldn't catch controllers forgetting to extend a module on an object, so you'd need thicker coverage at higher levels.

Jim Gay said on Saturday, October 01, 2011:

@David, you're exactly right. This is where the Context of DCI will come in and I'll be writing about that in the future. This post is only to push the idea.

Mike Gehard said on Saturday, October 01, 2011:

I am interested to see the future posts about module extension vs a different class to handle this.

I'm with @David about the controllers gaining knowledge of object decoration but maybe I'll be converted once I digest this more.

Thanks for starting the discussion.

Dan Hensgen said on Monday, October 03, 2011:

It's going to take a few reads to digest that DCI article. I'm intrigued though.

In your example, why does user extend FriendApprover? I didn't see anything in FriendApprover that needed access to user.

Jim Gay said on Monday, October 03, 2011:

@Dan, fair enough, but don't worry too much about my sample code. I've added a more explicit method to be called on the user, but answer this: what other thing would approve this friend request? Only the user cares.

Dan Hensgen said on Monday, October 03, 2011:

I'm not quite sold yet :). But I am going to give the DCI article another read. OO dogma can make it difficult to be pragmatic sometimes and this article addresses that to some degree.

Now if only I could find a Scala consultant or workshop…

Enrico Teotti said on Thursday, October 06, 2011:

I haven't read the DCI architecture page, but from the example you posted my question is what's the difference between this and pure object decoration (done on the class)?

The advantage I see is if you define the context you can avoid method duplication, but that seems like a bit of an edge case.

If you are plugging in functionality only when needed, I think it would be hard to idenify the context ie. we add a "has_a_lot_of_friends?" method to current_user in the friends#show, but what if we have in another controller an action showing all the users and telling sho has a lot of friends? You need to extend again?

Marc Grue said on Wednesday, October 19, 2011:

Check out also:

Find more in the archives

1999 - 2016 © Saturn Flyer LLC

Call Jim Gay at 571 403 0338