Abstract Method in Ruby

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



Leave a reply



Submit