In a Ruby base class such as Vehicle, how do you declare a method every subclass must implement, and what does raising NotImplementedError actually guarantee?
answer
- no abstract keyword in Ruby
- a stub that raises
- __method__ names the missing method
- respond_to? still answers true
- fails only when called
basics
~20 sRuby has no abstract keyword, so the base class defines a stub that raises NotImplementedError naming the class and method. It only guarantees a clear error when the stub is called; nothing is checked at load time.
solid answer
~40 sThe convention is a stub in the base class whose body is `raise NotImplementedError, "#{self.class} must implement #{__method__}"`. It documents the contract, and when a subclass forgets the override, the first call fails with a message naming the concrete class and the method. That is all it guarantees. Nothing checks at load time, `Vehicle.new` still works, and `respond_to?(:range_km)` returns `true` because the stub is a real method, so duck-typed callers are misled. `NotImplementedError` is documented for platform features such as `fork`, and it is a `ScriptError` rather than a `StandardError`, so some teams raise their own error class instead. Real enforcement comes from a test that checks every subclass.
code
ruby · 11 linesrequire "minitest/autorun"
class VehicleContractTest < Minitest::Test
CONCRETE = [ElectricCar, Scooter]
def test_every_vehicle_implements_range_km
CONCRETE.each do |klass|
assert_kind_of Integer, klass.new.range_km, "#{klass} must implement range_km"
end
end
endgo deeper
Write a base-class method that raises NotImplementedError with a message, and override it in a subclass so the base class's other methods can call it.
Explain that the stub fails only at call time, use self.class and method in the message, and connect it to the template method shape.
Name the limits: respond_to? stays true, the base is still instantiable, NotImplementedError is not a StandardError, and a contract test is the real enforcement.
Decide a codebase-wide convention for abstract hooks, the error class, stubs and contract tests, and when a module contract or composition makes the base class unnecessary.
## Ruby has no abstract methods Ruby has no `abstract` keyword and no interface declaration that the interpreter checks. A **base class** that expects subclasses to fill in a method therefore writes a **stub**: a normal method whose only job is to fail loudly if it is ever reached. ```ruby class Vehicle def summary "#{self.class.name}: #{range_km} km" end def range_km raise NotImplementedError, "#{self.class} must implement #{__method__}" end end class ElectricCar < Vehicle def range_km = 420 end class Scooter < Vehicle; end ElectricCar.new.summary # => "ElectricCar: 420 km" Scooter.new.summary # NotImplementedError: Scooter must implement range_km ``` This is the **template method** shape: `summary` in the base class calls a **hook** (`range_km`) that each subclass supplies. The stub makes the hook visible to readers of `Vehicle` and gives a precise failure. ## Writing a useful stub - Use `self.class`, not a hard-coded `Vehicle`, so the message names the subclass that forgot the override. - `__method__` returns the current method's name as a symbol, which keeps the message right if the method is renamed. - Keep the stub's parameter list identical to what overrides will take; RuboCop's `Lint/UnusedMethodArgument` has `IgnoreNotImplementedMethods: true` by default, so unused arguments in a method that only raises `NotImplementedError` are not reported. - Do not make the stub return `nil` or call `super`: `nil` defers the failure to somewhere far away, and `super` ends in a `NoMethodError` that blames the base class. ## What the stub does not guarantee | Expectation | Reality | |---|---| | Checked when the class loads | No: only when the stub is called | | `Vehicle.new` is refused | No: the base class is still instantiable | | `respond_to?(:range_km)` is `false` for a subclass that forgot | No: the stub is a real method, so it is `true` | The `respond_to?` row surprises people because the core docs mention a case where it is `false`: methods such as `fork` that the C runtime marks as unimplemented on the current platform report `false`. A Ruby-level stub that raises is not such a method, so duck-typing checks see it as present. ## Choosing the error class `NotImplementedError` is documented as "raised when a feature is not implemented on the current platform", and it descends from `ScriptError`, not `StandardError`. Using it for abstract stubs is a long-standing convention, but it is borrowing a class meant for something else, and how it interacts with a bare `rescue` belongs to the exception hierarchy. Many codebases therefore define their own error, for example `class AbstractMethodError < StandardError; end`, and raise that. Either choice is defensible; mixing both in one codebase is not. ## Three ways to leave a hook for subclasses | Approach | Failure when a subclass forgets | Visible in the base class | |---|---|---| | No method in `Vehicle` at all | `NoMethodError` raised from inside the caller | no | | Stub raising `NotImplementedError` | `NotImplementedError` naming class and method | yes | | Stub raising a custom `StandardError` subclass | your own error class, named for the contract | yes | All three fail at the same moment, the first call. The stub variants add documentation and a better message; they do not add an earlier check. ## Enforcing the contract for real Because Ruby checks nothing up front, enforcement has to come from tests: 1. Keep one shared contract test (an RSpec shared example group or a Minitest module) that every concrete subclass runs. 2. Or loop over an explicit list of concrete classes and call each hook once, so a subclass still carrying the stub fails in CI rather than in production. 3. Prevent direct use of the base class if that matters, for instance by making its constructor private, which is a visibility decision rather than an inheritance one. ## Why interviewers ask it The question separates candidates who know the idiom from those who know its limits. A strong answer writes the stub correctly, explains that it is a runtime tripwire rather than a compile-time rule, notes the `respond_to?` and error-class caveats, and says how a team actually catches a missing override before production does.
- Why is `super` a poor body for an abstract stub in Vehicle?If no class above `Vehicle` defines the method, `super` falls through to `method_missing` and raises `NoMethodError` with a message saying there is no superclass method. That blames `Vehicle` rather than the subclass that forgot the override and hides the fact that an override was expected. An explicit `raise` with `self.class` and `__method__` states the contract.
- Why raise from a stub instead of leaving range_km undefined in Vehicle?Leaving it undefined also fails at call time, with `NoMethodError` raised from inside `summary`, but the contract is invisible to anyone reading `Vehicle` and the error does not say an override was expected. A stub documents the hook next to the code that uses it and fails with a message naming the subclass and the method. The price is that `respond_to?(:range_km)` now returns `true`.
saying these in an interview costs you the question
- Ruby refuses to load a subclass that does not override a NotImplementedError stub.
- A subclass that forgot the override answers false to respond_to?(:range_km).
- Returning nil from the base method is a safe way to mark it abstract.
- Raising NotImplementedError makes Vehicle.new impossible, like an abstract class elsewhere.