Never Monkey-Patch Again
Today’s Skill Issue isn’t about Shroom, but it’s worth noting that I’ve got function calls type-checking and have worked out the parsing of function definitions with types. I want to do a couple things like support recursive function calls, then I’ll probably want to start working on type-inference. That sounds fun. (I guess I should probably work on modules and stuff at some point too.)
Anyway, I wanted to write a little about the further work we’ve been doing on Solidus’ order recalculator. As we worked to create Solidus’ in-memory order updater, a few things became clear to me. Those realizations led me to finally tackle a project I’d been eyeing for years.
The order updater is a single, monolithic class. It’s sometimes monkey-patched by stores. Those monkey-patches are very risky. The order updater is one of the most important components in Solidus and it hooks into all the core models.
If you modify it, every update risks bringing in drift in the existing class or in any of the affected models that could break the core function of your app in subtle and hard-to-debug ways. Is there a way we could construct an order updater that never needs to be monkey-patched?
Absolutely. Inspired by Rack’s middleware, I’ve come up with a new modular order recalculator. To fix the longstanding naming discrepancy1, I’m calling it Spree::OrderRecalculator.
Like with Rack, the new recalculator passes a context object through a series of “middleware”, each individually testable and (much more importantly) replaceable. Your store can add to or replace any of the steps in the process. The context class is also configurable, should you need to parameterize your order updates in new ways.
Here’s the gist of it:
module Spree
class OrderRecalculator
attr_reader :order
def initialize(order)
@order = order
end
def recalculate(persist: true)
run(Spree::Config.order_recalculation_middlewares, persist: persist)
end
def recalculate_payment_state
run(Spree::Config.payment_state_recalculation_middlewares)
order.payment_state
end
def recalculate_shipment_state
run(Spree::Config.shipment_state_recalculation_middlewares)
order.shipment_state
end
private
def run(middlewares, persist: true)
context = Spree::Config.order_recalculation_context_class.new(order: order, persist: persist)
Spree::MiddlewareRunner.call(middlewares, context)
end
end
end
To support this, I've broken out all the important functions that make up order recalculation into a series of composable, replaceable middleware. The default list looks like this:
EventTransactionPersistManipulativeQueryMonitorLineItemPricesItemCountShipmentAmountsPaymentTotalItemTotalShipmentTotalLegacyPromotionAdjusterTaxAdjustmentsItemTotalsAdjustmentTotalsCompletedState
Stores that need to make changes to the recalculation process can swap out any of these components, insert their own, or replace the whole set. The whole thing maintains the functionality of the in-memory order updater that we introduced previously and should have similar performance characteristics.
More work needs to be done to properly test this new recalculator and it has yet to be reviewed by the rest of the core team. We also want to adapt the modifications from some of the stores we work on to this recalculator. This will help us determine how effective this extension pattern is and evaluate any performance implications.
I had planned on recommending something far more conventional this week, and then I found this record from Father Dionysios Tabakis (π. Διονύσιος Ταμπάκης). This fellow is an Orthodox priest in a small Greek village who makes fretless doom/drone metal, among other genres. Trust me, this record is worth a listen.
-
The order updater is named "updater" because you used to call
#update!on orders to perform recalculation. This was confusing for new users as it overrode ActiveRecord's existing#update!method. We eventually changed the method to#recalculateand the configuration option matches that naming (order_recalculator), but the class that does the recalculation is unfortunately still calledOrderUpdater. ↩