Mastering IoC, DIP, and DI in Flutter and Ruby on Rails
Oct 23, 2024
Inversion of Control (IoC)
What is IoC?
Inversion of Control (IoC) is a design principle that reverses the traditional flow of control in a program. Instead of application code calling a library or framework, the framework calls the application code.
Traditional Approach
In the traditional approach, your code is responsible for controlling the flow of execution — calling libraries, managing dependencies, and directing operations.
Inversion of Control
With IoC, you hand over control to the framework. The framework decides when and how to call your code. This separates the task of execution from its implementation.
Why Use IoC?
- Decoupling: Reduces tight coupling between components.
- Reusability: Components can be reused across different contexts.
- Testability: Easier to test components in isolation.
- Maintainability: Changes in one part don’t ripple through the entire system.
IoC in Flutter
Flutter’s runApp() is a great example of IoC. You hand control to the framework, and Flutter takes care of rendering your widget tree.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp()); // Flutter takes control here
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// The framework calls this method to build the widget tree
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('IoC in Flutter'),
),
body: Center(
child: Text('Hello, Inversion of Control!'),
),
),
);
}
} IoC in Ruby on Rails
Rails manages the request-response cycle and route mapping for you. You define routes and controllers; the framework decides when to invoke them.
# config/routes.rb
Rails.application.routes.draw do
resources :articles
end
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all
# Rails automatically renders the corresponding view
end
end Summary
IoC shifts control from your code to the framework, enabling cleaner, more modular architecture.
Dependency Inversion Principle (DIP)
Understanding DIP
The Dependency Inversion Principle is the “D” in SOLID. It states:
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Benefits of DIP
- Reduces coupling between high-level and low-level components.
- Makes it easy to swap implementations without changing business logic.
- Increases flexibility and testability.
DIP in Flutter
Step 1: Define an Abstraction
abstract class NotificationService {
void send(String message);
} Step 2: Implement Concrete Classes
class EmailNotificationService implements NotificationService {
@override
void send(String message) {
print('Email sent: $message');
}
}
class SMSNotificationService implements NotificationService {
@override
void send(String message) {
print('SMS sent: $message');
}
} Step 3: High-Level Module Depends on Abstraction
class NotificationManager {
final NotificationService service;
NotificationManager(this.service);
void notify(String message) {
service.send(message);
}
} Usage
void main() {
NotificationService service = EmailNotificationService();
NotificationManager manager = NotificationManager(service);
manager.notify('Hello, DIP in Flutter!');
} DIP in Ruby on Rails
Step 1: Define an Interface
module PaymentGateway
def process(amount)
raise NotImplementedError, 'You must implement the process method'
end
end Step 2: Implement Concrete Classes
class StripeGateway
include PaymentGateway
def process(amount)
puts "Processed $#{amount} with Stripe."
end
end
class PaypalGateway
include PaymentGateway
def process(amount)
puts "Processed $#{amount} with PayPal."
end
end Step 3: High-Level Module Depends on Abstraction
class PaymentProcessor
def initialize(gateway)
@gateway = gateway
end
def pay(amount)
@gateway.process(amount)
end
end Usage
gateway = StripeGateway.new
processor = PaymentProcessor.new(gateway)
processor.pay(100) Dependency Injection (DI)
What is Dependency Injection?
Dependency Injection is a technique where an object receives its dependencies from external sources rather than creating them itself. Dependencies are supplied through constructors, setters, or method parameters.
Types of Dependency Injection
- Constructor Injection: Dependencies are provided via the constructor.
- Setter Injection: Dependencies are set via setter methods.
- Method Injection: Dependencies are passed as method parameters.
DI in Flutter
Without DI
class UserService {
final UserRepository _repository = UserRepository();
void fetchUser() {
_repository.getUser();
}
} With Constructor Injection
class UserService {
final UserRepository repository;
UserService(this.repository);
void fetchUser() {
repository.getUser();
}
} Mock Implementation for Testing
class MockUserRepository implements UserRepository {
@override
void getUser() {
print('Mock user data fetched');
}
} Testing
void main() {
UserRepository mockRepository = MockUserRepository();
UserService service = UserService(mockRepository);
service.fetchUser(); // Outputs: Mock user data fetched
} DI in Ruby on Rails
Without DI
class UserNotifier
def send_welcome_email(user)
mailer = Mailer.new
mailer.send_email(user.email, 'Welcome!')
end
end With Setter Injection
class UserNotifier
attr_writer :mailer
def send_welcome_email(user)
mailer.send_email(user.email, 'Welcome!')
end
private
def mailer
@mailer ||= Mailer.new
end
end Mock Implementation for Testing
class MockMailer
def send_email(email, message)
puts "Mock email sent to #{email} with message: #{message}"
end
end Testing
notifier = UserNotifier.new
notifier.mailer = MockMailer.new
notifier.send_welcome_email(user)
# Outputs: Mock email sent to user@example.com with message: Welcome! Conclusion
Benefits
Applying IoC, DIP, and DI in your codebase produces applications that are:
- Maintainable: Easier to update and extend.
- Testable: Components can be tested in isolation with mocks.
- Flexible: Implementations can be swapped without changing business logic.
- Scalable: Decoupled architecture grows cleanly with your product.
Key Takeaways
- Identify tightly coupled components in your codebase.
- Introduce abstractions to decouple high-level and low-level modules.
- Inject dependencies externally rather than creating them inside a class.
- Leverage your framework’s built-in control flow (IoC) wherever possible.
Additional Resources
- Clean Code by Robert C. Martin
- Inversion of Control Containers and the Dependency Injection Pattern by Martin Fowler