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.
7. Enable Labs @mark_menard
The Wyriki Domain
Page
Wiki
*
1
Create Wiki
Create Page
Update
Page
Create User
Loged
In User
Anony
mous
User
8. Enable Labs @mark_menard
Business Logic
ActiveRecord
ActionPack
Controllers
MySQL MongoDB PostgreSQL
Redis
Sidekiq
Resque
What was Jim trying to accomplish?
12. Enable Labs @mark_menard
Create Page
Loged
In User
Action
Controller ::
Base
ActiveRecord
:: Base
Pages
Controller
Application
Controller
Page
Wiki
13. Enable Labs @mark_menard
Create Page
Loged
In User
Action
Controller ::
Base
ActiveRecord
:: Base
Pages
Controller
Application
Controller
Page
Wiki
14. Enable Labs @mark_menard
Create Page
Loged
In User
Action
Controller ::
Base
ActiveRecord
:: Base
Pages
Controller
Application
Controller
Page
Wiki
15. 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
17. Enable Labs @mark_menard
Runners
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
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
18. 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>>
19. Enable Labs @mark_menard
Action
Controller ::
Base
ActiveRecord
:: Base
Page
Controller
Application
Controller
Some Model
Runner
Repo
Biz Model
<< wraps >>
<< gets and saves
stuff >>
20. Enable Labs @mark_menard
Action
Controller ::
Base
ActiveRecord
:: Base
Page
Controller
Application
Controller
Some Model
Runner
Repo
Biz Model
<< wraps >>
<< gets and saves
stuff >>
24. 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
31. 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
32. 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
33. 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
34. 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
35. Enable Labs @mark_menard
Enough Architecture! !
What about the Ruby!!
!
How did Jim actually !
do the callbacks and the
runners?
54. Enable Labs @mark_menard
Action
Controller ::
Base
ActiveRecord
:: Base
Page
Controller
Application
Controller
Page
Wiki
Create Page
Runner
<<protocol>>
Repo
Repo
<<protocol>>
context
55. Enable Labs @mark_menard
Action
Controller ::
Base
ActiveRecord
:: Base
Page
Controller
Application
Controller
Page
Wiki
Create Page
Runner
<<protocol>>
Repo
Repo
<<protocol>>
context
Domain
56. Enable Labs @mark_menard
Action
Controller ::
Base
ActiveRecord
:: Base
Page
Controller
Application
Controller
Page
Wiki
Create Page
Runner
<<protocol>>
Repo
Repo
<<protocol>>
context
Domain
57. Enable Labs @mark_menard
Action
Controller ::
Base
ActiveRecord
:: Base
Page
Controller
Application
Controller
Page
Wiki
Create Page
Runner
<<protocol>>
Repo
Repo
<<protocol>>
context
Domain
58. Enable Labs @mark_menard
# app/services/wiki_repository.rb
class WikiRepository
include Repo::UserMethods
include Repo::WikiMethods
include Repo::PageMethods
include Repo::PermissionMethods
end
59. 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
64. 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
65. 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
66. 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
67. 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
68. 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
69. 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
70. 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
71. 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
72. 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
73. 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