/schizo

DCI (data, context and interaction) for Ruby / Rails / ActiveRecord

Primary LanguageRuby

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 Object#extend, 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://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://victorsavkin.com/post/13966712168/dci-in-ruby

http://andrzejonsoftware.blogspot.com/2011/02/dci-and-rails.html

The Problem

So what’s wrong with just using Object#extend? 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 Object#extend:

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 facade classes and facade objects 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

Schizo::Facades::User::Poster inherits from User, that’s why poster.kind_of?(User) works natrually. poster.instance_of?(User) 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 ActiveSupport::Concern instead of Schizo::Role

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

Contact

@cjbottaro

Liscense

MIT or something