Skip to content

Instantly share code, notes, and snippets.

@mattbeedle
Last active May 27, 2016 13:14
Show Gist options
  • Save mattbeedle/4e3555b3b23729741b90 to your computer and use it in GitHub Desktop.
Save mattbeedle/4e3555b3b23729741b90 to your computer and use it in GitHub Desktop.
Hexagonal / DDD / Architecture Astronauting ;)
# Here's my experimentation so far with DDD and hexagonal architecture from the past few weeks.
# Code all written straight into this gist so probably full of typos and bugs.
# App consists of controllers, use_cases, policies, responses, services, forms, repositories, adapters, errors.
# Everything can be unit tested and mocked using bogus (https://www.relishapp.com/bogus/bogus/v/0-1-5/docs)
# Controllers can be tested with Rack::Test to ensure they run authetication and call the correct use case.
# All business logic is isolated from the delivery mechanism (grape) so that the delivery mechanism could
# be switched out.
# It would be easy for example to switch to Torqubox and JRuby to take advantage of the massive
# 10000 req/s performance (http://www.madebymarket.com/blog/dev/ruby-web-benchmark-report.html) compared
# to the slowness of Grape.
# controllers (application domain)
# Handle authentication and calling use cases
# use_cases (business domain)
# Tie buisiness logic together. Comparable to a story card in agile, e.g UserUpdatesLead.
# These are the only things a controller has access to. In theory the entire framework could be
# switched out, or the app could be inserted into a ruby motion codebase, or terminal application, etc
# just by calling the use cases.
# policies (business domain)
# Responsible for authorizating a user to perform a particular action on some object
# responses (application domain)
# These are part of the application domain rather than the business domain. They dry up the controller.
# They handle all the different responses that can happen from a use case.
# services (business domain)
# These are responsible for interacting with 3rd party services.
# They send email, send data to segement.io, etc. Usually by calling a background job.
# forms (business domain)
# These are responsible for validation and syncing data with models. Using reform.
# repositories (business domain)
# Removing the active record dependency from the app. These delegate persistence actions to a database
# adapter. In this case ActiveRecordAdapter
# adapters (business domain)
# These provide a unified interface to databases. They all need to define the basic CRUD operations and
# also a "unit of work" so that groups of database actions can be rolled back on failure.
# errors (business domain)
# These define business specific errors rather than just using the standard ones. Also map database specific
# errors to business ones so that the database can be switched out easily.
# MISSING
# Entities: So that active record models do not have to be used in the use cases.
# An entity would be passed to a repository and that would know how to sync it with the model.
# Mediators: Maybe use cases are taking care of too much. It could be really cool if use cases just run
# finders and authorization and then pass the objects through to a mediator to do the actual work. This
# would massively simplify testing too.
# The pattr_initialize stuff in the examples comes from: https://github.com/barsoom/attr_extras
# Controller just totally skinny. Only task is to authenticate the current user and run the appropriate use case
class API::V1::Leads < Grape::API
helpers do
def update_lead_use_case
@update_lead_use_case ||= build_update_lead_use_case
end
def build_update_lead_use_case
UpdateLeadUseCase.new(current_user, params[:id], params[:lead]).
tap do |use_case|
use_case.add_subscriber LeadUpdatedMailerService.new
use_case.add_subscriber LeadUpdateResponse.new(self)
end
end
end
resource :leads do
route_param :id do
patch { update_lead_use_case.run }
end
end
end
# This module is included into anything that needs to support the observer pattern.
# So far, just use cases.
module Publisher
def add_subscriber(object)
@subscribers ||= []
@subscribers << object
end
def publish(message, *args)
@subscribers.each do |subscriber|
subscriber.send(message, *args) if subscriber.respond_to?(message)
end if @subscribers
end
end
# Use cases. These connect the main business logic. Each use case would correspond to one
# story card on the wall. Perhaps the name could be better. Something like "UserUpdatesLeadUseCase".
# Maybe it could be modified even further to just take care of creating objects, authorizing, etc and
# then calling a Mediator to run the actual update?
class UpdateLeadUseCase
include ActiveSupport::Rescuable
include Publisher
rescue_from NotAuthorizedException, with: :not_authorized
rescue_from InvalidException, with: :invalid
pattr_initialize :user, :id, :attributes
attr_writer :form, :policy, :repository
def run
authorize!
validate!
update!
rescue Exception => e
rescue_with_handler(e)
end
private
def repository
@repository ||= LeadRepository.new
end
def policy
@policy ||= LeadPolicy.new(user, lead)
end
def form
@form ||= LeadForm.new(lead)
end
def lead
@lead ||= repository.find id
end
def authorize!
raise NotAuthorizedException unless policy.update?
end
def validate!
raise InvalidException unless form.validate(attributes)
end
def update!
form.sync
repository.save!(lead)
publish :updated_successfully, lead
end
def not_authorized(exception)
publish :not_authorized, exception
end
def invalid(exception)
publish :invalid, exception
end
end
class LeadForm < Reform::Form
model :lead
property :name
validates :name, presence: true
end
# Takes care of authorization logic
class LeadPolicy
pattr_initialize :user, :lead
def update?
lead.user_id == user.id
end
end
# This is to dry up the controller. It's a kind of subscriber and can be
# passed into any use case. It delegates all of it's methods to the controller
class LeadUpdateResponse < SimpleDelegator
def updated_successfully(lead)
present :lead, lead
end
def invalid(exception)
error!({ errors: exception.errors }, 422)
end
def not_authorized(exception)
error!({ message: exception.message }, 401)
end
end
# Another example of a subscriber. It also responds to the same updated_successfully
# and then schedules an email to be sent via some background process
# (using sucker_punch syntax in the example)
class LeadUpdatedMailerService
def updated_successfully(lead)
LeadUpdatedMailerJob.new.async.perform(user)
end
end
# Repository pattern to hide active record totally from the business logic.
# Wraps a database adapter
class BaseRepository
def find(id)
adapter.find id
end
def save(object)
adapter.save(object)
end
def destroy(object)
adapter.destroy(object)
end
def unit_of_work
adapter.unit_of_work
end
private
def not_found(exception)
raise RecordNotFoundException.new(exception)
end
def adapter
@adapter ||= ActiveRecordAdapter.new(database_klass)
end
end
# Lead specific repository with nicely named finders
def LeadRepository < BaseRepository
def find_for_user_by_name(user, name)
adapter.query do |dao|
dao.where(user_id: user.id, name: name)
end
end
private
def database_klass
Lead
end
end
# Provides an interface to ActiveRecord objects
# and returns errors defined in the business domain
# instead of ActiveRecord specific ones. In theory
# could be switched out for a MongoidAdapter or
# Neo4JAdapter or MemoryAdapter.
# Also provides a "UnitOfWork" which is a way of running
# a collection of tasks with rollback if they fail. In
# this case just a wrapper for a transaction
class ActiveRecordAdapter
include ActiveSupport::Rescuable
rescue_from ActiveRecord::RecordNotFound, with: :not_found
pattr_initialize :persistence
def find(id)
persistence.find id
rescue Exception => e
rescue_with_handler(e)
end
def save(object)
object.save
rescue Exception => e
rescue_with_handler(e)
end
def destroy(object)
object.destroy
rescue Exception => e
rescue_with_handler(e)
end
def query(&block)
yield(persistence)
rescue Exception => e
rescue_with_handler(e)
end
def unit_of_work
@unit_of_work ||= ActiveRecordAdapter::UnitOfWork.new
end
def method_missing(method_syn, *arguments, &block)
persistence.send(method_sym, *arguments, &block)
rescue Exception => e
rescue_with_handler(e)
end
private
def not_found(exception)
raise RecordNotFoundException.new(exception)
end
end
# Errors would need to be defined to cover everything
class RecordNotFoundException < StandardError
end
# Wrapper for a transaction. Usage:
# repository = LeadRepository.new
# unit_of_work = repository.unit_of_work
# unit_of_work.run do
# repository.save!(lead)
# repository.save!(another_lead)
# some_other_repository.save!(something_else)
# end
class ActiveRecordAdapter::UnitOfWork
def run(&block)
ActiveRecord::Base.transaction do
yield
end
end
end
@soulim
Copy link

soulim commented Aug 29, 2014

That looks impressive! 👍

I think you are right, a Mediator could be used to make actual work on objects which were prepared by a UseCase. In think case a UseCase is a higher layer on top of a Mediator.

Do you still like the idea of a Repository patter? It sounds good to me as an idea, but it was hard to use this approach in real life with Code Conformity.

@soulim
Copy link

soulim commented Aug 29, 2014

Love the idea of a Publisher! ❤️ The code of the use case looks sooo clean.

@mattbeedle
Copy link
Author

I'm still not totally sure about the repository pattern. I also like the idea, but haven't successfully used it in practice. I find it especially awkward to decide where to filter things for the current_user for example. I seem to end up defining lots of find_for_user_by_something methods in the repositories. I think perhaps it was difficult in Code Conformity though because of Datamappify. It was basically impossible there to built complex queries/joins.

I was inspired to look into the pattern again by https://github.com/lotus/model. That looks like a much nicer Data Mapper pattern implementation. In fact, in a new project, I would probably just try using lotusrb framework.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment