开发者

Simulating abstract classes in Ruby (Rails)

I want to simulate an abstract class in开发者_开发百科 Ruby on Rails. I.e. I want to raise an exception if someone tries to call Abstract.new, but he should be able to call Child.new (while Child < Abstract).

How to do this? Overwriting both new and initialize does not work.


In another comment, the OP mentions that the purpose of the abstract class is to share behavior (methods) needed by its children. In Ruby, that's often best done with a module used to "mix in" methods where needed. For example, instead of:

class Abstract
  def foo
    puts "foo!"
  end
end

class Concrete
end

Concrete.new.foo # => "foo!"

this:

module Foo
  def foo
    puts "foo!"
  end
end

class Concrete
  include Foo
end

Concrete.new.foo # => "foo!"

But here's how the original request might be satisfied:

#!/usr/bin/ruby1.8

class Abstract

  def initialize(*args)
    raise if self.class == Abstract
    super
  end

end

class Concrete < Abstract
end

Concrete.new # OK
Abstract.new # Raises an exception


Why would you want to do this? The point of abstract/interfaced classes are to hack Strongly typed languages into a dynamic paradigm. If you need your class to fit in the signature, name your methods according to the original class or make a facade and plug it in, no need to trick a compiler into allowing it, it just works.

def my_printer obj
  p obj.name
end

So I defined the interface as any object with a name property

class person
  attr_accessor :name
  def initialize
   @name = "Person"
  end
end

class Employee
   attr_accessor :name
  def initialize
   @name = "Employee"
   @wage = 23
  end
end

so nothing stops us from calling our printer method with either of these

my_printer Person.new
my_printer Employee.new

both print there names without a hitch :D


You almost always need to do this to enforce an API, when some third party is going to implement some stub, and you're sure they're going to mess it up. You can use specific prefix-templates in your parent class and a module that introspects on creation to achieve this:

module Abstract
  def check
    local = self.methods - Object.methods
    templates = []
    methods = []
    local.each do |l|
      if l =~ /abstract_(.*)/ # <--- Notice we look for abstract_* methods to bind to.
        templates.push $1
      end
      methods.push l.to_s
    end
    if !((templates & methods) == templates)
      raise "Class #{self.class.name} does not implement the required interface #{templates}"
    end
  end
end

class AbstractParent 
  include Abstract
  def initialize
    check
  end
  def abstract_call # <--- One abstract method here
  end
  def normal_call
  end
end

class Child < AbstractParent # <-- Bad child, no implementation
end

class GoodChild < AbstractParent
  def call # <-- Good child, has an implementation
  end
end

Test:

begin
  AbstractParent.new
  puts "Created AbstractParent"
rescue Exception => e
  puts "Unable to create AbstractParent"
  puts e.message
end

puts

begin
  Child.new
  puts "Created Child"
rescue Exception => e
  puts "Unable to create Child"
  puts e.message
end

puts

begin
  GoodChild.new
  puts "Created GoodChild"
rescue Exception => e
  puts "Unable to create GoodChild"
  puts e.message
end

Result:

[~] ruby junk.rb 
Unable to create AbstractParent
Class AbstractParent does not implement the required interface ["call"]

Unable to create Child
Class Child does not implement the required interface ["call"]

Created GoodChild


If you want this for doing STI, you could follow the suggestions in this thread:

class Periodical < ActiveRecord::Base
  private_class_method :new, :allocate
  validates_presence_of :type
end

class Book < Periodical
  public_class_method :new, :allocate
end

class Magazine < Periodical
  public_class_method :new, :allocate
end

Caveat: I'm not sure if this is a working solution. This hides new and allocate in the base class and re-enables them in child classes -- but that alone does not seem to prevent objects being created with create!. Adding the validation on type prevents the base class from being created. I guess you could also hide create!, but I'm not sure if that covers all the ways Rails can instantiate a model object.

0

上一篇:

下一篇:

精彩评论

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

最新问答

问答排行榜