Explore Dependency Injection (DI) and Inversion of Control (IoC) in Ruby to enhance decoupling, testing, and maintainability. Learn manual and framework-based DI implementations.
In the realm of software architecture, managing dependencies between objects is crucial for building scalable and maintainable applications. Dependency Injection (DI) and Inversion of Control (IoC) are two powerful patterns that facilitate this by promoting decoupling and enhancing testability. In this section, we will delve into these concepts, explore their benefits, and demonstrate how to implement them in Ruby.
Dependency Injection (DI) is a design pattern used to achieve Inversion of Control (IoC) between classes and their dependencies. Instead of a class creating its dependencies internally, they are provided externally, typically by a framework or a container. This approach leads to more modular and testable code.
Inversion of Control (IoC) is a broader principle where the control of object creation and lifecycle is inverted from the object itself to an external entity. DI is one of the most common ways to implement IoC.
DI is a specific implementation of the IoC principle. While IoC can be achieved through various methods like service locators or event-driven architectures, DI focuses on providing dependencies directly to objects, thus promoting loose coupling and enhancing testability.
Ruby, with its dynamic nature, provides several ways to implement DI. We will explore both manual DI and using frameworks like Dry::Container.
Manual DI involves explicitly passing dependencies to a class, either through constructors, setters, or interfaces.
Constructor Injection is the most common form of DI, where dependencies are provided through a class’s constructor.
1class DatabaseConnection
2 def initialize(adapter)
3 @adapter = adapter
4 end
5
6 def connect
7 @adapter.connect
8 end
9end
10
11class MySQLAdapter
12 def connect
13 puts "Connecting to MySQL database..."
14 end
15end
16
17# Injecting dependency manually
18adapter = MySQLAdapter.new
19db_connection = DatabaseConnection.new(adapter)
20db_connection.connect
In this example, DatabaseConnection is decoupled from the specific MySQLAdapter implementation, allowing for easy substitution with other adapters.
Setter Injection involves providing dependencies through setter methods.
1class DatabaseConnection
2 attr_writer :adapter
3
4 def connect
5 @adapter.connect
6 end
7end
8
9# Using setter injection
10db_connection = DatabaseConnection.new
11db_connection.adapter = MySQLAdapter.new
12db_connection.connect
Setter Injection provides flexibility in changing dependencies at runtime but requires additional methods for setting dependencies.
Interface Injection is less common in Ruby due to its dynamic nature but involves providing dependencies through an interface method.
1module Adapter
2 def set_adapter(adapter)
3 @adapter = adapter
4 end
5end
6
7class DatabaseConnection
8 include Adapter
9
10 def connect
11 @adapter.connect
12 end
13end
14
15# Using interface injection
16db_connection = DatabaseConnection.new
17db_connection.set_adapter(MySQLAdapter.new)
18db_connection.connect
While manual DI is straightforward, using a DI framework can simplify dependency management, especially in larger applications. One popular Ruby library for DI is Dry::Container.
Dry::Container is part of the dry-rb ecosystem, providing a simple and flexible way to manage dependencies.
1require 'dry/container'
2
3class DatabaseConnection
4 def initialize(adapter)
5 @adapter = adapter
6 end
7
8 def connect
9 @adapter.connect
10 end
11end
12
13class MySQLAdapter
14 def connect
15 puts "Connecting to MySQL database..."
16 end
17end
18
19# Setting up the container
20container = Dry::Container.new
21
22container.register(:adapter) { MySQLAdapter.new }
23container.register(:db_connection) { DatabaseConnection.new(container.resolve(:adapter)) }
24
25# Resolving dependencies
26db_connection = container.resolve(:db_connection)
27db_connection.connect
In this example, Dry::Container manages the creation and injection of dependencies, promoting a clean separation of concerns.
To better understand the flow of dependencies in DI, let’s visualize the process using a class diagram.
classDiagram
class DatabaseConnection {
-adapter
+connect()
}
class MySQLAdapter {
+connect()
}
DatabaseConnection --> MySQLAdapter : Dependency
This diagram illustrates how DatabaseConnection depends on MySQLAdapter, with the dependency being injected externally.
Experiment with the code examples provided by:
MySQLAdapter to simulate a different database connection.DatabaseConnection.Dry::Container to manage additional dependencies.Remember, mastering Dependency Injection and Inversion of Control is a journey. As you progress, you’ll find these patterns invaluable in building robust and maintainable Ruby applications. Keep experimenting, stay curious, and enjoy the journey!