Skip to content

Instantly share code, notes, and snippets.

@booch booch/morf.rb
Last active Oct 27, 2015

Embed
What would you like to do?
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
You can’t perform that action at this time.