-
-
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.