Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails - Best practice for abstract class definition and file naming

I want to define 3 classes:

  • a MotherClass (abstract, can not be inferred)
  • a SubClassA (inherits from MotherClass)
  • a SubClassB (inherits from MotherClass)

What is the best solution to declare it in Rails ?

1. Put everything in app/models/

  • MotherClass < AR::Base in app/models/mother_class.rb
  • SubClassA < MotherClass in app_models/sub_class_a.rb
  • SubClassB < MotherClass in app/models/sub_class_b.rb

Advantage: not very complicated to implement

Inconvenient: a big mess in models folder

2. Create a module for the two subclasses

  • MotherClass < AR::Base in app/models/mother_class.rb
  • MotherModule::SubClassA < MotherClass in app/models/mother_module/sub_class_a.rb
  • MotherModule::SubClassB < MotherClass in app/models/mother_module/sub_class_b.rb

Advantage: same than Solution 1

Inconvenient: naming MotherModule and MotherClass with different names, but they mean almost the same thing

3. Create a module for the 3 classes

  • MotherModule::Base < AR::Base in app/models/mother_module/base.rb
  • MotherModule::SubClassA < MotherModule::Base in app/models/mother_module/sub_class_a.rb
  • MotherModule::SubClassB < MotherModule::Base in app/models/mother_module/sub_class_b.rb

Advantage: very clean

Inconvenient: need some functions in Base to override (table_name for example)


So my question is: What is the best practice in Rails and - how to name my classes? - what are their directories?

like image 296
pierallard Avatar asked Jun 25 '14 07:06

pierallard


2 Answers

First of all, I think you must already realize that ruby does not have true abstract classes. But we can approximate the behavior. And while doing so, it sounds like you have a preference toward organizational structure which I will attempt to address.

I must start by saying, however, that I'm surprised that you're coming at the problem so strongly from the organizational angle. First on my mind would be whether I really wanted to implement single table inheritance or not and then let that drive the organizational problem. Usually the answer here is that Single Table Inheritance is not what you actually want. But... let's dive in!

Using Single Table Inheritance

Here's the standard way to utilize and organize models using Single Table Inheritance:

# app/models/mother_class.rb
class MotherClass < ActiveRecord::Base
  # An "abstract" method
  def method1
    raise NotImplementedError, "Subclasses must define `method1`."
  end

  def method2
    puts method1 # raises NotImplementedError if `method1` is not redefined by a subclass
  end
end

# app/models/sub_class_a.rb
class SubClassA < MotherClass
  def method1
    # do something
  end
end

# app/models/sub_class_b.rb
class SubClassB < MotherClass
  def method1
    # do something
  end
end

Given the above, we would get an exception when calling MotherClass.new.method2 but not when calling SubClassA.new.method2 or SubClassB.new.method2. So we've satisfied the "abstract" requirements. Organizationally, you called this a big mess in the models folder... which I can understand if you've got tons of these subclasses or something. But, remember that in single table inheritance even then parent class is a model and is / should be usable as such! So, that said, if you'd really like to organize your models file system better then you are free to do so. For example, you could do:

  • app/models/<some_organizational_name>/mother_class.rb
  • app/models/<some_organizational_name>/sub_class_a.rb
  • app/models/<some_organizational_name>/sub_class_b.rb

In this, we are keeping all other things (i.e. the Code for each of these models) the same. We're not namespacing these models in any way, we're just organizing them. To make this work it's just a matter of helping Rails to find the models now that we've placed them in a subfolder of the models folder without any other clues (i.e. without namespacing them). Please refer to this other Stack Overflow post for this. But, in short, you simply need to add the following to your config/application.rb file:

config.autoload_paths += Dir[Rails.root.join('app', 'models', '{**/}')]

Using Mixins

If you decide that Single Table Inheritance is not what you want (and they often aren't, really) then mixins can give you the same quasi-abstract functionality. And you can, again, be flexible on file organization. The common, organizational pattern for mixins is this:

# app/models/concerns/mother_module.rb
module MotherModule
  extend ActiveSupport::Concern

  # An "abstract" method
  def method1
    raise NotImplementedError, "Subclasses must define `method1`."
  end

  def method2
    puts method1 # raises NotImplementedError if `method1` is not redefined
  end
end

# app/models/sub_class_a.rb
class SubClassA
  include MotherModule

  def method1
    # do something
  end
end

# app/models/sub_class_b.rb
class SubClassB
  include MotherModule

  def method1
    # do something
  end
end

With this approach, we continue to not get an exception when calling SubClassA.new.method2 or SubClassB.new.method2 because we've overridden these methods in the "subclasses". And since we can't really call MotherModule#method1 directly it is certainly an abstract method.

In the above organization, we've tucked MotherModule away into the models/concerns folder. This is the common location for mixins in Rails these days. You didn't mention what rails version you're on, so if you don't already have a models/concerns folder you'll want to make one and then make rails autoload models from there. This would, again, be done in config/application.rb with the following line:

config.autoload_paths += Dir[Rails.root.join('app', 'concerns', '{**/}')]

The organization with the mixins approach is, in my opinion, simple and clear in that SubclassA and SubClassB are (obviously) models and, since they include the MotherModule concern they get the behaviors of MotherModule. If you wanted to group the subclass models, organizationally, into a folder then you could still do this of course. Just use the same approach outlined at the end of the Single Table Inheritance section, above. But I'd probably keep MotherModule located in the models/concerns folder still.

like image 174
pdobb Avatar answered Oct 08 '22 19:10

pdobb


Even though ruby doesn't really have abstract classes, it's powerful enough to let you implement it yourself by implementing self.included on a mixin module. Hopefully this generic example gives you enough to go on for your particular implementation.

module MotherInterface

  def self.included base
    required_class_methods = [:method1, :method2]
    required_instance_methods = [:fizzle, :fazzle]
    required_associations = [:traits, :whatevers]

    required_class_methods.each do |cm|
      raise "MotherInterface: please define .#{cm} class method on host class #{base.name}" unless base.respond_to?(cm)
    end

    required_instance_methods.each do |im|
      raise "MotherInterface: please define ##{im} instance method on host class #{base.name}" unless base.instance_methods.include?(im)
    end

    required_associations.each do |ass|
      raise "MotherInterface: please be sure #{base.name} has a :#{ass} association" unless base.reflections.key?(ass)
    end

    base.send :include, InstanceMethods
    base.extend ClassMethods
  end

  # inherited instance methods
  module InstanceMethods
    def foo
    end

    def bar
    end
  end

  # inherited class methods
  module ClassMethods
    def baz
    end

    def bat
    end
  end

end


class SubClassA < ActiveRecord::Base
  include MotherInterface
  # ... define required methods here ...
end

class SubClassB < ActiveRecord::Base
  include MotherInterface
end

Some advantages to this approach are:

  1. Yes, you can still technically instantiate the mixin, but it's not actually tied to active record, so it tastes more like an abstract class.
  2. The sub classes get to define their own connection information. You have two databases? Differing columns? Cool, no problem. Just implement your instance methods and stuff appropriately.
  3. The dividing line between parent and child is very obvious.

But, there are disadvantages too:

  1. All the meta programming is a bit more complex. You'll have to think a little abstractly (HA!) about how to organize your code.

There are probably other advantages and disadvantages I haven't considered, kind of in a hurry here.

Now, as far as file locations, I would suggest that the mixin itself, presumably mother_interface.rb, go someplace other than your models folder.

In config/application.rb, throw in a line like this:

config.autoload_paths << File.join(Rails.root, 'app', 'lib')

...and then you can create (rails)/app/lib/mother_interface.rb. Really, you should do it however makes sense to you. I dislike the word "concerns" for this, and other people dislike the word "lib." So, use whatever word you like, or make up your own.

like image 36
rude-n8 Avatar answered Oct 08 '22 19:10

rude-n8