A Tour of Wyriki

Post on 19-Jan-2015

175 Views

Category:

Technology

1 Downloads

Preview:

Click to see full reader

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/

mark@enablelabs.com

866-895-8189

Enable Labs@mark_menard

top related