DEV Community

Cover image for Thinking in Abstractions
João Hélio for Marley Spoon

Posted on

Thinking in Abstractions

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
Enter fullscreen mode Exit fullscreen mode
# 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)
Enter fullscreen mode Exit fullscreen mode

Pros

  1. Simplicity in Small-Scale Scenarios: using if-else statements can be the quickest way to implement logic.
  2. Fewer Lines of Code: For simple conditional logic may result in fewer lines of code at first.
  3. 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

  1. Mixed Responsibilities: The Order class is now responsible for both general order logic and the specific rules for Halloween and Christmas orders.
  2. Difficult to Extend: Adding a new order type means modifying the core Order class, which introduces risk and reduces flexibility.
  3. 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
Enter fullscreen mode Exit fullscreen mode
# 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)
Enter fullscreen mode Exit fullscreen mode

Pros

  1. Separation of Concerns: The base Order class only handles shared behaviour. The specific rules for HalloweenOrder and ChristmasOrder are handled in their respective classes.
  2. Scalability: Adding a new order type (e.g., EasterOrder, BlackFridayOrder) is easy.
  3. Easier to Test: Testing new use cases doesn't require a shared setup.

Cons

  1. Overhead for Simple Use Cases: When the use case is simple, setting up multiple classes and abstractions can introduce unnecessary complexity.
  2. Over-Engineering: YAGNI ("You Aren’t Gonna Need It") we might introduce abstractions for future possibilities that never materialize.
  3. 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.

Top comments (0)