If it’s a process, model it as one

Today I could review code from a client. I do this Wednesdays and Fridays. I am always looking forward to these days since it's an interesting project and they are great people. The lead dev is also a reader on this list, but I wouldn't have written anything else if it was different. 😜

The client is rebuilding their web shop. One of my suggestions during the initial code quality audit concerned the handling of prices and their calculations. Today I reviewed the updated code for the topic of handling prices and the checkout in their shop. I won't go into detail regarding their code. What I do want to note is the topic of the checkout, though.

You see, when starting to model the checkout for an online shop you perhaps begin with the Cart. Then you'll have an Order and perhaps LineItems. And somehow these things belong together. Somewhere in there, you'll have a Payment of some kind. Perhaps you'll contact PayPal and receive a webhook after the payment was processed. But where do you put all the logic regarding these things? Does your Order handle all this? Does it keep track of its LineItems? And who takes the coupons for the discounts? Where do you apply them? You could have a Price class, that knows how to count the items and apply a discount.
And then you jump around sending messages to your instances of objects. The cart creates a new order, which takes the line items and tells the price to sum it up. Then the order takes the total price and throws it over to the payment processor. But now who receives the webhook from PayPal? The order? Most probably.

Stop. This is more or less the exact same thing that I built a few years ago. You can get this to work. Your tests will be horrifying and you will have to mock all the other objects to get this to work.

I want to propose a better way.
Have a look at this Ruby pseudo code:

# app/processes/book_purchase.rb
class BookPurchase # < ApplicationRecord (if you need persistence…?)
  def initialize(cart, ...other_attributes)
    @cart = cart
    @payed = false
    # ... other things
  end

  # step 1
  def check_availability
    # for every book in the @cart, check availability
    # handle different cases of availability
  end

  # step 2
  def calculate_discounts
    # foo
  end

  # step 3
  def calculate_shipping_costs
    # what the name says
  end

  # step 4
  def finalize_order
    # wait for payment
  end

  # step 5
  def request_payment
    # send off to payment gateway
    # pass self.id to reference this instance (or keep it in memory or wherever) 
  end

  # step 6 - come back here as callback after payment was handled
  def ship_order(payment_params)
    # find instance by id or something else
    if payment_params[:payed]
      @payed = true

      BookShipping.new(x, y, z).process! # start next process
    end
  end
end 

Now you have one object that has the logic and knowledge regarding how to process a book purchase. If you want to change something about the process, change it here. Don't try to piece together how it works, everytime you need to make a change and haven't touched the code in 3 months.

This email is already rather long, so I stop right here. If you have questions on this or could propose improvements, please click reply and let me know.

Yours,
Holger

Similar Posts:

    None Found

Leave a Reply