This post uses a recent discussion we had in CookiesHQ as an example. It is one of those “but why change it if it works!?” situations that you can find in an early stage application code.
The problem
This is the logic we are working with
- A product belongs to a category.
- A product belongs to a vendor.
- A product can be active or not (active boolean).
- A category can be active or not (active boolean).
- A vendor can be active or not (active boolean).
- In order to be active, a product needs to be active and part of an active category and an active vendor.
- We need to keep the active boolean clean as, as part of the API, a vendor can choose manually to set a product as active or not, same for a category.
Quickly scanning this, the Rails logic sounds easy
Model product
belongs_to :category
belongs_to :vendor
scope :active, -> { joins(:category, :vendor).where(categories: {active: true}, vendors: {active: true}, active: true) }
end
The problem with this approach is that we move the logic of an active category and vendor in the product model.
It is fundamentally wrong as if our logic changes later in our application (say to be considered as active a Category need to be active:true and have an image) then we will have a hard time going back to every place where the logic has been set.
We will then end up in an unmaintainable code, hard to test, and not scalable at all.
Solution
This problem is easily solved by having a Category.active scope and a Vendor.active scope.
Model Category
scope :active, -> { where(active: true) }
end
Model Vendor
scope :active, -> { where(active: true) }
end
So now we can independently test the active/unactive state of the vendor and category.
Using that logic we can now refactor or old line to:
scope :active, -> { joins(:category, :vendor).merge(Category.active).merge(Vendor.active).where(active: true) }
The logic is now abstracted to each model.
BONUS
Using Rails 4+ where.not the .inactive version of the product is now super easy
Model Product
scope :inactive, -> { where.not(id: Product.active) }
end
Nothing shown here is rocket science, but it is just a reminder for everyone writing code: when you smell that something is wrong, stop immediately and refactor straight away.
The logic showed in this post is now easy to test, reliable and scalable. All this in just 10 minutes. We love the quick wins!