Alex's Slip-box

These are my org-mode notes in sort of Zettelkasten style

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:

  1. extend can be used to create a class method that provides a declarative API to the base class for dynamically defining behavior.
  2. prepend can be used to override the base class’ methods while yet being able to use super 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

Search Results