Skip to content

Instantly share code, notes, and snippets.

@booch
Last active October 27, 2015 19:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save booch/3b32ec5a3557c9e6ccbf to your computer and use it in GitHub Desktop.
Save booch/3b32ec5a3557c9e6ccbf to your computer and use it in GitHub Desktop.
MORF (generic hexagonal Ruby framework) ideas
# MORF - My Own Ruby Framework #
# This is based on Uncle Bob's talk at Midwest Ruby 2011,
# [Architecture the Lost Years](https://www.youtube.com/watch?v=WpkDN78P884),
# as well as his article on [Clean Architecture](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html).
# Also inspired by Gary Bernhardt's [Boundaries](https://www.youtube.com/watch?v=yTkzNHF6rMs) talk,
# and lots of other immutability (functional core) ideas being used within OOP programming (like Piotr Solnica's work).
ENV['DB_URL'] = 'memory:morf_example'
module MORF
def self.Interaction(*args)
Interaction.tap do |modul|
# modul.extend Blah if args[:blah]
end
end
def self.DomainObject(*args)
DomainObject
end
def self.Entity(*args)
Entity
end
def self.Repo(*args)
Repo
end
end
module MORF::Web
def self.Controller(*args)
Controller
end
end
require "riposte"
module MORF
module Interaction
include Riposte::Helper
def initialize(datastore: = nil)
@datastore = datastore
end
private
def datastore
@datastore ||= ENV["DB_URL"]
end
end
end
module MORF
# A domain object encodes business logic in the problem domain space. This is typically not specific to the application.
module DomainObject
def self.included(base)
base.class_eval do
def self.attribute(name, type=String, options={})
attributes << Attribute.new(name.to_sym, type, options)
attr_reader name
end
def self.attributes
@attributes ||= []
end
def self.copy(original, attribute_values = {})
self.new(original.attributes.merge(attribute_values))
end
end
end
def initialize(attribute_values = {})
attribute_values.each do |k,v|
# TODO: Perform any necessary type coercions.
instance_variable_set("@#{k}".to_sym, v) if attributes.map(&:name).include?(k.to_sym)
# TODO: What should we do about any values passed in for undefined attributes?
end
# TODO: Ensure all required attributes have been set.
# TODO: Perform any necessary validations.
end
# Return a copy of the object, with updated attributes.
def copy(attribute_values={})
self.class.copy(self, attribute_values)
end
def attributes
self.class.attributes
end
# TODO: Need to be able to compare domain objects to each other.
class Attribute
attr_reader :name, :type, :options
def initialize(name, type=String, options={})
@name = name
@type = type
@options = options
end
end
end
end
module MORF
# An Entity is a DomainObject with an ID.
# Entities are immutable by default.
module Entity
def self.included(base)
base.class_eval do
include DomainObject
attribute :id, Integer
end
end
end
end
module MORF
module Repo
def initialize(datastore:)
end
def insert(entity)
# TODO: Grab entity's attributes, shove them into data store.
end
def get(id)
# TODO: Grab entity from datastore by its ID.
end
def all
# TODO: Grab all entities from datastore.
end
def find(*criteria)
# TODO: Grab all entities from datastore that match the criteria.
end
end
end
module MORF::Web
class Response
attr_accessor :code
attr_accessor :headers
attr_accessor :body
end
module Rendering
def render
# TODO: Return a Response.
end
def redirect(to:)
Response.new(code: 303, headers: {Location: to}, body: '')
end
end
module Controller
include Rendering
attr_reader :request
def initialize(request:)
@request = request
end
end
end
# Interaction / Interactions (Uncle Bob calls these Interactors or Use Cases)
# Other possible names: Transaction, Command, Function, Feature, Service Object.
# "Function" is bad, because that conflicts with functions in programming languages.
# "Transaction" is bad, because that conflicts with DB transactions.
# "Command" is questionable, because the GoF command requires the client code to know about the entities involved.
# However, Russ Olson's "Design Patterns in Ruby" ignores that.
#require "morf"
#require "repo/pact"
#require "entitity/pact"
class CreatePact
include MORF.Interaction
def call(description:, deadline:)
pact = Pact.new(description: description, deadline: deadline)
repo.insert(pact)
end
private
# Should probably move this to MORF::Interaction, but that would require determining the repo class.
def repo
@repo ||= PactRepo.new(datastore: datastore)
end
end
# Entity (Model)
#require "morf"
class Pact
include MORF.Entity
attribute :description, String
attribute :deadline, DateTime
end
# Repo (might be a thin layer on top of Sequel)
#require "morf"
#require "entity/pact"
class PactRepo
include MORF.Repo(entity_class: Pact, table: "pacts")
end
# Web Page Controller
#require "morf/web"
class Web::PactController
include MORF::Web.Controller
# TODO: Need to handle URL paths that include IDs, and collections versus items.
def post(params)
# FIXME: I don't like this at all. The `new` and `call` seem superfluous.
CreatePact.new.call(description: params[:description], deadline: params[:deadline]) do |on|
on.display { |pact| render "pact/create", pact: PactPresentation.new(pact) }
on.permission_denied { render "pact/permission_denied" }
end
# TODO: Return a Response object (render and redirect return such an object).
# Or a String, that will be turned into a Response object.
end
end
# Web Front Controller (maybe)
# Form Object
# Validations
# Authentication
# Presentation
# View
### Testing our example Interation.
# Note that we can use `.()` as a shorthand for `.call()`.
CreatePact.new.(description: 'Blah', deadline: Date.today)
### Considerations
# Authentication (interactor requiring something from the controller)
# Authorization (interactor)
# Web security (CSRF, XSS, CORS)
# Web caching
# Web hypermedia (multiple representations for a given resource)
# Server push (Server-sent events, Web Sockets, HTTP 2)
# Optimistic locking (harder with immutable entities)
# Unit of Work pattern is important
# Need to possibly worry about wrapping DB accesses within transactions
# Should consider interactors that are combinations of other interactors
# Should the repo for an entity named `Person` be called `PersonRepo`, `People`, `Person::Repo`, or `Repo::People`?
# I think I prefer Repo::People.
### Differences from Uncle Bob
# Still can't tell from top level what the app is for
# Some of the names have been changed
# I think passing entities around in Ruby is fine, as long as entities are immutable
# Because then the entity attributes can be lazy, and not generated if they're not needed
# Because hashes are stringly-typed, so don't prevent trying to access the wrong attribute
# Repository pattern
# But I have the repo depend on the entity it contains, in contrast to https://subvisual.co/blog/posts/20-clean-architecture
# One reason: I want to be able to have scopes that depend on info contained in the entity
# Not sure my use of a DB/persistence URL is sufficiently abstracting away that dependecy direction
# TO CONSIDER: How do we DRY up the various actions that all act on the same kind of object?
# Perhaps we can do some of what Decent Exposure does.
class OrderInteractions
def initialize(repository: = nil)
@repository = repository
end
def list
end
def create
end
def get(id, &block)
order = repository.find(id)
# Think I'd prefer if this was `respond_with` instead of `react_to`.
react_to :display, order, &block
rescue RecordNotFound
react_to :not_found, id, &block
rescue Exception => exception
react_to :exception, exception, id, &block
end
def update
end
def delete
end
private
def repository
@repository ||= OrderRepo.new
end
def order(id)
end
def orders
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment