Lesson 4
Dependency Management for Service Objects in Rails
Dependency Management for Service Objects in Rails

Welcome back! In your last lesson, you learned about services in Rails and how they help manage business logic. Today, we’ll build on that by discussing dependency management for service objects. Properly managing dependencies is crucial to building scalable and maintainable Ruby on Rails applications.

Dependency management refers to how different parts of your code rely on each other and get what they need to work. By managing these dependencies, you can make each part of your code more independent and easier to test. In Ruby on Rails, this involves managing instances of service objects in a way that avoids unnecessary duplication and ensures they can access their own dependencies.

To make the code more independent, follow these steps:

  1. Service objects will always return a "response object" with a specific shape.
  2. Service objects maintain their own dependencies.
  3. Service objects should handle their own failure as well as surface the failures of any dependencies.
Singleton Dependencies using Rails Initializers

One way to manage dependencies is by using Rails initializers to create singletons for your services. This ensures that the same instance of a service is used throughout the application. Singletons are preferable in this case because they ensure consistency and reduce the overhead of creating multiple instances, making dependency management more efficient across an application.

First, let's define a simple logging service to illustrate:

Ruby
1class LoggingService 2 def log(message) 3 puts message 4 end 5end

Now, create an initializer to instantiate a singleton of LoggingService:

Ruby
1# config/initializers/services.rb 2Rails.application.configure do 3 config.logging_service = LoggingService.new 4end

This config object makes the service accessible across the entire application.

Injecting Singleton Dependencies

Next, we'll use this singleton in a NotifierService that depends on LoggingService:

Ruby
1class NotifierService 2 def initialize(logging_service: Rails.application.config.logging_service) 3 @logging_service = logging_service 4 end 5 6 def send_notification(message) 7 @logging_service.log("Sending notification: #{message}") 8 # Code to send notification 9 "Notification sent: #{message}" 10 end 11end

Here, we inject the singleton LoggingService instance from the Rails configuration. This ensures we are not creating a new instance every time.

Managing Dependencies with Service Objects

Let's consider a more complex example where services have their own dependencies. Suppose we have a PaymentService that depends on LoggingService and a TransactionValidator:

First, define TransactionValidator:

Ruby
1class TransactionValidator 2 def validate(transaction) 3 # Assume validation logic here 4 true 5 end 6end

Then, set up the initializer:

Ruby
1# config/initializers/services.rb 2Rails.application.config.to_prepare do 3 config.logging_service = LoggingService.new 4 config.transaction_validator = TransactionValidator.new 5end

Now, let's create the PaymentService:

Ruby
1class PaymentService 2 def initialize(logging_service: Rails.application.config.logging_service, 3 transaction_validator: Rails.application.config.transaction_validator) 4 @logging_service = logging_service 5 @transaction_validator = transaction_validator 6 end 7 8 def process_payment(transaction) 9 if @transaction_validator.validate(transaction) 10 @logging_service.log("Processing payment for transaction: #{transaction}") 11 # Code to process the payment 12 "Payment processed" 13 else 14 @logging_service.log("Transaction validation failed: #{transaction}") 15 "Payment failed" 16 end 17 end 18end
Integrating with Controllers

Finally, let’s use PaymentService within a controller. We’ll create a PaymentsController that initializes the service and uses it in an action method:

Ruby
1class PaymentsController < ApplicationController 2 def initialize(service = PaymentService.new) 3 @payment_service = service 4 end 5 6 def create 7 transaction = params[:transaction] 8 result = @payment_service.process_payment(transaction) 9 render json: { result: result } 10 end 11end

And update the routes in routes.rb:

Ruby
1Rails.application.routes.draw do 2 post 'payments/create', to: 'payments#create' 3end

As you can see, we have added a post route in the routes definition, mapping it directly to a controller action. When a POST request is made to /payments/create, it routes the request to the create method within the PaymentsController, which processes the request and renders the result.

Conclusion

Fantastic! You’ve just learned how to manage dependencies effectively in your Rails applications. In this example, we made the code independent by configuring singletons in initializers and injecting these dependencies into service objects, reducing redundancy and ensuring consistency. By making each service object handle its own dependencies and failures, we enhance the modularity, maintainability, and testability of our Rails application.

Excited to see how it all works in action? Let's move on to the practice section where you'll get to implement dependency management yourself. Keep up the great work!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.