Thinking in Abstractions
João Hélio
Posted on October 9, 2024
A quick fix in the code can solve many issues, but it may create complexity that becomes difficult to maintain over time. This happens as more and more rules are added to address specific problems within a general context. Let's look at a use case and explore some potential solutions.
Use Case
Imagine you have a general Order
class, but now the business requires handling two specific cases. The Halloween Order must contain at least ten line items, while the Christmas Order can be empty but must include at least one add-on.
First Try: IF-ELSE
Statement
We can attempt to handle this with conditionals like the following:
class Order
attr_accessor :line_items, :plan, :type, :add_ons
def initialize(line_items, plan = 'Basic', type = 'Standard', add_ons = [])
@line_items = line_items
@plan = plan
@type = type
@add_ons = add_ons
end
def validate
if @type == 'Standard'
raise 'Total amount must be greater least one line item' if @line_items.empty?
end
if @type == 'Halloween'
raise 'Halloween order must have at least ten line items' if @line_items.size < 10
end
if @type == 'Christmas'
raise 'Christmas order must have at least one add-on' if @add_ons.empty?
end
end
end
# Example usage
halloween_order = Order.new([], 'Premium', 'Halloween')
halloween_order.validate
=> `validate': Halloween order must have at least ten line items (RuntimeError)
christmas_order = Order.new([], 'Enterprise', 'Christmas')
christmas_order.validate
=> `validate': Total amount must be greater least one line item (RuntimeError)
Pros
-
Simplicity in Small-Scale Scenarios: using
if-else
statements can be the quickest way to implement logic. - Fewer Lines of Code: For simple conditional logic may result in fewer lines of code at first.
-
No Need for Overhead in Basic Use Cases: In situations where we don’t expect many changes or extensions to the logic,
if-else
can avoid unnecessary overhead.
Cons
-
Mixed Responsibilities: The
Order
class is now responsible for both general order logic and the specific rules for Halloween and Christmas orders. -
Difficult to Extend: Adding a new order type means modifying the core
Order
class, which introduces risk and reduces flexibility. - Harder to Test: We can end up having a test that is hard to understand when specific rules are incorporated.
Second try: Abstraction Through Inheritance
We can create specialized classes that capture the architectural intent. By abstracting common behaviour into a base class and allowing each type to manage its own rules.
class Order
attr_accessor :line_items, :plan, :add_ons
def initialize(line_items, plan = 'Basic', add_ons = [])
@line_items = line_items
@plan = plan
@add_ons = add_ons
end
def validate
raise 'Total amount must be greater least one line item' if @line_items.empty?
end
end
class HalloweenOrder < Order
def initialize(line_items, plan = 'Basic', add_ons = [])
super(line_items, plan, add_ons)
end
def validate
raise 'Halloween order must have at least ten line items' if @line_items.size < 10
end
end
class ChristmasOrder < Order
def initialize(line_items, plan = 'Basic', add_ons)
super(line_items, plan, add_ons)
end
def validate
raise 'Christmas order must have at least one add-on' if @add_ons.empty?
end
end
# Example usage
halloween_order = HalloweenOrder.new([], 'Premium')
halloween_order.validate
=> `validate': Halloween order must have at least ten line items (RuntimeError)
christmas_order = ChristmasOrder.new([], 'Enterprise')
christmas_order.validate
=> `validate': Christmas order must have at least one add-on (RuntimeError)
Pros
-
Separation of Concerns: The base
Order
class only handles shared behaviour. The specific rules forHalloweenOrder
andChristmasOrder
are handled in their respective classes. -
Scalability: Adding a new order type (e.g.,
EasterOrder
,BlackFridayOrder
) is easy. - Easier to Test: Testing new use cases doesn't require a shared setup.
Cons
- Overhead for Simple Use Cases: When the use case is simple, setting up multiple classes and abstractions can introduce unnecessary complexity.
- Over-Engineering: YAGNI ("You Aren’t Gonna Need It") we might introduce abstractions for future possibilities that never materialize.
- More Classes to Maintain: Abstracting logic into specialized classes can increase the number of files and classes, which means more "moving parts".
Conclusion
The patch and the module abstraction methods have their place in software design. The key is to assess the complexity and evolution of the system.
Start simple and refactor when complexity arises. This balance ensures the architecture remains maintainable and adaptable without becoming overly complicated.
Posted on October 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.