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:
- Service objects will always return a "response object" with a specific shape.
- Service objects maintain their own dependencies.
- Service objects should handle their own failure as well as surface the failures of any dependencies.
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:
Ruby1class LoggingService 2 def log(message) 3 puts message 4 end 5end
Now, create an initializer to instantiate a singleton of LoggingService
:
Ruby1# 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.
Next, we'll use this singleton in a NotifierService
that depends on LoggingService
:
Ruby1class 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.
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
:
Ruby1class TransactionValidator 2 def validate(transaction) 3 # Assume validation logic here 4 true 5 end 6end
Then, set up the initializer:
Ruby1# 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
:
Ruby1class 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
Finally, let’s use PaymentService
within a controller. We’ll create a PaymentsController
that initializes the service and uses it in an action method:
Ruby1class 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
:
Ruby1Rails.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.
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!