Last active
May 27, 2016 13:14
-
-
Save mattbeedle/4e3555b3b23729741b90 to your computer and use it in GitHub Desktop.
Hexagonal / DDD / Architecture Astronauting ;)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
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
Love the idea of a Publisher! ❤️ The code of the use case looks sooo clean.