+2

Policy Object

In the previous post, I explained about Plan object, which is just my idea. In this article I will explain about a somewhat similar concept -- Policy object, which is invented by more credible people 😃.

Background

Some people who are fighting with complicated business logic might love Service Object. It is a new layer where we put all complicated business logic to avoid super fat model. But there is a problem that it is not easy to test. Personally I believe that service itself is not hard to test, but we use service for complicated logic so it is hard to test. Anyway, in my project I often feel that it is very difficult to test. This is because our database is rather complicated with more than 500 tables and I need a lot of effort for preparing data to test.

For example, sometimes we need to make a data across more than 10 tables to make one test. It takes time to create and very hard to maintain because the most of the testing code is spent for making data.

Policy Object

To avoid such situation, we have Policy Object. This is a PORO (Plain Old Ruby Object) which answers important business questions.

For example, think about an e-commerce website. In this site shipping fee is free if you buy more than $100 exluding tax, it's free if today is the customer's birthday and shipping fee is half price if you live in Vietnam.

class Service::MakeOrder
  def make_order( order )
    order.shipping_fee = calculate_shipping_fee(order)
    order.save!
  end

  def calculate_shipping_fee( order )
    if order.line_items.map{|i| i.amount_without_tax }.sum > 100.0

    elsif order.customer.birthday == Date.today

    elsif order.customer.address.country == "Vietnam"
      FEE / 2
    else
      FEE
  end
end

So the calculate_shipping_fee method is a bit complicated. To test this method, we need to create order data with many child data property attached. For each test case we need to think and modify the test data. It's too complicated. If test is complicated, who knows it works well?

To resolve this situation we can introduce Policy object like this

class Service::MakeOrder
  def make_order( order , policy = Policy.new(order) )
    order.shipping_fee = calculate_shipping_fee(order)
    order.save!
  end

  def calculate_shipping_fee( policy )
    if policy.free_shipping?

    elsif policy.half_shipping?
      FEE / 2
    else
      FEE
  end

  class Policy
    def free_shipping?
      @order.line_items.map{|i| i.amount_without_tax }.sum > 100.0 ||
        @order.customer.birthday == Date.today
    end

    def half_shipping?
      @order.customer.address.country == "Vietnam"
    end
  end
end

There are some advantages in this code compared to the previous code.

The first one is that the main logic of calculating code only depends on the policy object. So we can easily stub it. It means when we test calculate_shipping_fee it is quite easy to prepare data. We don't need to make order object tree anymore. Of course we need to test Policy object independently. In this case we need to make data but the complexity is reduced.

The second one is the a bit clearer structure of our code. In the previous code very important business decision is embedded in the middle of the code. But with the new code we know that the important condition is in the Policy object so it is rather easy to find it.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí