SOLID: Dependency Inversion Principle in Ruby


Created by Stan on 18-04-2023


The Dependency Inversion Principle (DIP) is one of the five principles of object-oriented programming and design known as SOLID. It promotes the decoupling of high-level and low-level modules in a system by depending on abstractions rather than concrete implementations. In this article, we will explore the Dependency Inversion Principle and demonstrate how to apply it in a Ruby application.

Understanding the Dependency Inversion Principle

The Dependency Inversion Principle consists of two key rules:
- 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.

Applying the Dependency Inversion Principle in Ruby

We can apply DIP in a Ruby application using interfaces (in the form of abstract base classes or duck typing) or dependency injection. In this article, we'll demonstrate DIP using duck typing and dependency injection.

Consider a scenario where we have a DataExporter class that exports data to different formats, such as JSON or CSV.

First, we set up two classes, JsonExporter and CsvExporter.

require 'json'

class JsonExporter
  def export(data)
    json_data = data.to_json
    File.open('data.json', 'w') do |file|
      file.write(json_data)
    end
    puts 'Data exported to data.json'
  end
end
require 'csv'

class CsvExporter
  def export(data)
    CSV.open('data.csv', 'w') do |csv|
      data.each do |row|
        csv << row
      end
    end
    puts 'Data exported to data.csv'
  end
end

Without applying DIP, our DataExporter class would look like this.

class DataExporter
  def export_data(data)
    json_exporter = JsonExporter.new
    json_exporter.export(data)
    csv_exporter = CsvExporter.new
    csv_exporter.export(data)
  end
end

In this example, the DataExporter class directly creates instances of JsonExporter and CsvExporter, making it tightly coupled to those implementations. This violates the DIP and makes the code less flexible and maintainable.

We will now apply DIP to the DataExporter class using duck-typing and dependency injection.

class DataExporter
  def initialize(exporters)
    @exporters = exporters
  end

  def export_data(data)
    @exporters.each do |exporter|
      exporter.export(data)
    end
  end
end

By applying DIP, we've decoupled the DataExporter class from the specific implementations of JsonExporter and CsvExporter. The DataExporter class now depends on an abstract concept of an "exporter" rather than concrete implementations. This makes the code more flexible, maintainable, and testable.

To use the new DataExporter, we would inject the dependencies like this:

books = [
  { title: 'The Catcher in the Rye', author: 'J.D. Salinger', year: 1951 },
  { title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
  { title: 'Pride and Prejudice', author: 'Jane Austen', year: 1813 },
  { title: '1984', author: 'George Orwell', year: 1949 },
  { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 }
]

# Map books to CSV data
csv_data = books.map { |book| [book[:title], book[:author], book[:year]] }

# Add headers for CSV data
csv_data.unshift(['Title', 'Author', 'Year'])


json_exporter = JsonExporter.new
csv_exporter = CsvExporter.new
data_exporter = DataExporter.new([json_exporter, csv_exporter])

data_exporter.export_data(books) # JSON export
data_exporter.export_data(csv_data) # CSV export

In this example, we've created custom data in the form of an array of hashes representing books. We then convert this data into a CSV-friendly format by creating an array of arrays and adding a header row.

Finally, we initialize the JsonExporter, CsvExporter, and DataExporter instances and use the export_data method to export the custom data in both JSON and CSV formats. The exported files, 'data.json' and 'data.csv', will contain the book data in their respective formats.



Related Posts