sam
sam

Reputation: 1161

Ruby/Rails - force subclasses to override specific methods?

I'm wondering if there is a way to force a subclass to override a method from its parent method in either Ruby or Rails (in Java you would do this with an abstract class/method).

Let's say I have the following classes:

class Pet
end

class Dog < Pet
  def collar_color
    "red"
  end
end

class Cat < Pet
end

dog = Dog.new
dog.collar_color
==> red

cat = Cat.new
cat.collar_color
==> NoMethodError

In this example I would never instantiate a Pet object, but it exists to serve as a way to collect common methods to common classes. But let's say I want to ensure that all subclasses of Pet override the collar_color method. Is there a way to do that? Could I achieve it through testing in some way? Assume I don't want a default defined in the parent class.

My real-life use case is a collection of classes that all have polymorphic ownership of another class. If I have a display page of the owned class, then one of the owner classes not having a method could leave me with a NoMethodError problem.

Upvotes: 2

Views: 1561

Answers (3)

max
max

Reputation: 102222

Ruby has relatively few keywords but it provides the basic building blocks to implement something that vaguely resembles abstract classes or methods.

In its simplest form you just raise an error in the parent "abstract" method:

class AbstractMethodError < StandardError
  def initialize(klass, m)
    super("Expected #{klass} to implement #{m}")
  end
end

class Pet
  def collar_color
    raise AbstractMethodError.new(self.class, __method__)
  end
end

class Cat < Pet
  
end

Cat.new.collar_color # Expected Cat to implement collar_color (AbstractMethodError)

__method__ is a magic variable that contains the name of the current method.

You can make this a bit more elegant by creating a class method that defines the "abstract method":

module Abstractions
  def abstract_method(name)
    define_method(name) do
      raise AbstractMethodError.new(self.class, __method__)
    end
  end
end

class Pet
  extend Abstractions
  abstract_method :collar_color
end

However Ruby is a dynamic langauge and you don't have a compile time check so this will only give a slightly more obvious error message when the method is called. It doesn't actually give any guarentees that subclasses implement the method.

That is down to testing or using type checkers like Sorbet or RBS. In general it might be helpful when learning to forget everything you think you know about Object Oriented Programming and learn the Ruby way. It has a very different design philophy compared to Java - instead of abstract methods and interfaces you use duck typing to see if the object responds to that method.

Upvotes: 3

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369536

No, there is no way to enforce this.

I can guarantee you, whatever idea you can come up with, it will break in some way.

First off: doing this statically is out of the question. Determining whether a method is overridden or not is known to be equivalent to solving the Halting Problem.

So, you have to do it dynamically. But even that is going to be problematic.

For example: you could implement the inherited hook and check whether every class that inherits from Pet implements the method. But, that will prevent someone from inheriting their own abstract class. (Also, there is no guarantee when the inherited hook will run – it could run when the class is opened, i.e. before the methods are defined.)

Also, even if you can check that the method exists at the point where a class inherits Pet, the method can still be removed again later, so you don't get any guarantees. And, of course, they can just provide a dummy method, in order to get around your protection.

You could create default implementations of the methods that just raise an Exception, but there is no need to do that: if you don't create a default implementation, that will already raise a NoMethodError exception anyway. (If you do go down this route, do not use NotImplementedError. Instead, use a custom exception that inherits from RuntimeError.)

There are examples of this in the core library: for example, the Enumerable mixin depends on an abstract method each that must be implemented by subclasses. And the way this is handled is by simply documenting that fact:

Usage

To use module Enumerable in a collection class:

  • Include it:
    include Enumerable
    
  • Implement method #each which must yield successive elements of the collection. The method will be called by almost any Enumerable method.

That is actually the way any type-related issues have been dealt with in Ruby since the beginning. Since Ruby does not have types, typing only happens in the programmer's head and type information is only written down in documentation.

There always were informal third-party type annotation languages that were used by various IDEs. More recently, two type annotation languages have been introduced: RBI, a third-party type annotation language used by the Sorbet type checker, and RBS, a type annotation language that is part of Ruby proper.

As far as I know, RBS has no way of expressing abstract methods, but RBI does:

class Pet
  extend T::Sig
  extend T::Helpers
  interface!

  sig {abstract.returns(String)}
  def collar_color; end
end

This will give you a type error if there is object instantiated from a subclass that does not at some point in the inheritance chain define the method. But, of course, only if the user of the code actually type-checks the code using a type-checker like Sorbet. If the user of the code does not type-check it, they will not get a type error.

Upvotes: 4

Nurfitra Pujo Santiko
Nurfitra Pujo Santiko

Reputation: 19

Just define the default method implementation in the abstract class by raising Not implemented error or something. By doing that you also clarifies in your class design that when others / you want to inherit the Pet class they need to override collar_color method. Clarity is a good think and there is no benefit in not defining a default method in the abstract class.

Or if you want to achieve that by testing you can create a test case for Pet class that check if its descendants is defining their own collar_color method or not. I think Rails / Ruby 3.1 have .descendants methods defined or you can just google them.

# Pet_spec.rb 
describe "descendants must implement collar_color" do
  it "should not throw error" do
    descendants = Pet.descendants
    descendants.each do |descendant|
      expect { descendant.new.collar_color }.to.not raise_error
    end
  end
end

Upvotes: 1

Related Questions