Decorator and Presenter usage example
Single responsibility principle
a class should have only a single responsibility (i.e. changes to only one part of the software's specification should be able to affect the specification of the class).
Open/closed principle
"software entities … should be open for extension, but closed for modification."
Liskov substitution principle
"objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program." See also design by contract.
Interface segregation principle
"many client-specific interfaces are better than one general-purpose interface."
Dependency inversion principle
"high level modules should not depend upon low level modules. Both should depend upon abstractions"
We have all been there when you see your controller action getting way too long and hold too much business logic.
You know you need to email the user, adjust an account, maybe submit to Stripe and finally ping a Slack Webhook.
Well, where should it go? This code needs to exist and doesn’t seem to fit in the model.
This my friend, is where Service Objects come in!
class Analytics
def self.get_data(algolia_application)
usage = {}
acl = %w[
total_write_operations
total_read_operations
records
]
uri = URI("https://status.algolia.com/1/usage/#{acl.join(',')}/period/month")
res = perform_request(uri, algolia_application['monitoring_api_key'])
json = JSON.parse(res.body)
return [:bad_request, error: json['reason']] if json['reason'].present?
# ...
[:ok, usage]
end
def self.retrieve_analytics_information
# ...
end
end
Class based with the following signature:
initialize()
execute()
execute!()
class Analytics
attr_reader :algolia_application
def initialize(algolia_application)
@algolia_application = algolia_application
end
def execute(from:)
usage = {}
acl = %w[
total_write_operations
total_read_operations
records
]
uri = URI("https://status.algolia.com/1/usage/#{acl.join(',')}/period/month")
res = perform_request(uri, algolia_application['monitoring_api_key'])
json = JSON.parse(res.body)
return [:bad_request, error: json['reason']] if json['reason'].present?
# ...
[:ok, usage]
end
private
def retrieve_analytics_information
# ...
end
end
analytics = Analytics.new(config.algolia_application)
analytics.execute(from: 1.month)
analytics.execute(from: 2.month)
class AuthenticateUser
include Interactor
def call
if user = User.authenticate(context.email, context.password)
context.user = user
context.token = user.secret_token
else
context.fail!(message: "authenticate_user.failure")
end
end
end
class SessionsController < ApplicationController
def create
result = AuthenticateUser.call(session_params)
if result.success?
session[:user_token] = result.token
redirect_to result.user
else
flash.now[:message] = t(result.message)
render :new
end
end
private
def session_params
params.require(:session).permit(:email, :password)
end
end
Interactors (building blocks)
Organizers (orchestrate interactors)
# our Analytics Organizer
class StoreAnalytics
include Interactor::Organizer
organize Analytics::RetrieveAnalytics, Analytics::RetrieveUsage
end
### ...
## in controller
def analytics
result = StoreAnalytics.call(algolia_application)
if result.success?
result.usage
result.analytics
render AnalyticsPresenter.new(result).as_json
else
render status: result.error.status, json: result.error.json
end
end