Abstract Method in Ruby
Abstract methods are supposed to be less useful in Ruby because it's not strongly statically typed.
However, this is what I do:
class AbstractThing
MESS = "SYSTEM ERROR: method missing"
def method_one; raise MESS; end
def method_two; raise MESS; end
end
class ConcreteThing < AbstractThing
def method_one
puts "hi"
end
end
a = ConcreteThing.new
a.method_two # -> raises error.
It rarely seems to be necessary, however.
How to implement an abstract class in Ruby
I don't like using abstract classes in Ruby (there's almost always a better way). If you really think it's the best technique for the situation though, you can use the following snippet to be more declarative about which methods are abstract:
module Abstract
def abstract_methods(*args)
args.each do |name|
class_eval(<<-END, __FILE__, __LINE__)
def #{name}(*args)
raise NotImplementedError.new("You must implement #{name}.")
end
END
# important that this END is capitalized, since it marks the end of <<-END
end
end
end
require 'rubygems'
require 'rspec'
describe "abstract methods" do
before(:each) do
@klass = Class.new do
extend Abstract
abstract_methods :foo, :bar
end
end
it "raises NoMethodError" do
proc {
@klass.new.foo
}.should raise_error(NoMethodError)
end
it "can be overridden" do
subclass = Class.new(@klass) do
def foo
:overridden
end
end
subclass.new.foo.should == :overridden
end
end
Basically, you just call abstract_methods
with the list of methods that are abstract, and when they get called by an instance of the abstract class, a NotImplementedError
exception will be raised.
Using sorbet interface abstraction on singleton methods
I think you want to extend VersionedStruct
instead of trying to do the magic mixes in class methods trick. That works really well:
# typed: true
module VersionedStruct
extend T::Sig
extend T::Helpers
abstract!
sig { abstract.returns(Integer) }
def version; end
sig { abstract.params(payload: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def migrate(payload); end
end
class MyStruct < T::Struct
extend T::Sig
extend VersionedStruct
const :v1_field, String
const :v2_field, String
sig { override.returns(Integer) }
def self.version
2
end
sig { override.params(data: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def self.migrate(data)
return {} if data[:v2_field]
data.merge(v2_field: "default value")
end
end
Alternatives to abstract classes in Ruby?
Ruby does not offer this functionality, no. You are responsible for making sure that your classes implement what they ought to implement.
Part of the reason that such functionality is impossible for Ruby is that Ruby classes can be reopened, and Ruby supports loading arbitrary code at runtime, so we can't know whether a class implements a certain interface until we try to call it.
Supposing an Animal
must have an eat
method, and I do the following:
class Cat < Animal
def talk
puts "meow"
end
end
class Cat
def eat
puts "om nom nom"
end
end
By the end of that file, the Cat
will have its eat
definition, because Ruby classes can reopened and modified multiple times. Should the code error out after the first definition because eat
wasn't defined yet? That implementation would hurt more than it would help, since reopening classes is common, even if this example is contrived. Should it error out once the eat
method is called and does not exist, so we can be certain that it's defined once we need it? Well, if the method were missing, that would happen, anyway. The interpreter can never know if another class definition is on the way, so it can never cut you off until the method is actually called.
In short, superclasses simply cannot possibly require a method to be defined in Ruby, because the dynamic nature of classes contradict such a goal.
Sorry! This is a place where unit testing might come in handy, though, to ensure that your subclasses do what they're supposed to be doing, anyway.
Rails - Best practice for abstract class definition and file naming
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.
Abstract class vs Concrete class?
If you have an object which mustn't get instantiated you should use an abstract class. A concrete class, though, should be used if you want to instantiate this class.
Imagine you want several animal classes. But the rule is just to instantiate specific animals like a dog, cat or a mouse. But now all of those animals share some properties and methods - like a name. So you put them in a base class Animal
to avoid code duplication. You can't create an instance of Animal
though:
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
// sleep
}
}
public class Cat : Animal
{
public void Meow()
{
// meooooow
}
}
public void DoSomething()
{
var animal = new Animal(); // illegal operation
var cat = new Cat(); // works fine
}
Integer and other (?) abstract classes in Ruby
You can undefine the new
class method calling undef_method
on the singleton class of your class, for example:
class Foo
class << self
undef_method(:new)
end
end
Foo.new
# NoMethodError: undefined method `new' for Foo:Class
Or alternatively:
class Foo; end
Foo.singleton_class.send(:undef_method, :new)
Update: for the sake of completeness here's a quick (and possibly dirty) solution to the problem mentioned by @tadman in the comments. It uses the inherited
method to define a new
singleton method for the subclasses of Foo
that mimics the implementation of Class#new
:
class Foo
class << self
undef_method(:new)
def inherited(subclass)
subclass.define_singleton_method(:new) do |*args|
obj = subclass.allocate
obj.send(:initialize, *args)
obj
end
end
end
end
class Bar < Foo
attr_reader :bar
def initialize(bar)
@bar = bar
end
end
Bar.new('foobar').bar # => "foobar"
Related Topics
How to List All Objects Created from a Class in Ruby
Rails App: Solr Throwing Rsolr::Error::Http - 404 Not Found When Executing Search
Ruby Gem Not Found Although It Is Installed
What Does :: (Double Colon) Mean in Ruby
Activerecord Select Except Columns
Search Multiple Models at Once with Ransack
Is Inject the Same Thing as Reduce in Ruby
I Have a Gem Installed But Require 'Gemname' Does Not Work. Why
Put Haml Tags Inside Link_To Helper
How to Check Whether a Value in a String Is an Ip Address
Repairing Postgresql After Upgrade to Osx Mavericks
Can't Install Thrift Gem on Os X El Capitan
Why Is 'Return a or B' a Void Value Expression Error in Ruby
How to or Should I Find an Object by the Object_Id Attribute in Ruby