Class macro and wrapping methods
Here’s a pattern for 1) creating a class macro that can dynamically define behavior and 2) wrapping methods (ie, for logging, benchmarking, validating)
# Example
This is a super ridiculous example, but it gets the basic point across.
Here we want to include validating that the three sides, can in fact, be used to make a triangle (eg, the sum of any two sides must be gte the remaining side) as part of the predicate methods.
Instead of calling valid?
inside each of the predicate methods, the same
methods are “wrapped” by dynamically prepending a module that defines the same
methods but adds the call to valid?
while then calling super
to invoke the
original methods.
It’s a really stupid example, but it shows how:
extend
can be used to create a class method that provides a declarative API to the base class for dynamically defining behavior.prepend
can be used to override the base class’ methods while yet being able to usesuper
to invoke the original behavior.
require 'forwardable' class Triangle module Validator def self.included(base) base.extend ClassMethods validator_methods = const_set("#{base.name}ValidatorMethods", Module.new) base.prepend validator_methods end module ClassMethods def validate_is_triangle_for(methods) validator_methods = const_get("#{name}ValidatorMethods") methods.each do |method| validator_methods.define_method(method) do valid? && super() end end end end def valid? satisfies_triangle_inequality? && sides.sum.nonzero? end def satisfies_triangle_inequality? a, b, c = sides a + b >= c && a + c >= b && b + c >= a end end end class Triangle extend Forwardable def_delegator :sides, :uniq include Triangle::Validator validate_is_triangle_for %i[equilateral? isosceles? scalene?] attr_reader :sides def initialize(sides) @sides = sides end def equilateral? uniq.length == 1 end def isosceles? uniq.length == 2 || equilateral? end def scalene? uniq.length == 3 end end