Schizo
DCI (data, context and interaction) for Ruby / Rails / ActiveRecord
Install / Use
/learn @cjbottaro/SchizoREADME
== Schizo {<img src="https://secure.travis-ci.org/cjbottaro/schizo.png" />}[http://travis-ci.org/cjbottaro/schizo]
Schizo is a libary that aids in using DCI (data, context and interaction) in Ruby/Rails projects. It aims to overcome some of the shortcomings of using plain <tt>Object#extend</tt>, namely the issue that extending a role can permenantly alter a class.
== Quickstart
Dive in...
class User < ActiveRecord::Base include Schizo::Data end
module Poster extend Schizo::Role
included do
has_many :posts
end
def post_count_in_english
"#{name} has #{posts.count} post(s)"
end
end
user = User.find(1) user.as(Poster) do |poster| poster.respond_to?(:posts) # => true user.respond_to?(:posts) # => false
poster.respond_to?(:post_count_in_english) # => true
user.respond_to?(:post_count_in_english) # => false
poster.kind_of?(User) # => true
poster.instance_of?(User) # => true
poster.class.name # => "User"
end
== DCI
{\http://en.wikipedia.org/wiki/Data,_context_and_interaction}[http://en.wikipedia.org/wiki/Data,_context_and_interaction]
{\http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby}[http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby]
{\http://saturnflyer.com/blog/jim/2011/10/04/oop-dci-and-ruby-what-your-system-is-vs-what-your-system-does/}[http://saturnflyer.com/blog/jim/2011/10/04/oop-dci-and-ruby-what-your-system-is-vs-what-your-system-does/]
{\http://victorsavkin.com/post/13966712168/dci-in-ruby}[http://victorsavkin.com/post/13966712168/dci-in-ruby]
{\http://andrzejonsoftware.blogspot.com/2011/02/dci-and-rails.html}[http://andrzejonsoftware.blogspot.com/2011/02/dci-and-rails.html]
== The Problem
So what's wrong with just using <tt>Object#extend</tt>? Nothing, until you want to avoid altering an instance's class as a side effect of adorning the instance with a role... which happens often when using ActiveRecord.
Consider the following use of DCI and ActiveRecord with plain old <tt>Object#extend</tt>:
class User < ActiveRecord::Base end
module Poster def self.extended(object) object.class.class_eval do has_many :posts end end
def post_count_in_english
"#{name} has #{posts.count} post(s)"
end
end
user1 = User.find(1) user1.extend(Poster) user1.respond_to?(:posts) # Ok
user2 = User.find(2) user2.respond_to?(:posts) # Oops, extending user1 ended up changing all users!
That goes against the core concept in DCI that your data should only be injected with behavior for a specific context.
== The Magic
So how does Schizo work? It creates <i>facade classes</i> and <i>facade objects</i> that stand in for the classes and objects you really want. The facades try to quack as best they can like the real objects/classes.
This is easier to explain in an example (continuing from the Quickstart example):
user = User.find(1) user.as(Poster) do |poster| poster.kind_of?(User) # => true poster.instance_of?(User) # => true
poster.class.name # => "User"
poster.class # => Schizo::Facades::User::Poster
end
<tt>Schizo::Facades::User::Poster</tt> inherits from +User+, that's why <tt>poster.kind_of?(User)</tt> works natrually. <tt>poster.instance_of?(User)</tt> works because of the facade consciously trying to quack like +User+.
== Facades and Objects
So knowing you're working with a facade instead of the original object, some of the gotchas become obvious.
class Foo include Schizo::Data attr_reader :bar def initialize @bar = "low" end end
module Baz extend Schizo::Role def set_bar(value) @bar = value end end
foo = Foo.new baz = foo.as(Baz) baz.set_bar("high") baz.bar # => "high" foo.bar # => "low"
Makes perfect sense, right? But what about this...
foo = Foo.new foo.as(Baz) do |baz| baz.set_bar("high") end foo.bar # => "high"
What?! Nah, it's really simple. At the end of the code block, +baz.actualize+ is called. All +#actualize+ does is copy over the instances variables from the facade to the real object.
You can get the exact same affect by doing:
foo = Foo.new baz = foo.as(Baz) baz.set_bar("high") baz.actualize foo.bar # => "high"
Hmm, maybe +#actualize+ should be renamed +#converge+... what do you think?
== Multiple Roles and Nesting
You can adorn a data object with more than one role...
poster = User.new.as(Poster) commenter = poster.as(Commenter) # Has all the methods of a Commenter AND Poster
Alternatively...
User.new.as(Poster) do |poster| poster.as(Commenter) do |commenter| # Has all the methods of a Commenter AND Poster end end
== ActiveSupport::Concern
You can use <tt>ActiveSupport::Concern</tt> instead of <tt>Schizo::Role</tt>
module Baz extend ActiveSupport::Concern def something; end end
foo = Foo.new baz = foo.as(Baz) baz.something
== Documentation
{\http://doc.stochasticbytes.com/schizo/index.html}[http://doc.stochasticbytes.com/schizo/index.html]
== Contact
{@cjbottaro}[http://twitter.com/cjbottaro]
== Liscense
MIT or something
