SOLID: Inversion of Control in Ruby


Created by Stan on 18-04-2023


Inversion of Control (IoC) is a software design principle that promotes the decoupling of dependencies in a system, leading to more modular, maintainable, and testable code. IoC is also known as the Hollywood Principle, due to its motto: "Don't call us; we'll call you." This article will discuss the concept of IoC and demonstrate how to implement it in a Ruby application.

Understanding Inversion of Control

In a traditional software design, components directly call other components or create instances of dependencies, leading to a tightly coupled system. This tight coupling can make it challenging to modify, maintain, and test the application.

Inversion of Control addresses this issue by inverting the control flow, such that the dependencies are provided to a component, rather than the component creating or acquiring them. This results in a more modular and decoupled system, which is easier to maintain and test.

Implementing Inversion of Control in Ruby

There are various ways to achieve IoC in a Ruby application, such as using dependency injection, service locators, or IoC containers. In this article, we'll focus on dependency injection as a way to demonstrate IoC in Ruby.

Dependency Injection

Dependency Injection (DI) is a technique to achieve IoC by providing dependencies to a component through its constructor, properties, or methods. It allows for better separation of concerns and makes testing easier by enabling the substitution of dependencies with test doubles.

Let's consider an example where a Notifier class is responsible for sending notifications via email or SMS.

We can use the mail gem for sending emails and the twilio-ruby gem for sending SMS messages. First, you'll need to install these gems:

gem install mail twilio-ruby

Without IoC:

require 'mail'

class EmailService
  def send_email(to, message)
    # Code to send an email
  end
end
require 'twilio-ruby'

class SmsService
  def send_sms(to, message)
    # Code to send an SMS
  end
end
class Notifier
  def initialize
    @email_service = EmailService.new
    @sms_service = SmsService.new
  end

  def send_notification(to, message)
    @email_service.send_email(to, message)
    @sms_service.send_sms(to, message)
  end
end

In the above code, the Notifier class directly creates instances of EmailService and SmsService, making it tightly coupled to those implementations. This tight coupling makes it difficult to modify, maintain, or test the Notifier class.

Applying IoC using Dependency Injection:

require 'mail'

class EmailService
  def send_email(to, message)
    # Code to send an email
  end
end
require 'twilio-ruby'

class SmsService
  def send_sms(to, message)
    # Code to send an SMS
  end
end
class Notifier
  def initialize(email_service, sms_service)
    @email_service = email_service
    @sms_service = sms_service
  end

  def send_notification(to, message)
    @email_service.send_email(to, message)
    @sms_service.send_sms(to, message)
  end
end

By applying IoC through dependency injection, we've decoupled the Notifier class from the specific implementations of EmailService and SmsService. Now, the Notifier class no longer needs to know how to create these services, and we can easily substitute them with alternative implementations or test doubles.

Real-World Use Cases for Inversion of Control

Inversion of Control can be applied to various real-world scenarios to improve the design of an application. Some common use cases include:

Database access: Decoupling components from specific database implementations allows for easy switching between different database systems or connection strategies.
Third-party services: By decoupling components from specific third-party service implementations, it becomes easier to switch between providers or adapt to API changes.
Logging and error handling: IoC can be used to decouple components from specific logging or error handling strategies, allowing for more flexible and configurable logging/error handling in an application.

IoC Libraries for Ruby

While IoC can be implemented manually, several Ruby libraries can help simplify the process:

dry-rb: A collection of Ruby libraries that includes dry-container and dry-auto_inject for dependency injection and managing IoC containers.
light-service: A library for organizing Ruby code into a series of actions, with explicit dependencies passed between them.
injectable: A minimalistic dependency injection library for Ruby.



Related Posts