Module: Tuile::Final
- Included in:
- Component
- Defined in:
- lib/tuile/final.rb,
sig/tuile.rbs
Overview
Ruby's missing final keyword. A class extends this, marks the methods a
subclass may not redefine, and calls #verify_final! from its own
initialize:
class Component
extend Final
final :children, :parent, :add_child
def initialize = Component.verify_final!(self.class)
end
Class.new(Component) { def children = [] }.new # => Tuile::Error
Marking a method says only that it is final. Why — the invariant an override would break, and the seam to use instead — belongs in that method's own rdoc, which is where the raise sends the reader.
Implementation details
The check resolves each final method's owner instead of hooking
method_added, which fires earlier but sees only def and
define_method — an override arriving through an include or a prepend
slips past it. Nothing fires late enough to place the check automatically,
which is why the extending class owes the explicit #verify_final! call.
Instance Method Summary collapse
-
#final(*names) ⇒ Symbol, ::Array[Symbol]
Marks each of
namesnon-overridable by subclasses. -
#final_methods ⇒ ::Array[Symbol]
@return — the methods marked #final on this class.
-
#verify_final!(klass) ⇒ void
Raises unless
klassinherits every #final_methods entry from this class.
Instance Method Details
#final(*names) ⇒ Symbol, ::Array[Symbol]
Marks each of names non-overridable by subclasses. Declare them in one
call near the top of the class; the names may be forward references,
since nothing is resolved until #verify_final! runs.
final :parent, :children, :add_child
It reads better as a keyword on the definition (final def foo, which
parses — def hands back its name), but don't: YARD has no handler for
the macro, so a decorated def loses its parameter list and sord then
generates def foo: () -> void into sig/, silently.
@param names — method names.
@return — the names — a bare Symbol when exactly one was given.
44 45 46 47 48 |
# File 'lib/tuile/final.rb', line 44 def final(*names) names = names.flatten (@final_methods ||= []).concat(names) names.one? ? names.first : names end |
#final_methods ⇒ ::Array[Symbol]
@return — the methods marked #final on this class.
51 |
# File 'lib/tuile/final.rb', line 51 def final_methods = @final_methods || [] |
#verify_final!(klass) ⇒ void
This method returns an undefined value.
Raises unless klass inherits every #final_methods entry from this
class. Memoized per class, so a construction-time call costs one hash
lookup after the first instance.
@param klass — the class being instantiated.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/tuile/final.rb', line 60 def verify_final!(klass) @final_verified ||= {} return if @final_verified.key?(klass) overridden = final_methods.reject { klass.instance_method(_1).owner == self } unless overridden.empty? raise Error, "#{klass} overrides #{overridden.join(", ")}, which #{overridden.one? ? "is" : "are"} final " \ "on #{self} and may not be redefined. See the rdoc of " \ "#{overridden.map { "#{self}##{_1}" }.join(", ")} for the invariant at stake and the seam " \ "to use instead." end @final_verified[klass] = true end |