Updates from June, 2022 Toggle Comment Threads | Keyboard Shortcuts

  • ThomasPowell 9:21 pm on June 21, 2022 Permalink | Reply
    Tags: design patterns, factory pattern   

    Factory Pattern in Ruby 

    A discussion came up today discussing how the factory pattern looks in certain languages, and I found myself thinking about how it might look in Ruby. A quick Google resulted in an example that seemed to violate part of the reason for the Factory Pattern in the first place (there was a distinct factory for every subclass?), so I decided to ponder my own contrived example to refresh my knowledge.

    Factory Pattern Contrived Example: Shapes

    One example that I quickly thought of (but quickly became useless beyond three sides) was a factory that got an instance of a Shape subclass when given a variable number of sides. Ultimately, a Triangle can have its perimeter and area determined if only the lengths of the sides are known, but any shape beyond that needs either diagonals or angles specified. So I settled on the factory producing either a Triangle that could report back an area or a perimeter or a generic Polygon (arbitrarily excluding Triangle from this) that would only report back perimeter and raise an InterdeterminateArea if the area was requested.

    No news is Good news

    A hallmark of the factory pattern is blocking instantiation directly on the target classes (in this case, making new protected so that only classes within the hierarchy can access it. If our .new method is properly protected, then we should receive a NoMethodError calling .new. (It is enough to assert that NoMethodError is raised because otherwise, all classes should respond to .new:

    The MiniTest cases

    require 'minitest/autorun'
    require_relative 'shape_factory'
    
    class TestShapeFactory < MiniTest::Unit::TestCase
      def test_triangle
        triangle=Shape.get_shape(3,3,3)
        assert_in_epsilon(3.89, triangle.area, 0.01)
        assert_equal(9, triangle.perimeter)
        assert_instance_of(Triangle, triangle)
      end
    
      def test_polygon
        polygon=Shape.get_shape(4,4,4,4)
        assert_equal(16, polygon.perimeter)
        assert_raises(IndeterminateArea) {polygon.area}
        assert_instance_of(Polygon, polygon)
      end
    
      def test_new_shape
        assert_raises(NoMethodError) {Triangle.new}
        assert_raises(NoMethodError) {Polygon.new}
        assert_raises(NoMethodError) {Shape.new}
      end
    end
    

    Above we have are testing that get_shape with 3 arguments returns a Triangle that can return an area (using assert_in_epsilon because the result is irrational) and a perimeter. test_polygon tests that a shape with 4 sides returns a Polygon that can correctly return a perimeter but raises an InterdeterminateArea when #area is called. Finally, test_new_shape validates that we have correctly blocked .new calls on Shape, Polygon, and Triangle.

    shape_factory.rb

    class Shape
      # be sure to protect {class}.new, not instance.new
      class << self
        protected :new
      end
    
      def self.get_shape(*sides)
        case sides.length
        when 3
          Triangle.new(*sides)
        else
          Polygon.new(*sides)
        end
      end
    
      def perimeter
        @perimeter ||= @sides.inject(:+)
      end
    
      def area
        raise 'not implemented'
      end
    
      def initialize(*sides)
        @sides = sides
      end
    end
    
    class Triangle < Shape
      def area
        # Heron's Formula
        Math.sqrt(perimeter/2.0*(@sides.map {|s| perimeter/2.0 - s}.inject(:*)))
      end
    end
    
    class IndeterminateArea < StandardError; end
    
    class Polygon < Shape
      def area
        raise IndeterminateArea
      end
    end
    

    Ultimately, Polygon could probably be collapsed into Shape and only return Triangle. Of course, implementations of shapes with more sides could be made to provide area, but they would require providing additional information to get_shape that might expose the need for the sender to have an awareness of implementation details. Another option might be to pass the successive vertices of the polygon which might allow deeper inferring of how to calculate meaningful information from the shape, but this example is a start with a factory pattern implementation that can be expanded as necessary.

     
  • ThomasPowell 11:07 pm on June 14, 2022 Permalink | Reply
    Tags: alias, define_method, metaprogramming,   

    Ruby object creation and method call debugging without mangling your gems directory 

    The Problem: Debugging Calls to Code in Gems

    Say you’re having a problem that ultimately doesn’t manifest itself until you get somewhere in gem source code, and the exception or symptoms don’t clearly indicate why things are breaking. This can especially be the case with test setups that leak mocks or state between tests. You can, of course edit the gem source, but why not alias the original method and debug with a substitute method that calls the original?

    Hooking into new via an alias

    If you have some hunches about object initialization going awry between working and failing versions of a test, you can declare the following troubleshoot_new method (wherever, as long as it’s accessible in your example):

    def troubleshoot_new(klass)
      klass.class_eval do
        class << self
          alias :old_new :new
          def new(*args)
            puts self.to_s + args.inspect
            old_new *args
          end
        end
      end
    end
    

    The above code aliases the original new method on the class and inserts a logging of the class name and arguments sent to new before calling the original. This could be replaced by a hook to binding.pry or byebug to drop into a debugger instead. Then you could call troubleshoot_new(ClassName) as follows in your individual examples to track object creations.

    class Box # arbitrary class
      def initialize(x, y, z)
      end
    end
          
    troubleshoot_new(Box)
    

    Then on object instantiation you’ll get the logging (the Box[1,2,3])

    irb(main):027:0> Box.new(1,2,3)
    Box[1, 2, 3]
    => #<Box:0x00000001047c7d38>
    

    Cleaning up

    If you want to clean up after the alias

    def untroubleshoot_new(klass)
      klass.class_eval do
        class << self
          alias :new :old_new
        end
      end
    end
    
    untroubleshoot_new(Box)
    

    Which will alias the old method back:

    irb(main):029:0> Box.new(1,2,3)
    => #<Box:0x00000001047576c8>
    irb(main):030:0>
    

    Using a block to wrap the alias/unalias

    To be a little more assured that the alias/unalias will happen, this could be implemented in block form instead:

    def troubleshoot_new(klass)
      klass.class_eval do
        class << self
          alias :old_new :new
          def new(*args)
            puts self.to_s + args.inspect
            old_new *args
          end
        end
      end
    
      yield
    
      klass.class_eval do
        class << self
          alias :new :old_new
        end
      end
    end
    
    irb(main):030:1* troubleshoot_new(Box) do
    irb(main):031:1*   p Box.new(1,2,3)
    irb(main):032:0> end
    irb(main):033:0> p Box.new(4,5,6)
    Box[1, 2, 3]
    #<Box:0x0000000104886440>
    #<Box:0x00000001048860f8>
    

    Active debugging via a hook into the object creation

    Another way you can use this is to drop directly into a method deep in the call chain instead of having to debug you way into it with step in pry. I extended the new strategy to an arbitrary method (instance method shown here), since it’s often a specific method call that’s the trigger for a failure.

    def troubleshoot(klass, method)
      klass.class_eval do
        alias_method "old_#{method}".to_sym, method
        define_method method do |*args|
          puts klass.to_s + ':' + method.to_s + "->" + args.inspect
          # binding.pry # to debug from here
          send "old_#{method}".to_sym, *args
        end
      end
    
      yield
    
      klass.class_eval do
        alias_method method, "old_#{method}".to_sym
      end
    end
    
    class Box
      attr_accessor :x, :y, :z
      def initialize(x, y, z)
        @x=x
        @y=y
        @z=z
      end
    
      def join_it(insert)
        puts [x,y,z].join(insert).inspect
      end
    end
    
    troubleshoot(Box, :join_it) do
      box = Box.new(1,2,3)
      box.join_it("<->")
    end
    
    #> Box:join_it->["<->"]
    #> "1<->2<->3"
    

    The commented out binding.pry can be uncommented to jump directly into the entry point for a problem.

    May your problems rarely be this deep

    Having to dig into problems that are deep in gem source means that you’re probably already in a world of pain, but hopefully the above strategy will inspire you with additional debugging tools if the worst case debugging scenario visits you.

     
  • ThomasPowell 5:49 pm on May 6, 2022 Permalink | Reply
    Tags: , , warnings   

    Quiet “Don’t run bundler as root” warnings, Bundler >= 2.3.7 

    Why you wouldn’t want to run bundler as root

    As a macOS user, you might want to be warned against running bundler as root (specifically against the system ruby), because you can cause subsequent problems for installing future gems.

    The closing of issue #2936 in the rubygems/bundler project added an error message:

    Don’t run Bundler as root. Bundler can ask for sudo if it is needed, and installing your bundle as root will break this application for all non-root users on this machine.

    But what if this is deployment to a Linux machine and installing to the system ruby is your intent?

    If you want to keep your logs clean of the warning message (especially if you have warnings highlighted), you can use one of the following options:

    The following sets in ~/.bundle/config (/root/.bundle/config if root/sudo)

    bundle config silence_root_warning true

    The following can also be used if you want to temporarily disable the warning (a la “I know what I’m doing”)

    export BUNDLE_SILENCE_ROOT_WARNING=true

    As seen below using the bundle config option will fix sudo bundle install to not show the warning.

    bundler as root warning

     
c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
Reply
e
Edit
o
Show/Hide comments
t
Go to top
l
Go to login
h
Show/Hide help
shift + esc
Cancel
%d bloggers like this: