UsersController
User
Cost of change
mantainability cost
¯\_(ツ)_/¯
SignedHttpClient
AnalyticsClient
HttpClient
AnalyticsClient
HttpClient
AnalyticsClient.http_client = Typhoeus
Configuring the class
analytics_client.submit(name: 'SignUp', http_client: Typhoeus)
As an argument
client = AnalyticsClient.new(http_client: Typhoeus)
Configuring the instance
The caller decides
Common in Rails with initializers
Classes are singletons
Most flexible
We build structure that represents your application config.
Let this structure build the instances
And configure them
We can achieve the picture that we imagined before
# Gemfile
gem 'little_boxes'
# box.rb
require 'little_boxes'
class MainBox
include LittleBoxes::Box
let(:logger) { Logger.new('application.log') }
end
> MainBox.new
=> #<MainBox :logger>
> box.logger
=> #<Logger:0x0056366a76c810 ...>
> box.logger
=> #<Logger:0x0056366a76c810 ...>
require 'typhoeus'
class AnalyticsService
def submit(event)
response = Typhoeus.post(
'http://example.com/events',
body: event
)
response.code == 201
end
end
> service = AnalyticsService.new
=> #<AnalyticsService:0x0055fd585398e0>
> service.submit(name: "SignUp")
=> true
class AnalyticsService
attr_accessor :http_client
attr_accessor :server_url
def initialize(http_client:, server_url:)
@http_client = http_client
@server_url = server_url
end
def submit(event)
response = http_client.post(
"#{server_url}/events",
body: event
)
response.code == 201
end
end
> require 'typhoeus'
> service = AnalyticsService.new(http_client: Typhoeus, server_url: "http://example.com")
=> #<AnalyticsService:0x00561d6851b860 @http_client=Typhoeus, @server_url="http://example.com">
> service.submit(name: "SignUp")
=> true
class AnalyticsService
attr_accessor :http_client
attr_accessor :server_url
def initialize(http_client:, server_url:)
@http_client = http_client
@server_url = server_url
end
def submit(event)
response = http_client.post(
"#{server_url}/events",
body: event
)
response.code == 201
end
end
> box = MainBox.new
=> #<MainBox :analytics_service, :http_client, :server_url>
> box.analytics_service.submit(name: "SignUp")
=> true
class MainBox
include LittleBoxes::Box
let(:analytics_service) do |box|
AnalyticsService.new(
http_client: box.http_client,
server_url: box.server_url
)
end
let(:http_client) do
require 'typhoeus'
Typhoeus
end
let(:server_url) { "https://example.com" }
end
way too much typing!
Image courtesy of giphy
way too much typing!
class AnalyticsService
include LittleBoxes::Configurable
dependency :http_client
dependency :server_url
def submit(event)
response = http_client.post(
"#{server_url}/events",
body: event
)
response.code == 201
end
end
> box = MainBox.new
=> #<MainBox :analytics_service, :http_client, :server_url>
> box.analytics_service.submit(name: "SignUp")
=> true
class MainBox
include LittleBoxes::Box
letc(:analytics_service) do
AnalyticsService.new
end
let(:http_client) do
require 'typhoeus'
Typhoeus
end
let(:server_url) { "https://example.com" }
end
Image courtesy of giphy
class AnalyticsService
attr_accessor :http_client
attr_accessor :server_url
def initialize(http_client:, server_url:)
@http_client = http_client
@server_url = server_url
end
def submit(event)
response = http_client.post(
"#{server_url}/events",
body: event
)
response.code == 201
end
end
class MainBox
include LittleBoxes::Box
let(:analytics_service) do |box|
AnalyticsService.new(
http_client: box.http_client,
server_url: box.server_url
)
end
let(:http_client) do
require 'typhoeus'
Typhoeus
end
let(:server_url) { "https://example.com" }
end
> box = MainBox.new
=> #<MainBox :analytics_service, :http_client, :server_url>
class MainBox
include LittleBoxes::Box
letc(:analytics_service) { AnalyticsService.new }
let(:http_client) { require 'typhoeus'; Typhoeus }
let(:server_url) { "https://example.com" }
end
> service = box.analytics_service
=> #<AnalyticsService:0x00563aa942c1f8 @config={:box=>#<MainBox...>}>
> service.submit({})
> service
=> #<AnalyticsService:0x00563aa942c1f8 @config={:box=>#<MainBox...>,
:http_client=>Typhoeus, :server_url=>"https://example.com"}>
# class_dependency() for class dependencies
class_dependency(:logger)
# default values with callbacks
dependency(:log) { Logger.new('/dev/null') }
dependency(:log) { |box| box.logger }
# then() callbacks to override dependencies by hand
letc(:analytics) { AnalyticsService.new }.then do |service, box|
service.logger = box.debug_logger
end
# get() and getc() don't memoize
get(:service_url) { 'http://example.com' }
# eager_let, eager_get, etc. to force them to run at boot time
eager_let(:aws_sdk) do
require 'aws-sdk'
Aws.use_bundled_cert!
Aws
end
class MainBox
include LittleBoxes::Box
let(:logger) { Logger.new('./todos.log') }
let(:http_client) { require 'typhoeus'; Typhoeus }
let(:todos_model) { Todo }
let(:todos_create_endpoint) { TodosCreateEndpoint.new }
let(:todos_list_endpoint) { TodosListEndpoint.new }
let(:todos_delete_endpoint) { TodosDeleteEndpoint.new }
letc(:analytics_service) { AnalyticsService.new }
let(:analytics_url) { "https://example.com" }
end
> box = MainBox.new
=> #<MainBox :logger, :todos_model, :todos_create_endpoint, :todos_list_endpoint,
:todos_delete_endpoint, :analytics_service, :analytics_url, :http_client>
class MainBox
include LittleBoxes::Box
let(:logger) { Logger.new('./todos.log') }
let(:http_client) { require 'typhoeus'; Typhoeus }
box(:todos) do
let(:model) { Todo }
box(:endpoints) do
let(:create) { TodosCreateEndpoint.new }
let(:list) { TodosListEndpoint.new }
let(:delete) { TodosDeleteEndpoint.new }
end
end
box(:analytics) do
letc(:service) { AnalyticsService.new }
let(:url) { "https://example.com" }
end
end
> box = MainBox.new
=> #<MainBox :logger, :todos, :analytics, :http_client>
> box.todos
=> #<Box[todos] :model, :endpoints>
MainBox
todos
model
create
endpoints
list
delete
client
logger
service
url
analytics
MainBox
╟─ logger
╟─ todos
║ ╟─ model
║ ╙─ endpoints
║ ╟─ create
║ ╟─ list
║ ╙─ delete
╙─ analytics
╟─ service
╟─ client
╙─ url
class TodosBox
include LittleBoxes::Box
let(:model) { Todo }
box(:endpoints) do
let(:create) { TodosCreateEndpoint.new }
let(:list) { TodosListEndpoint.new }
let(:delete) { TodosDeleteEndpoint.new }
end
end
class MainBox
include LittleBoxes::Box
box :todos, TodosBox
box :analytics, AnalyticsBox
let(:http_client) { require 'typhoeus'; Typhoeus }
let(:logger) { Logger.new('./todos.log') }
end
> box = MainBox.new
=> #<MainBox :logger, :todos, :analytics, :http_client>
> box.todos
=> #<Box[todos] :model, :endpoints>
manuel@manuel-XPS-L322X:~/ws/todo$ ls -l
total 44
-rwxr-xr-x 1 manuel manuel 99 Jan 13 14:49 cli
-rw-rw-r-- 1 manuel manuel 56 Mar 5 08:31 config.ru
-rw-rw-r-- 1 manuel manuel 44 Mar 4 19:17 config-sample.yml
-rw-rw-r-- 1 manuel manuel 44 Mar 4 19:34 config.yml
-rw-rw-r-- 1 manuel manuel 213 Mar 5 08:42 Gemfile
-rw-rw-r-- 1 manuel manuel 1157 Mar 5 08:39 Gemfile.lock
drwxrwxr-x 3 manuel manuel 4096 Mar 4 21:10 lib
-rw-rw-r-- 1 manuel manuel 31 Mar 5 08:31 Procfile
-rw-rw-r-- 1 manuel manuel 256 Mar 5 08:55 Rakefile
-rw-rw-r-- 1 manuel manuel 1251 Mar 5 09:02 README.md
drwxrwxr-x 4 manuel manuel 4096 Mar 12 21:46 spec
manuel@manuel-XPS-L322X:~/ws/todo$ ./cli test
....................................................
Finished in 0.14894 seconds (files took 0.17862 seconds to load)
52 examples, 0 failures
manuel@manuel-XPS-L322X:~/ws/todo$ ./cli console
Use `box = Todo::Box.new` to inspect the app itself
[1] pry(main)> box = Todo::Box.new
=> #<Todo::Box :app, :cfg, :rack, :todos>
[2] pry(main)> box.todos
=> #<Box[todos] :entity, :repo, :endpoints>
[3] pry(main)> box.todos.repo
=> #<Todo::TodosRepo:0x0055ac0f718db8 @config={:new_todo=>#<Method: Class#new>}>
[4] pry(main)> box.todos.repo.find_all
=> []
RSpec.describe AnalyticsService do
let(:service) do
AnalyticsService.new http_client: http_client, server_url: url
end
let(:url) { 'http://example.com' }
let(:event) { {name: 'UserCreated'} }
let(:http_client) { double(:http_client, post: response) }
let(:response) { double(:response, code: 201) }
it 'posts the event' do
expect(http_client).
to receive(:post).
with('http://example.com/events', body: event)
service.submit event
end
end
require_relative '../helper'
require 'rack/test'
RSpec.describe 'API' do
include Rack::Test::Methods
let(:box) { TodosBox.new }
let(:app) { box.rack_app }
describe 'GET /todos' do
it 'returns 200' do
get '/todos'
expect(last_response.status).to eq 200
end
end
end
...