Architecture & dependency management in Ruby/Rails

Forkert Pavel

Rubyist for over than 4 years

Has committed to various opensource projects

DevOps at Wix.com

In the previous episodes...

OOP, FP & Dependency management in Ruby/Rails
http://fxposter.github.io/dependency-management-presentation/
We Have To Go Deeper
class MediaService::PictureDatasource
  def initialize(cdn)
    @cdn = cdn
  end

  def delete(path)
    # delete ...
    cdn.delete(path)
  end
end
class MediaService::PictureDatasource
  def delete(path)
    # delete ...
    MediaService.cdn.delete(path)
  end
end
class MediaService::PictureDatasource
  def delete(path)
    # delete ...
    Akamai.new(:username => 'A',
               :password => 'B',
               :hostname => 'C').delete(path)
  end
end

Are those options interchangeable?

class MediaService::PictureDatasource
  def initialize(cdn)
    @cdn = cdn
  end

  def delete(path)
    # delete ...
    cdn.delete(path)
  end
end
class MediaService::PictureDatasource
  def delete(path)
    # delete ...
    MediaService.cdn.delete(path)
  end
end
MediaService.with_cdn(akamai) do
  datasource.delete(path)
end

Dependency Inversion?

Yes, but that's not what I'd like to point out

Locality of the objects

MyGem.configure do |config|
end

MyGem.call_method
config = MyGem::Config.new(:a => 'b')
client = MyGem::Client.new(config)
client.call_method

Dependency Inversion -> Interface Ownership

But... Ruby does not have interfaces...

Does it need interface ownership?

class AddsProductToCart
  def initialize(user)
    @user = user
  end

  def call(product)
    @user.cart << product
  end
end
class AddsProductToCart
  def call(product)
    if product.published? && @user.is_customer_of?(product.vendor)
      @user.cart << product
    end
  end
end
class User
  def cart
  end
end
class User
  def profile
  end
end

class Profile
  def cart
  end
end
user.cart -> user.profile.cart
class AddsProductToCart
  def call(product)
    if product.published? && @user.is_customer_of?(product.vendor)
      @user.cart << product
    end
  end
end

Use adapters!

Don't mock what you don't own

"Interface ownership" how to

When you create a new object and see that it needs some dependency, then define dependency's interface in terms of what you need from it, not in terms of what objects/classes you actually have in the application already

"Interface ownership" and external gems

What if this class is in some gem?

class AddsProductToCart
  def initialize(user)
    @user = user
  end

  def call(product)
    if product.published? && @user.is_customer_of?(product.vendor)
      @user.cart << product
    end
  end
end

Check out rails_admin

Treat and design your objects so that you don't need to change them every time something else is changed. Let something else change the way the object works.

Dependency inversion in Rails apps?

DHH

Wat?

Rails is OMAKASE!

http://david.heinemeierhansson.com/2012/rails-is-omakase.html

class ExportController
  def export
    MyApp::Zookeeper.export_current_data_to_production
    MyApp::Zookeeper.export_current_data_to_staging
    MyApp::Zookeeper.export_current_data_to_ci
  end
end

vs

class ExportController
  def export
    zk = MyApp::Zookeeper.new(production_config)
    zk.export(SomeModel.current_data_for(SomeModel.env(:production)))
  end
end

Another layer

MyApp::Zookeeper.export_current_data_to_production knows about your application, but the parts it is using does not know anything.

It is the starting point for the "whole new world"

module MyApp::Zookeeper
  def self.export_current_data_to_production
    dumper = ProductionDataDumper.new(database_records,
                                      config[:production_prefix])
    export(client_for(config[:production]), dumper.dump)
  end
end

export_current_data_to_production is not reusable and swappable by itself, but the export method and ProductionDataDumper are.

class ApplicationController
  def zookeeper
    @zookeeper ||= MyApp::Zookeeper
    # MyApp::Zookeeper.new(Rails.config(:zookeeper))
  end
end

class SomeController < ApplicationController
  def production_export
    zookeeper.export(Model.export_records)
  end
end

To sum up

  • Minimize global state in gems
  • Applications is less restrictive in terms of global state, but still "local" stuff is much more manageable
  • Dependencies' interfaces should actually be "object which needs the dependency"-centric, not the "use what you already have"-centric
  • Abstract stuff and try not to break the abstractions every time you need something
  • Analyze your solutions, think what are the pros and cons
  • Happy hacking!

Thanks!