a tour of wyriki
DESCRIPTION
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.TRANSCRIPT
Enable Labs @mark_menard
A Tour of Wyriki
Mark Menard
Ruby Nation!June 7, 2014
@mark_menard !Enable Labs
Enable Labs @mark_menard
Jim Weirich
Enable Labs @mark_menardhttp://www.flickr.com/photos/langalex
Enable Labs @mark_menard
TDD
Enable Labs @mark_menard
Yea… whatever…
Enable Labs @mark_menard
What is Wyriki?
Enable Labs @mark_menard
The Wyriki Domain
Page
Wiki
*
1
Create Wiki
Create Page
Update Page
Create User
Loged In User
Anonymous User
Enable Labs @mark_menard
Business Logic
ActiveRecord
ActionPack
Controllers
MySQL MongoDB PostgreSQL
Redis
Sidekiq
Resque
What was Jim trying to accomplish?
Enable Labs @mark_menard
Testing
Enable Labs @mark_menard
Why?!!
When?
Enable Labs @mark_menard
Typical Rails
Enable Labs @mark_menard
Create Page
Loged In User
Action Controller ::
Base
ActiveRecord :: Base
Pages Controller
Application Controller
Page
Wiki
Enable Labs @mark_menard
Create Page
Loged In User
Action Controller ::
Base
ActiveRecord :: Base
Pages Controller
Application Controller
Page
Wiki
Enable Labs @mark_menard
Create Page
Loged In User
Action Controller ::
Base
ActiveRecord :: Base
Pages Controller
Application Controller
Page
Wiki
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
Enable Labs @mark_menard
Wyriki Style
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
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>>
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Some Model
Runner
Repo
Biz Model
<< wraps >><< gets and saves
stuff >>
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Some Model
Runner
Repo
Biz Model
<< wraps >><< gets and saves
stuff >>
Enable Labs @mark_menard
Enable Labs @mark_menard
Runners
Enable Labs @mark_menard
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
Enable Labs @mark_menard
This is the !Domain
Enable Labs @mark_menard
This is the !Domain
This is Rails
Enable Labs @mark_menard
This is our Context.
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
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
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
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
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
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
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
Enable Labs @mark_menard
Enough Architecture! !What about the Ruby!!
!
How did Jim actually !do the callbacks and the
runners?
Enable Labs @mark_menard
Runner
Named Callbacks
<<protocol>>context
<<protocol>>Repo
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
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
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
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
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
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
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
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
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
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
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
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
Enable Labs @mark_menard
Some Lessons
Enable Labs @mark_menard
Repositories
Enable Labs @mark_menard
Domain Rails
Enable Labs @mark_menard
Enable Labs @mark_menard
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Page
WikiCreate Page Runner
<<protocol>>Repo Repo
<<protocol>>context
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Page
WikiCreate Page Runner
<<protocol>>Repo Repo
<<protocol>>context
Domain
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Page
WikiCreate Page Runner
<<protocol>>Repo Repo
<<protocol>>context
Domain
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Page
WikiCreate Page Runner
<<protocol>>Repo Repo
<<protocol>>context
Domain
Enable Labs @mark_menard
# app/services/wiki_repository.rb class WikiRepository include Repo::UserMethods include Repo::WikiMethods include Repo::PageMethods include Repo::PermissionMethods end
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
Enable Labs @mark_menard
Domain
Enable Labs @mark_menard
Biz Objects
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Page
WikiCreate Page Runner
<<protocol>>Repo Repo
<<protocol>>context
Domain
Enable Labs @mark_menard
Biz Model ActiveRecord :: Base
Simple Delegator
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
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
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
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
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
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
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
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
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
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
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Some Model
Runner
Repo
Biz Model
<< wraps >>
Enable Labs @mark_menard
Action Controller ::
Base
ActiveRecord :: Base
Page Controller
Application Controller
Some Model
Runner
Repo
Biz Model
<< wraps >>
Enable Labs @mark_menard
More Lessons
Enable Labs @mark_menard
Why?
Enable Labs @mark_menard
Isolated Business Logic
Enable Labs @mark_menard
Incremental Approach
Enable Labs @mark_menard
Fast Tests
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
Enable Labs @mark_menard
Should we decouple?
Enable Labs @mark_menard
http://www.flickr.com/photos/dwortlehock/
Thanks for Everything Jim!
Enable Labs @mark_menard
Start Today
http://www.enablelabs.com/
866-895-8189
Enable Labs@mark_menard