
Extracting Service Objects for Cleaner and More Maintainable Code
Hello, fellow software designers! Today, we’re going to explore a powerful refactoring technique known as Extracting Service Objects. This technique helps you simplify your models and controllers, improve code maintainability, and adhere to the Single Responsibility Principle (SRP). Let’s dive into what service objects are, why you should use them, and how to extract them effectively.
What Does “Extract Service Objects” Mean?
Extracting Service Objects involves moving specific business logic or operations out of models or controllers and into dedicated service classes, often called service objects. This practice helps to:
- Simplify the Original Class: By removing complex logic or multiple responsibilities.
- Enhance Reusability: Service objects can be reused across different parts of the application.
- Improve Testability: Service objects can be independently tested.
- Follow SOLID Principles: Particularly the Single Responsibility Principle (SRP).
Why Extract Service Objects?
Over time, as your application grows, models or controllers can become “fat” — containing too much logic or too many responsibilities. This makes the code harder to understand, maintain, and test. Extracting service objects addresses this issue by isolating specific logic into its own class.
How to Extract Service Objects
- Identify the Business Logic or Operations: Look for complex or domain-specific logic inside your models or controllers that are not strictly related to their primary responsibility.
- Create a Service Object: Create a new class that encapsulates the identified logic.
- Move the Logic to the Service Object: Refactor the logic out of the model or controller and into the new service object.
- Use the Service Object: Replace the extracted logic in your model or controller with a call to the new service object.
Example: Extracting Service Objects
Let’s look at a practical example. Suppose you have a controller action that handles creating an order and calculating the total price with discounts, taxes, and shipping fees.
Original “Fat” Controller
class OrdersController < ApplicationController
def create
order = Order.new(order_params)
if order.save
# Calculate total price with discounts, taxes, and shipping
discount = calculate_discount(order)
tax = calculate_tax(order)
shipping = calculate_shipping(order)
total_price = order.subtotal - discount + tax + shipping
order.update(total_price: total_price)
render json: { order: order, success: true, message: :order_created }
else
render json: { success: false, errors: order.errors }
end
end
private
def calculate_discount(order)
# Complex discount calculation logic here
end
def calculate_tax(order)
# Complex tax calculation logic here
end
def calculate_shipping(order)
# Complex shipping calculation logic here
end
def order_params
params.require(:order).permit(:item_id, :quantity, :user_id)
end
end
Here, the create action is handling multiple responsibilities, making it hard to maintain.
Step 1: Extract Service Objects
Let’s extract the logic for calculating the total price into a service object.
Service Object: OrderTotalCalculatorService
# app/services/order_total_calculator_service.rb
class OrderTotalCalculatorService
def initialize(order)
@order = order
end
def execute
discount = calculate_discount
tax = calculate_tax
shipping = calculate_shipping
total_price = @order.subtotal - discount + tax + shipping
total_price
end
private
attr_reader :order
def calculate_discount
# Logic to calculate discount
end
def calculate_tax
# Logic to calculate tax
end
def calculate_shipping
# Logic to calculate shipping
end
end
Step 2: Refactor the Controller to Use the Service Object
Refactor the controller to delegate the total price calculation to the service object:
class OrdersController < ApplicationController
def create
order = Order.new(order_params)
if order.save
# Use the service object to calculate the total price
total_price = OrderTotalCalculatorService.new(order).execute
order.update(total_price: total_price)
render json: { order: order, success: true, message: :order_created }
else
render json: { success: false, errors: order.errors }
end
end
private
def order_params
params.require(:order).permit(:item_id, :quantity, :user_id)
end
end
Step 3: Further Extract Smaller Services if Needed
If the methods inside OrderTotalCalculatorService
are still complex, extract them into smaller services, like DiscountCalculatorService
, TaxCalculatorService
, and ShippingCalculatorService
.
Benefits of Extracting Service Objects
- Improved Readability: Each service has a single responsibility and a clear purpose.
- Enhanced Reusability: Service objects can be reused in different parts of the application.
- Simplified Testing: Each service can be tested in isolation.
- Easier Maintenance: Changes can be made in one place without affecting other parts of the application.
- Better Adherence to SOLID Principles: Especially SRP and OCP.
Conclusion
Extracting service objects is a powerful technique to improve the structure, readability, and maintainability of your code. It keeps your models and controllers clean and focused while encapsulating complex or domain-specific logic in dedicated classes.
Stay tuned for more insights into software design principles and patterns!
Thôi Lo Code Đi Kẻo Sếp nạt!!