Reputation: 1161
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
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
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
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