class_eval and the terrible, horrible, no good, very bad day


Integrating dependencies into our applications can be difficult. This is particularly true when we are customizing a generic framework like Solidus to meet our specific business needs. As eager rubyists, many of us jump at the chance to take advantage of Ruby's open classes. We have even been encouraged to do so.

What's so bad about class_eval?

While using tools like class_eval can be an expedient fix to add some logic into a dependency you don't control, if you use it too often you're going to end up having a terrible, horrible, no good, very bad day.

No API enforcement

Opening a ruby class gives you the ability to add, change, and remove behaviour from ANY method in the class. Nothing is off-limits: private methods, method signatures, and everything else. This makes it incredibly easy to break the dependency by changing a method that is used within it or somewhere else you didn't expect.

# dependency/order.rb
class Dependency::Order
  def order_total
    items.sum(&:amount)
  end
end

# my_app/order_decorator.rb
Depencency::Order.class_eval do
  def order_total(tax_amount)
    items.sum(&:amount) + tax_amount
  end
end

# We just changed the method signature. If the dependency ever calls
# order.order_total internally, we get an argument error

In addition, when we are only modifying existing methods, we end up copying dependency code into our application. This is great in the short term... until a new version of the dependency is released and the internal implementation of the method is changed. Your class_eval override is now obscuring that change from your application.

# dependency/order.rb
# Version 2.0
class Dependency::Order
  def order_total
    perform_super_important_fraud_check
    items.sum(&:amount)
  end
end

# If we upgrade to v2.0 of the dependency, our class_eval above won't know
# about the super_important_fraud_check

class_eval collisions

So you've used a class_eval on a dependency. Oops. Not the end of the world... until someone else tries to hijack the same class with a second class_eval. This is more likely to happen then you might think, especially in an eco-system like Solidus-Contrib. Many extensions in Solidus need to add behaviour to the same classes: User, Order, Shipment, etc. So what happens if more than one class_eval is used on the same class? If both decorators try and change the behaviour of the same method, only one of the new implementations will be used.

# dependency_extension/order.rb
Dependency::Order.class_eval do
  def order_total
    subtotal = items.sum(&:amount)
    add_promotion_discounts(subtotal)
  end
end

# Assuming we also have the code from previous examples. What happens when I call
# order.order_total? Does the amount include taxes or does it include promotion
# discounts? Who knows really...

This is called a load order dependency. Your application will exhibit different behaviour depending on the order the classes and decorators are loaded. This is bad and incredibly difficult to debug.

So what's the alternative?

"Alright, I get it". class_eval == 'bad'. We still need a way of customizing our dependencies to meet our own application needs. Here are a few strategies for adding some custom logic into other people's code, without breaking it.

Composition

The Composition pattern is arguably the most flexible solution, but it requires some negotiation with the dependency. If your dependency can provide endpoints to swap out certain classes which are responsible for specific tasks, it gives everyone using the dependency the ability to customise the it for their needs. So long as your modular class responds to the API that the dependency expects, your new custom code should integrate smoothly with the dependency.

This is, in fact, what Solidus is doing to provide extension points for things like the Variant Pricer and the Stock Estimator. If you want an extension point somewhere new: send a PR. You're likely not the only person wanting to customize that area, but the core team may not know that.

# dependency/order.rb
class Dependency::Order
  def order_total
    Config.order_total_calculator_class.new(items).total
  end
end

# my_app/order_total_calculator
class MyApp::OrderTotalCalculator
  def total
    items.sum(&:amount) + tax_amount
  end

  def tax_amount
    # ...
  end
end

# my_appy/initializers/dependency.rb
Dependency::Config.order_total_calculator_class = MyAppy::OrderTotalCalculator

# We have created our own order total calculator and plugged it into the
# dependency

Advantages:

  • Your changes are visible to the dependency.
  • You're not changing dependency code.
  • You are immune to others overriding your behaviour with conflicting class_evals.
  • You have provided a safe interface to add customized logic to the dependency.

Disadvantages:

  • You need to have an existing extension point.
  • Or, you need to be able to send a PR to your dependency and create the extension point. (Yaay open source! Let's make software better and more flexible together.)

When to use:

  • You have an extension point to hook into.
  • You can provide and extension point to hook into.

Decorators... real ones

The Decorator pattern allow you to wrap a dependency class with a class of your own. You can add methods, change methods and add to existing methods all within the safety of your own application.

# my_app/taxable_order.rb
class TaxableOrder
  def new(dependency_order)
    @order = dependency_order
  end

  def order_total(tax_amount)
    @order.order_total + tax_amount
  end
end

# Awesome we added some custom logic without changing anything internal to the
# dependency. However, this class is only accessible from our application, not from
# within the dependency itself.

Advantages:

  • You're not changing dependency code.
  • You are immune to others overriding your behaviour with conflicting class_evals.

Disadvantages:

  • It's still very possible to obscure changes made in the dependency.
  • Your changes are not visible to the dependency. Your changes only affect the places in your application where you interact with the decorator.

When to use:

  • Your changes are independent of the original implementation.
  • You are only adding, not changing existing behaviour.

Module#prepend

When all else fails and the above options are not viable for your situation, we can fallback to Module#prepend (as of Ruby 2.0). Let's agree now that this is only marginally better than a class_eval. The difference being a prepended module is added to the ancestors chain of the receiving class. This small difference gives us the ability to use super, and that can make a huge difference.

# my_app/taxable_order.rb
module TaxableOrder
  def order_total
    super + tax_amount
  end

  def tax_amount
    # ...
  end
end

Dependency::Order.prepend TaxableOrder

So long as we are only adding methods, or adding behaviour around an existing method, we can use the super keyword to call the original implementation. This means when the dependency is updated, your module automagically knows about it.

Advantages:

  • Your changes are visible to the dependency.
  • You can take advantage of updates in the dependency without changing your application code.

Disadvantages:

  • It's still very possible to obscure changes made in the dependency (if you forget to use super).
  • Your implementation may conflict with other prepends or class_evals.

When to use:

  • After all else has failed you.

After Thoughts

Here at Stembolt, we are a no class_eval company. We think you should be too. Despite it being the fast and easy solution, monkey patching classes leads to fragile code that is difficult to debug, maintain, and extend. Our solution is to provide customizable extension points to allow collaborators to safely extend our applications without breaking core functionality. If you have your own solution to this problem let us know. We would love to hear about it!