Top Banner
Enable Labs @mark_menard A Tour of Wyriki Mark Menard Ruby Nation June 7, 2014 @mark_menard Enable Labs
84

A Tour of Wyriki

Jan 19, 2015

Download

Technology

Mark Menard

Jim Weirich gave us many things. Among his last was Wyriki, a small Rails app described in his own words as an "Experimental Rails application to explore decoupling app logic from Rails." Many of us paid our final respects to Jim on his last commit to this project. Now it's time to learn from it.

In this talk we'll explore how Jim applied the principles of Object Oriented Design to achieve his goals of decoupling; look at how he used decoupling to speed up testing; how decoupling improved and simplified his tests; and look at his design style. Jim's legacy leaves a lot to learn from, let's do it.
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: A Tour of Wyriki

Enable Labs @mark_menard

A Tour of Wyriki

Mark Menard

Ruby Nation!June 7, 2014

@mark_menard !Enable Labs

Page 2: A Tour of Wyriki

Enable Labs @mark_menard

Jim Weirich

Page 3: A Tour of Wyriki

Enable Labs @mark_menardhttp://www.flickr.com/photos/langalex

Page 4: A Tour of Wyriki

Enable Labs @mark_menard

TDD

Page 5: A Tour of Wyriki

Enable Labs @mark_menard

Yea… whatever…

Page 6: A Tour of Wyriki

Enable Labs @mark_menard

What is Wyriki?

Page 7: A Tour of Wyriki

Enable Labs @mark_menard

The Wyriki Domain

Page

Wiki

*

1

Create Wiki

Create Page

Update Page

Create User

Loged In User

Anonymous User

Page 8: A Tour of Wyriki

Enable Labs @mark_menard

Business Logic

ActiveRecord

ActionPack

Controllers

MySQL MongoDB PostgreSQL

Redis

Sidekiq

Resque

What was Jim trying to accomplish?

Page 9: A Tour of Wyriki

Enable Labs @mark_menard

Testing

Page 10: A Tour of Wyriki

Enable Labs @mark_menard

Why?!!

When?

Page 11: A Tour of Wyriki

Enable Labs @mark_menard

Typical Rails

Page 12: A Tour of Wyriki

Enable Labs @mark_menard

Create Page

Loged In User

Action Controller ::

Base

ActiveRecord :: Base

Pages Controller

Application Controller

Page

Wiki

Page 13: A Tour of Wyriki

Enable Labs @mark_menard

Create Page

Loged In User

Action Controller ::

Base

ActiveRecord :: Base

Pages Controller

Application Controller

Page

Wiki

Page 14: A Tour of Wyriki

Enable Labs @mark_menard

Create Page

Loged In User

Action Controller ::

Base

ActiveRecord :: Base

Pages Controller

Application Controller

Page

Wiki

Page 15: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/pages_controller.rb def create @wiki = Wiki.find(params[:wiki_id]) @page = @wiki.pages.new(page_params) if @page.save redirect_to [@wiki, @page], notice: "#{@page.name} created" else render :new end end

Page 16: A Tour of Wyriki

Enable Labs @mark_menard

Wyriki Style

Page 17: A Tour of Wyriki

Enable Labs @mark_menard

Runnersclass Runner attr_reader :context ! def initialize(context) @callbacks = NamedCallbacks.new @context = context yield(@callbacks) if block_given? end ! def repo context.repo end ! def success(*args) callback(:success, *args) end ! def failure(*args) callback(:failure, *args) end ! def callback(name, *args) @callbacks.call(name, *args) args end end

class Model < SimpleDelegator include BlockActiveRecord ! def data datum = self while datum.biz? datum = datum.__getobj__ end datum end ! def ==(other) if other.respond_to?(:data) data == other.data else data == other end end ! def biz? true end ! def class data.class end ! def self.wrap(model) model ? new(model) : nil end ! def self.wraps(models) models.map { |model| wrap(model) } end !end

Business Models

Repositories

module UserMethods def all_users Biz::User.wraps(User.all_users) end ! def new_user(attrs={}) Biz::User.wrap(User.new(attrs)) end ! def find_user(user_id) Biz::User.wrap(User.find(user_id)) end ! def save_user(user) user.data.save end ! def update_user(user, attrs) user.data.update_attributes(attrs) end ! def destroy_user(user_id) User.destroy(user_id) end end

Page 18: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Page

Wiki

Create Page Runner

<<protocol>>Repo

Repo

<<protocol>>context

<<protocol>>Biz Page

<<protocol>>Biz Wiki Biz::Wiki

Biz::Page

<<protocol>>Wiki Data

<<protocol>>Page Data<<wraps>>

<<wraps>>

Page 19: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Some Model

Runner

Repo

Biz Model

<< wraps >><< gets and saves

stuff >>

Page 20: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Some Model

Runner

Repo

Biz Model

<< wraps >><< gets and saves

stuff >>

Page 21: A Tour of Wyriki

Enable Labs @mark_menard

Page 22: A Tour of Wyriki

Enable Labs @mark_menard

Runners

Page 23: A Tour of Wyriki

Enable Labs @mark_menard

Page 24: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Page

Wiki

Create Page Runner

<<protocol>>context

Rails

Not Rails

Create Page

Loged In User

Page 25: A Tour of Wyriki

Enable Labs @mark_menard

This is the !Domain

Page 26: A Tour of Wyriki

Enable Labs @mark_menard

This is the !Domain

This is Rails

Page 27: A Tour of Wyriki

Enable Labs @mark_menard

This is our Context.

Page 28: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/page_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 29: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/page_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 30: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/page_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 31: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/page_runners.rb class Create < Runner def run(wiki_id, page_params) wiki = Wiki.find(params[:wiki_id]) page = wiki.pages.new(page_params) if page.save success(page) else failure(wiki, page) end end end

Page 32: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/page_runners.rb class Create < Runner def run(wiki_id, page_params) wiki = Wiki.find(params[:wiki_id]) page = wiki.pages.new(page_params) if page.save success(page) else failure(wiki, page) end end end

# app/controllers/page_controller.rb def create Create.new(self, params[:wiki_id], page_params).run do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 33: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/page_runners.rb class Create < Runner def run(wiki_id, page_params) wiki = Wiki.find(params[:wiki_id]) page = wiki.pages.new(page_params) if page.save success(page) else failure(wiki, page) end end end

# app/controllers/page_controller.rb def create Create.new(self, params[:wiki_id], page_params).run do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 34: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Page

Wiki

Create Page Runner

<<protocol>>context

Rails

Not Rails

Create Page

Loged In User

Page 35: A Tour of Wyriki

Enable Labs @mark_menard

Enough Architecture! !What about the Ruby!!

!

How did Jim actually !do the callbacks and the

runners?

Page 36: A Tour of Wyriki

Enable Labs @mark_menard

Runner

Named Callbacks

<<protocol>>context

<<protocol>>Repo

Page 37: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Runner

Named Callbacks

<<protocol>>context

<<protocol>>Repo

Page 38: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Runner

Named Callbacks

<<protocol>>context

<<protocol>>Repo

Page 39: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Runner

Named Callbacks

<<protocol>>context

<<protocol>>Repo

Page 40: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/runner.rb class Runner attr_reader :context ! def initialize(context) @callbacks = NamedCallbacks.new @context = context yield(@callbacks) if block_given? end ! def repo context.repo end ! def success(*args) callback(:success, *args) end ! def failure(*args) callback(:failure, *args) end ! def callback(name, *args) @callbacks.call(name, *args) args end end

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Runner

Named Callbacks

<<protocol>>context

<<protocol>>Repo

Page 41: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/runner.rb class Runner attr_reader :context ! def initialize(context) @callbacks = NamedCallbacks.new @context = context yield(@callbacks) if block_given? end ! def repo context.repo end ! def success(*args) callback(:success, *args) end ! def failure(*args) callback(:failure, *args) end ! def callback(name, *args) @callbacks.call(name, *args) args end end

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Runner

Named Callbacks

<<protocol>>context

<<protocol>>Repo

Page 42: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/runner.rb class Runner attr_reader :context ! def initialize(context) @callbacks = NamedCallbacks.new @context = context yield(@callbacks) if block_given? end ! # … end

# app/runners/named_callbacks.rb class NamedCallbacks def initialize @callbacks = {} end ! def method_missing(sym, *args, &block) @callbacks[sym] = block end ! # … end

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 43: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/runner.rb class Runner attr_reader :context ! def initialize(context) @callbacks = NamedCallbacks.new @context = context yield(@callbacks) if block_given? end ! # … end

# app/runners/named_callbacks.rb class NamedCallbacks def initialize @callbacks = {} end ! def method_missing(sym, *args, &block) @callbacks[sym] = block end ! # … end

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 44: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/named_callbacks.rb class NamedCallbacks def initialize @callbacks = {} end ! def method_missing(sym, *args, &block) @callbacks[sym] = block end ! # … end

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 45: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/named_callbacks.rb class NamedCallbacks def initialize @callbacks = {} end ! def method_missing(sym, *args, &block) @callbacks[sym] = block end ! # … end

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 46: A Tour of Wyriki

Enable Labs @mark_menard

# app/runners/named_callbacks.rb class NamedCallbacks def initialize @callbacks = {} end ! def method_missing(sym, *args, &block) @callbacks[sym] = block end ! # … end

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Page 47: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Weirich Block Style

Page 48: A Tour of Wyriki

Enable Labs @mark_menard

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

# app/controllers/pages_controller.rb def create run(Create, params[:wiki_id], page_params) do |on| on.success { |page| redirect_to [page.wiki, page], notice: "#{page.name} created" } on.failure { |wiki, page| render :new } end end

Weirich Block Style

Page 49: A Tour of Wyriki

Enable Labs @mark_menard

Some Lessons

Page 50: A Tour of Wyriki

Enable Labs @mark_menard

Repositories

Page 51: A Tour of Wyriki

Enable Labs @mark_menard

Domain Rails

Page 52: A Tour of Wyriki

Enable Labs @mark_menard

Page 53: A Tour of Wyriki

Enable Labs @mark_menard

Page 54: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Page

WikiCreate Page Runner

<<protocol>>Repo Repo

<<protocol>>context

Page 55: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Page

WikiCreate Page Runner

<<protocol>>Repo Repo

<<protocol>>context

Domain

Page 56: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Page

WikiCreate Page Runner

<<protocol>>Repo Repo

<<protocol>>context

Domain

Page 57: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Page

WikiCreate Page Runner

<<protocol>>Repo Repo

<<protocol>>context

Domain

Page 58: A Tour of Wyriki

Enable Labs @mark_menard

# app/services/wiki_repository.rb class WikiRepository include Repo::UserMethods include Repo::WikiMethods include Repo::PageMethods include Repo::PermissionMethods end

Page 59: A Tour of Wyriki

Enable Labs @mark_menard

# app/services/repo/page_methods.rb module PageMethods def find_wiki_page(wiki_id, page_id) wiki = Wiki.find(wiki_id) page = wiki.pages.find(page_id) ! # … end ! # … ! def save_page(page) page.data.save end ! # … end

Page 60: A Tour of Wyriki

Enable Labs @mark_menard

Domain

Page 61: A Tour of Wyriki

Enable Labs @mark_menard

Biz Objects

Page 62: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Page

WikiCreate Page Runner

<<protocol>>Repo Repo

<<protocol>>context

Domain

Page 63: A Tour of Wyriki

Enable Labs @mark_menard

Biz Model ActiveRecord :: Base

Simple Delegator

Page 64: A Tour of Wyriki

Enable Labs @mark_menard

# app/models/biz/model.rb module Biz class Model < SimpleDelegator include BlockActiveRecord ! def data datum = self while datum.biz? datum = datum.__getobj__ end datum end ! def ==(other) if other.respond_to?(:data) data == other.data else data == other end end

def biz? true end ! def class data.class end ! def self.wrap(model) model ? new(model) : nil end ! def self.wraps(models) models.map { |model| wrap(model) } end ! end end

Page 65: A Tour of Wyriki

Enable Labs @mark_menard

# app/models/biz/model.rb module Biz class Model < SimpleDelegator include BlockActiveRecord ! def data datum = self while datum.biz? datum = datum.__getobj__ end datum end ! def ==(other) if other.respond_to?(:data) data == other.data else data == other end end

def biz? true end ! def class data.class end ! def self.wrap(model) model ? new(model) : nil end ! def self.wraps(models) models.map { |model| wrap(model) } end ! end end

Page 66: A Tour of Wyriki

Enable Labs @mark_menard

# app/models/biz/model.rb module Biz class Model < SimpleDelegator include BlockActiveRecord ! def data datum = self while datum.biz? datum = datum.__getobj__ end datum end ! def ==(other) if other.respond_to?(:data) data == other.data else data == other end end

def biz? true end ! def class data.class end ! def self.wrap(model) model ? new(model) : nil end ! def self.wraps(models) models.map { |model| wrap(model) } end ! end end

Page 67: A Tour of Wyriki

Enable Labs @mark_menard

# app/models/biz/model.rb module Biz class Model < SimpleDelegator include BlockActiveRecord ! def data datum = self while datum.biz? datum = datum.__getobj__ end datum end ! def ==(other) if other.respond_to?(:data) data == other.data else data == other end end

def biz? true end ! def class data.class end ! def self.wrap(model) model ? new(model) : nil end ! def self.wraps(models) models.map { |model| wrap(model) } end ! end end

Page 68: A Tour of Wyriki

Enable Labs @mark_menard

# app/models/biz/model.rb module Biz class Model < SimpleDelegator include BlockActiveRecord ! def data datum = self while datum.biz? datum = datum.__getobj__ end datum end ! def ==(other) if other.respond_to?(:data) data == other.data else data == other end end

def biz? true end ! def class data.class end ! def self.wrap(model) model ? new(model) : nil end ! def self.wraps(models) models.map { |model| wrap(model) } end ! end end

Page 69: A Tour of Wyriki

Enable Labs @mark_menard

# app/models/biz/model.rb module Biz class Model < SimpleDelegator include BlockActiveRecord ! def data datum = self while datum.biz? datum = datum.__getobj__ end datum end ! def ==(other) if other.respond_to?(:data) data == other.data else data == other end end

def biz? true end ! def class data.class end ! def self.wrap(model) model ? new(model) : nil end ! def self.wraps(models) models.map { |model| wrap(model) } end ! end end

Page 70: A Tour of Wyriki

Enable Labs @mark_menard

# app/services/repo/page_methods.rb module PageMethods def find_wiki_page(wiki_id, page_id) wiki = Wiki.find(wiki_id) page = wiki.pages.find(page_id) Biz::Page.wrap(page) end ! # … ! def save_page(page) page.data.save end ! # … end

Page 71: A Tour of Wyriki

Enable Labs @mark_menard

# app/services/repo/page_methods.rb module PageMethods def find_wiki_page(wiki_id, page_id) wiki = Wiki.find(wiki_id) page = wiki.pages.find(page_id) Biz::Page.wrap(page) end ! # … ! def save_page(page) page.data.save end ! # … end

Page 72: A Tour of Wyriki

Enable Labs @mark_menard

module Biz class Page < Model def wiki Biz::Wiki.wrap(super) end ! def html_content(context) Kramdown::Document.new(referenced_content(context)).to_html end ! def referenced_content(context) content.gsub(/(([A-Z][a-z0-9]+){2,})/) { |page_name| if wiki.page?(context.repo, page_name) "[#{page_name}](#{context.named_page_path(wiki.name,page_name)})" elsif context.current_user.can_write?(wiki) "#{page_name}[?](#{context.new_named_page_path(wiki.name, page_name)})" else page_name end } end end end

Page 73: A Tour of Wyriki

Enable Labs @mark_menard

module Biz class Page < Model def wiki Biz::Wiki.wrap(super) end ! def html_content(context) Kramdown::Document.new(referenced_content(context)).to_html end ! def referenced_content(context) content.gsub(/(([A-Z][a-z0-9]+){2,})/) { |page_name| if wiki.page?(context.repo, page_name) "[#{page_name}](#{context.named_page_path(wiki.name,page_name)})" elsif context.current_user.can_write?(wiki) "#{page_name}[?](#{context.new_named_page_path(wiki.name, page_name)})" else page_name end } end end end

Page 74: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Some Model

Runner

Repo

Biz Model

<< wraps >>

Page 75: A Tour of Wyriki

Enable Labs @mark_menard

Action Controller ::

Base

ActiveRecord :: Base

Page Controller

Application Controller

Some Model

Runner

Repo

Biz Model

<< wraps >>

Page 76: A Tour of Wyriki

Enable Labs @mark_menard

More Lessons

Page 77: A Tour of Wyriki

Enable Labs @mark_menard

Why?

Page 78: A Tour of Wyriki

Enable Labs @mark_menard

Isolated Business Logic

Page 79: A Tour of Wyriki

Enable Labs @mark_menard

Incremental Approach

Page 80: A Tour of Wyriki

Enable Labs @mark_menard

Fast Tests

Page 81: A Tour of Wyriki

Enable Labs @mark_menard

$ time rspec spec/runners spec/models/biz (git)-[master] ...................................................................................................... !Finished in 0.17573 seconds 102 examples, 0 failures rspec spec/runners spec/models/biz 0.61s user 0.07s system 99% cpu 0.683 total

Page 82: A Tour of Wyriki

Enable Labs @mark_menard

Should we decouple?

Page 83: A Tour of Wyriki

Enable Labs @mark_menard

http://www.flickr.com/photos/dwortlehock/

Thanks for Everything Jim!

Page 84: A Tour of Wyriki

Enable Labs @mark_menard

Start Today

http://www.enablelabs.com/

[email protected]

866-895-8189

Enable Labs@mark_menard