diy di in ruby
TRANSCRIPT
MeNikita Shilnikov
• github.com/flash-gordon
• dry-rb and rom-rb core team member
4/103
Method
def user_repo # dependency resolutionend
def call(name) user_repo.create(name: name)end
16/103
Constant
Pros:
• Dumb simple
• Easy to follow
• You don't need a method
Cons:
• High coupling
• Hard to test (?)
• Less extendable18/103
Method
let(:fake_repo) { FakeRepo.new }let(:create_user) { CreateUser.new(fake_repo) }
example do create_user.(name: 'Jade')end
20/103
Dependency Inversion PrincipleA. High-level modules should not depend on low-level modules. Both should depend on abstractions.B. Abstractions should not depend on details. Details should depend on abstractions.
25/103
class CreateUser attr_reader :user_repo
def initialize(user_repo) @user_repo = user_repo endend
32/103
ContainerContainer = { user_repo: UserRepo.new }
def initialize(user_repo = nil) @user_repo = user_repo || Container[:user_repo]end
33/103
create_user = CreateUser.new(UserRepo.new)create_user = CreateUser.newcreate_user = CreateUser.new(FakeRepo.new)
36/103
create_user = CreateUser.new(UserRepo.new)create_user = CreateUser.newcreate_user = CreateUser.new(FakeRepo.new)
before { Container[:user_repo] = FakeRepo.new }
let(:create_user) { CreateUser.new }
example { create_user.(name: 'Jade') }
37/103
Loading order
Conainter = { user_repo: -> { UserRepo.new }, create_user: -> { CreateUser.new }}
40/103
Loading order
Conainter = { user_repo: -> { UserRepo.new }, create_user: -> { CreateUser.new }}
user = Container[:create_user].call.call(name: 'Jade')
41/103
dry-container
Container = Dry::Container.newContainer.register(:user_repo) { UserRepo.new }Container.register(:create_user) { CreateUser.new }
43/103
dry-container
Dir['repositories/*.rb'].each do |path| key = path.sub('repositories/', '').sub('.rb', '') class_name = Inflecto.camelize(key)
Container.register(key) do require path
Inflecto.contantize(class_name).new endend
48/103
dry-container
Dir['repositories/*.rb'].each do |path| key = path.sub('repositories/', '').sub('.rb', '') class_name = Inflecto.camelize(key)
Container.register(key) do require path
Inflecto.contantize(class_name).new endend
49/103
dry-container
Dir['repositories/*.rb'].each do |path| key = path.sub('repositories/', '').sub('.rb', '') class_name = Inflecto.camelize(key)
Container.register(key) do require path
Inflecto.contantize(class_name).new endend
50/103
dry-container
Container.enable_stubs!
around do |ex| Container.stub(:user_repo, FakeRepo.new) { ex.run }end
51/103
class CreateUser attr_reader :user_repo, :user_created_worker
def initialize(user_repo: Container[:user_repo], user_created_worker: Container[:user_created_worker]) @user_repo = user_repo @user_created_worker = user_created_worker endend
53/103
dry-auto_inject
class CreateUserWithAccount include Import['repo.user_repo', 'workers.user_created']
def call(name) user = user_repo.create(name: name) user_created.(user.user_id) user endend
58/103
dry-auto_inject
class CreateUserWithAccount include Import['repo.user_repo', 'workers.user_created']
def call(name) user = user_repo.create(name: name) user_created.(user.user_id) user endend
59/103
dry-auto_inject
class CreateUserFromParams attr_reader :params
def initialize(params) @param = params end
def create UserRepo.new.create(params) endend
62/103
dry-auto_inject
class CreateUserFromParams include Import['user_repo']
attr_reader :params
def initialize(params, **other) super(other)
@param = params endend
63/103
Benefits
• You don't need to think about building objects
• All code is built on the same principles
• It's far easier to write testable code
• You, most likely, will write functional-ish code
• Speeds up tests in a natural way
65/103
dry-system• Extends $LOAD_PATH
• Uses require to load files
• Registers dependencies automatically
• Builds dependencies using name conventions
• Can split an application into sub-apps
69/103
dry-systemclass Application < Dry::System::Container configure do |config| config.root = Pathname('./app') endend
70/103
dry-systemclass Application < Dry::System::Container configure do |config| config.root = Pathname('./app')
config.auto_register = 'lib' end
load_paths!('lib')end
71/103
dry-systemApplication.register('utils.logger', Logger.new($stdout))Application.finalize!Application['utils.logger']
72/103
dry-systemImport = Application.injector
class CreateUser include Import['repos.user_repo', 'utils.logger']
def call(name) # ... endend
73/103
Stateful componentsApplication.finalize(:persistence) do |container| start do require 'sequel' conn = Sequel.connect(ENV['DB_URL']) container.register('persistence.db', conn) end
stop do db.disconnect endend
74/103
Application graph• web.services.create_user [640ms]
• user_repo.create [10ms]• web.view.user_created [215ms]
• external.billing.create_user [278ms]
80/103
Singleton containerImport = Dry::AutoInject(Container)
class CreateUser include Import['repo.user_repo']end
82/103
Sharing the contextApplication.finalize!logger = TaggedLogger.new(env['X-Request-ID'])Application.register('logger', logger) # => Error
84/103
Sharing the contextApplication.register_abstract('logger')Application.finalize!logger = TaggedLogger.new(env['X-Request-ID'])AppWithLogger = Application.provide(logger: logger)
86/103
Sharing the contextImport = Dry::AutoInject(Application)
class CreateUser include Import['logger']end
88/103
Passing the container as a dependency
Container.register('create_user') { |app_with_logger| CreateUser.new(container: app_with_logger)}
90/103
Cons:
• Can't be memoized
• Can violate boundaries
Pros:
• Reduces code duplcation
• Simplifies context sharing
96/103
Cons:
• Can't be memoized
• Can violate boundaries
Pros:
• Reduces code duplcation (no need to pass arguments)
• Simplifies context sharing
• Kills global state
97/103
Recap• DI is a way to implement the dependency inversion
principle
• This makes your code easier to test and write
• Can break an application into sub-apps
• Can remove the global state as a whole (100% FP)
• Can be done in Ruby in one evening
• Doesn't have any major disadvantages *99/103
DI 101• Replace hard dependencies with interfaces (plain Ruby)
• Put dependencies into a container (dry-container)
• Inject dependencies automatically (dry-auto_inject)
• Organize application code in modules (dry-system)
100/103
Caveats
• Don't put every single thing into a container
• Make dependencies pure (aka thread-safe)
• Keep interfaces simple
• Injecting a dozen of deps at once is bad, m'kay?
• Fear new abstractions
101/103
Thank you
• github.com/flash-gordon
• dry-rb.org
• dry-rb/dry-container
• dry-rb/dry-auto_inject
• dry-rb/dry-system
• dry-rb/dry-web-blog
103/103