arrow_back All posts

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

  1. Identify tightly coupled components in your codebase.
  2. Introduce abstractions to decouple high-level and low-level modules.
  3. Inject dependencies externally rather than creating them inside a class.
  4. Leverage your framework’s built-in control flow (IoC) wherever possible.

Additional Resources