-
-
Save mattbeedle/4e3555b3b23729741b90 to your computer and use it in GitHub Desktop.
# 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 |
Love the idea of a Publisher! ❤️ The code of the use case looks sooo clean.
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.
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.