DEV Community

Cover image for Watch Out When Overriding Memoized Methods
Nikola Buhinicek for Productive

Posted on • Updated on

Watch Out When Overriding Memoized Methods

This post was inspired by a 🐛 that was caused by not thinking enough when overriding a memoized method. Once we noticed the buggy behaviour, it took me some binding.pry time to figure out what was going on.

Lets check out what happened.

Code snippets

module Actions
  class FormAction
    attr_reader ...

    def initialize(...); ...; end

    def execute_action
      interpolate_properties

      form.process(form_attributes)
    end

    private

    def interpolate_properties
      interpolatable_attributes.each do |attribute|
        form_attributes[attribute] = interpolate(attribute)
      end
    end

    def interpolatable_attributes
      raise NotImplementedError
    end

    ...

    def form_attributes
      @form_attributes ||= action[:attributes].stringify_keys!
    end
  end  
end
Enter fullscreen mode Exit fullscreen mode

A simple Ruby class which is the base class for the specific actions we need to implement for our Rails app.

The action for which we figured out the bug was the action of creating comments on various objects. Its code looks like:

module Actions
  class CreateComment < FormAction
    private

    def interpolatable_attributes
      ['body']
    end

    ...

    def form_attributes
      super.merge("#{commentable_type}_id": action_object.id)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Not much code is actually shown but just enough to spot the bug 🐛. Found it?

What was going on

Basically what was happening when running CreateComment#execute_action was the following:

In the execute_action we firstly need to interpolate some attributes - defined by the specific action class. In this case, for the CreateComment action that was only the 'body' attribute.

def execute_action
  # form_attributes = { 'body' => 'non interpolated' } 
  interpolate_properties

  form.process(form_attributes)
end
Enter fullscreen mode Exit fullscreen mode

That's fine - we do some logic, the 'body' attribute gets interpolated and that value is set as the new value for the 'body' key in the form_attributes hash.

def execute_action
  interpolate_properties
  # form_attributes = { 'body' => 'interpolated' }
  form.process(form_attributes)
end
Enter fullscreen mode Exit fullscreen mode

But, as the implementation of the CreateComment class uses super.merge(...) what we get is actually a new hash each time the method is called.

def execute_action
  interpolate_properties

  form.process(form_attributes) 
  # process method called with { 'body' => 'non interpolated' }
end
Enter fullscreen mode Exit fullscreen mode

The next time, after the interpolation is over, the method form_attributes is called when passing that hash to our form object and in that moment, the form object gets a newly generated hash that doesn't have the interpolated 'body' value. So the interpolation was actually pointless as would any other modification of the form_attributes hash prior to the form.process call be.

The solution 🐛🔨

module Actions
  class CreateComment < FormAction
    private

    ...

    def form_attributes
      @form_attributes ||= super.merge("#{commentable_type}_id": action_object.id)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Added memoization to the CreateComment#form_attributes method.

Simple as that and when you think about it, also pretty obvious that it needed to look like this from the start.

p.s. These snippets maybe look a bit too simple and memoization probably looks like not needed but it's a stripped version of the real classes. The form_attributes method is called and modified quite a few times 🙃

Top comments (2)

Collapse
 
karadza profile image
Juraj

Great eye for catching such a subtle bug, I have to remember this for the future

Collapse
 
buha profile image
Nikola Buhinicek

Anyone had similar "problems" with this? 🐛 @||=