开发者

Whats a good ruby idiom for breaking up a large class into modules?

I have a large class with lots of methods and it's starting to get a bit unorganized and hard to navigate. I'd like to break it up into modules, where each module is a collection of class and instance methods. Perhaps something like this:

UPDATE: I've now realized that this is a pretty poor example. You probably wouldn't want to move validations or attributes out of the core class.

class Large
  include Validations
  include Attributes
  i开发者_如何学Cnclude BusinessLogic
  include Callbacks
end

After reading Yehuda's post about Better Ruby Idioms, I'm curious how others are tackling this problem. Here's the two methods I can think of.

First Method

module Foo
  module Validations
    module ClassMethods
      def bar
        "bar"
      end
    end

    module InstanceMethods
      def baz
        "baz"
      end
    end
  end

  class Large
    extend Validations::ClassMethods
    include Validations::InstanceMethods
  end
end

Second Method

module Foo
  module Validations
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def bar
        "bar"
      end
    end

    def baz
      "baz"
    end
  end

  class Base
    include Validations
  end
end

My questions are:

  • Is there a better way to do this?
  • How do you get a one-liner module mixin for a set of class/instance methods with the least amount of magic?
  • How do you namespace these modules to the base class without namespacing the class itself?
  • How do you organize these files?


Breaking a class into modules, while tempting (because it's so easy in Ruby), is rarely the right answer. I usually regard the temptation to break out modules as the code's way of telling me it wants to be split into more tightly-focussed classes. A class that's so big you want to break it into multiple files is pretty much guaranteed to be violating the Single Responsibility Principle.

EDIT: To elaborate a bit on why breaking code into modules is a bad idea: it's confusing to the reader/maintainer. A class should represent a single tightly-focussed concept. It's bad enough when you have to scroll hundreds of lines to find the definition of an instance method used at the other end of a long class file. It's even worse when you come across an instance method call and have to go looking in another file for it.


After doing what Avdi said, these are the things I would do before putting anything into a module:

  1. Whether this module can or will be used in any other class?
  2. Would it make sense to extract the functionality of these modules into a different or base class?

If the answer for 1 is no and 2 is yes then IMHO that indicates to better have a class rather a module.

Also, I think putting attributes in a module is conceptually wrong because classes never share their attributes or instance variables or in other words their internal state with any other class. The attributes of a class belongs to that class only.

Business logics do definitely belong to the class itself and if the business logic of class A has some common responsibilities with class C then that needs to be extracted into a base class to make it clear instead of just putting it into a module.


The standard idiom seems to be

foo.rb
foo/base.rb
foo/validations.rb
foo/network.rb
foo/bar.rb

and foo.rb would be something like

class Foo
  include Foo::Base
  include Foo::Validations
  include Foo::Network
  include Foo::Bar
end

This is the standard idiom, and it works fairly well for letting you break things up. Don't do class methods vs instance methods. Those are generally pretty arbitrary distinctions, and you're better off putting code that deals with similar subjects together. That will minimize how many files you have to touch for any given change.

BEWARE: Rails can get confused by nesting models like this, at least if everything were classes. I think it'll do better with all the nested files just being modules, but you'll have to see. I'm still suggesting this because it's the normal idiom used by the Ruby community, but you may have to avoid having both a foo.rb and a foo/ directory amongst your Rails models (if that's the kind of class you're talking about).


Although including different modules will work, it is generally more troublesome than simply reopening the class in multiple places.

There is a (very simple) gem that you can use to makes this as pretty as can be: concerned_with

Example (from the readme)

# app/models/user.rb
class User < ActiveRecord::Base
  concerned_with :validations,
                 :authentication
end

# app/models/user/validations.rb
class User < ActiveRecord::Base
  validates_presence_of :name
end

#app/models/user/authentication.rb
class User < ActiveRecord::Base
  def self.authenticate(name, password)
    find_by_name_and_password(name, password)
  end
end


I tend to use Ruby's duck typing approach to interfaces, which basically allows you to send any message to any object, which then evaluates what to do with it.

This approach allows me to stick to the same pattern Avdi mentions, keeping classes small and concise- only ever being responsible for one thing. The great thing about Ruby is that you can delegate responsibilities to other concise classes, without muddling any of the logic together. For example:

class Dog
  def initialize(name)
    @name = name
  end

  def bark  
    "woof"
  end

  def fetch(object)
    "here's that #{object}"
  end

  def sit
    "sitting down"
  end

  private
  attr_accessor :name
end

Here we have my dog class that has loads of dog related methods. They're all specific to dog, so could happily reside here. However, there would be a problem if these methods got a bit complex, calling other methods or perhaps this dog learns a bunch of new tricks!? So I could separate these out into their own classes and then delegate responsibility to those, like so:

class Tricks
  def initialize(name)
    @name = name
  end

  def fetch(object)
    "here's that #{object}"
  end

  def sit
    "sitting down"
  end

  def come_when_called(my_name)
    "I'm coming" if my_name == name
  end

  def put_toy_away(object)
    "#{fetch(object)}, I'll put it away"
  end

  private 
  attr_reader :name
end

class Dog
  def initialize(name)
    @name = name
  end

  delegate :sit, :fetch, :come_when_called, :put_away_toy, to: :tricks_klass

  def bark  
    "woof"
  end

  private
  attr_accessor :name

  def tricks_klass
    @tricks_klass ||= Tricks.new(name)
  end
end

So now, that Dog class really starts to behave like an interface to dog-related behaviors, whilst these tricks are no longer coupled to it. This'll make testing easier by being able to instantiate a Tricks object and test it more generically, without the need for a Dog (because they don't always listen).

Now, we could have a Cat class that delegates responsibility to this Tricks class as well- although, that'd be one smart Cat!

You could also now use the Tricks class on its own- that's the power encapsulating single behavior its own class. You could even separate these behaviors even further- but only you as the developer know if that's worth while!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜