Dependency Injection with Dry::Container
Todo app example:
require 'dry/container' module Models class Task attr_reader :id attr_accessor :text, :priority def initialize(id:, text:, priority:) @id = id @text = text @priority = priority end end class Priority attr_reader :value LOW = 0 MEDIUM = 1 HIGH = 2 ALL = [LOW, MEDIUM, HIGH] def initialize(value) raise "Invalid priority value." unless ALL.include?(value) @value = value end def max? @value == HIGH end def min? @value == LOW end def up return self if max? self.class.new(@value + 1) end def down return self if min? self.class.new(@value - 1) end def self.low self.new LOW end end end module Factories class Task def initialize(enumerator:) @enumerator = enumerator end def call(text:, priority:) task_id = @enumerator.next Models::Task.new( id: task_id, text: text, priority: priority, ) end end end module Repositories class Tasks def initialize @tasks = [] end # commands def add(task) @tasks.push(task) end def remove_by_id(task_id) @tasks = @tasks.select { |task| task.id != task_id } end # queries def list @tasks.sort_by { |task| task.priority.value }.reverse end def find_by_id(task_id) @tasks.detect { |task| task.id == task_id } end end end module TaskPrinters class Base def call(task) raise NotImplementedError, 'Provide implementation for #call' end end class Plain < Base def call(task) "- #{task.id}: #{task.text}" end end class Csv < Base def call(task) "#{task.id},#{task.text},#{task.priority.value}" end end end module UseCases class PrintTasks def initialize(task_printer:, tasks_repository:) @task_printer = task_printer @tasks_repository = tasks_repository end def call @tasks_repository.list.each do |task| output = @task_printer.call(task) puts output end end end class AddTask def initialize(task_factory:, tasks_repository:) @task_factory = task_factory @tasks_repository = tasks_repository end def call(text) task = @task_factory.call( text: text, priority: Models::Priority.low ) @tasks_repository.add(task) end end class RemoveTask def initialize(tasks_repository:) @tasks_repository = tasks_repository end def call(task_id) @tasks_repository.remove_by_id(task_id) end end class EditTask def initialize(tasks_repository:) @tasks_repository = tasks_repository end def call(task_id:, text:) task = @tasks_repository.find_by_id(task_id) return if task.nil? task.text = text end end class UpTask def initialize(tasks_repository:) @tasks_repository = tasks_repository end def call(task_id:) task = @tasks_repository.find_by_id(task_id) return if task.nil? task.priority = task.priority.up end end class DownTask def initialize(tasks_repository:) @tasks_repository = tasks_repository end def call(task_id:) task = @tasks_repository.find_by_id(task_id) return if task.nil? task.priority = task.priority.down end end end module Container def self.build container = Dry::Container.new container.namespace('todo') do register(:task_enumerator, memoize: true) do (0..Float::INFINITY).to_enum end register(:task_factory, memoize: true) do Factories::Task.new( enumerator: resolve(:task_enumerator), ) end register(:tasks_repository, memoize: true) do Repositories::Tasks.new end register(:plain_printer, memoize: true) do TaskPrinters::Plain.new end register(:csv_printer, memoize: true) do TaskPrinters::Csv.new end register(:print_tasks) do UseCases::PrintTasks.new( tasks_repository: resolve(:tasks_repository), task_printer: resolve(:plain_printer), ) end register(:add_task) do UseCases::AddTask.new( tasks_repository: resolve(:tasks_repository), task_factory: resolve(:task_factory), ) end register(:remove_task) do UseCases::RemoveTask.new( tasks_repository: resolve(:tasks_repository), ) end register(:edit_task) do UseCases::EditTask.new( tasks_repository: resolve(:tasks_repository), ) end register(:up_task) do UseCases::UpTask.new( tasks_repository: resolve(:tasks_repository), ) end register(:down_task) do UseCases::DownTask.new( tasks_repository: resolve(:tasks_repository), ) end end end end if __FILE__ == $0 container = Container.build add_task = container.resolve('todo.add_task') print_tasks = container.resolve('todo.print_tasks') remove_task = container.resolve('todo.remove_task') edit_task = container.resolve('todo.edit_task') up_task = container.resolve('todo.up_task') down_task = container.resolve('todo.down_task') puts 'Create 2 tasks:' add_task.call('A task example!') add_task.call('Another task!') up_task.call(task_id: 0) print_tasks.call puts 'Edit task:' edit_task.call(task_id: 1, text: 'Text updated!') print_tasks.call puts 'Increase priority:' up_task.call(task_id: 1) print_tasks.call puts 'Remove a task:' remove_task.call(0) print_tasks.call end
Tests example:
require 'dry/container/stub' require 'rspec' require './main' RSpec.describe Repositories::Tasks do it 'is empty initially' do expect(subject.list).to be_empty end context 'with tasks' do let(:priority0) { double('priority0', value: Models::Priority::HIGH) } let(:priority1) { double('priority1', value: Models::Priority::MEDIUM) } let(:task0) { double('task0', id: 0, priority: priority0) } let(:task1) { double('task1', id: 1, priority: priority1) } before do subject.add(task0) subject.add(task1) end describe '#list' do it 'returns all tasks' do expect(subject.list).to eq([task0, task1]) end end describe '#find_by_id' do it 'returns task for the specified id' do expect(subject.find_by_id(task0.id)).to eq(task0) end it 'returns nil if not found' do expect(subject.find_by_id(2)).to be_nil end end describe '#remove_by_id' do it 'removes task with specified id' do expect { subject.remove_by_id(task0.id) } .to change { subject.list.size }.by(-1) end it 'does not remove if not found' do expect { subject.remove_by_id(2) } .not_to change { subject.list.size } end end end end RSpec.describe UseCases::AddTask do let(:enumerator) { double('enumerator', next: 1) } let(:task_factory) { Factories::Task.new(enumerator: enumerator) } let(:tasks_repository) { double('tasks_repository', add: nil) } let(:text) { 'Test text' } subject do described_class.new( task_factory: task_factory, tasks_repository: tasks_repository, ) end it 'adds task to the repository' do subject.call(text) expect(tasks_repository).to have_received(:add).with(Models::Task) do |task| expect(task.priority.min?).to be_truthy expect(task.text).to eq(text) end end end RSpec.describe 'A container test' do let(:task) do Models::Task.new( id: 1, text: 'Example text.', priority: Models::Priority.low ) end let(:tasks_repository) do double('tasks_repository', add: nil, list: [task]) end subject { Container.build } before do subject.enable_stubs! subject.stub('todo.tasks_repository', tasks_repository) end it 'prints added task' do print_tasks = subject.resolve('todo.print_tasks') expect { print_tasks.call }.to output("- 1: Example text.\n").to_stdout end end
Licensed under CC BY-SA 3.0