Cascading Classes

This is a small library that helps simplify and manage deeply hierarchal data. It can be used for brainstorming. It can be used to model a tree of data of any depth and breadth. You can use it for brainstorming or to carry active content.

Warning: This library commits the blasphemous crime of utilizing classes as objects. Stuff like this:

Parent.lname = 'brownstein'
Parent.fname = "charlie"
Parent.name = Proc.new{|c| c.fname + ' ' + c.lname}

Child.fname = "sam"
p Child.name     #  =>  "sam brownstein"

Whereas normally you might write something like this:

class Parent
  attr_accessor :fname, :lname

  def name
    fname + ' ' + lname
  end
end

obj = Parent.new
obj.fname = 'sam'
obj.lname = 'brownstein'
p obj.name     #  =>  'sam brownstien'

You have been warned!

Take it for a quick spin

Assume the following simple class hierarchy:

class A
  extend CC
end

class B < A
end

class C < B
end

Notice that CC is an alias for CascadingClasses

class A
  extend CC
end

is equivalent to

class A
  extend CascadingClasses
end

Note also the difference between the extend and the include versions. Class instances will not "inherit" cascaded properties with extend but will with include.

class Person
  extend CC

  cascade do
    language default: "english"
  end
end

Brit = Class.new(Person)     #  same as: class Brit < Person; end
john = Person.new

p Brit.language       #  =>  "english"
p john.language     #  =>  NoMethodError: undefined method 'language' for #<Person:0x0000...>

compare that to the following:

class Person
  include CC

  cascade do
    language default: "english"
  end
end

Brit = Class.new(Person)
john = Person.new

p Brit.language       #  =>  "english"
p john.language     #  =>  "english"

The john instance doesn't have the language property in one and does in another.

There are two versions to emphasize the point that calling cascade is about endowing your descendents with traits. And instances aren't inheritable. They don't have children. Consider a class hierarchy a hundred levels deep. Calling cascade on any one class will effect it and all its descendents. If you want instances of all those classes to be effected as well use include. Otherwise use extend.

Continuing the tour

Let's create a new example

Class A
  extend CC
end

Class B < A; end

Class C < B; end

A is the top-level parent. B is its child. C is the child of B.

A  <  B  <  C

create name and city properties:

A.cascade do
  name default: "Tom"
  city default: "New York"
end

Here we provide A and all its descendents with the name and city properties.

p A.name     #  =>  "Tom"
p B.name     #  =>  "Tom"
p C.name     #  =>  "Tom"

Descendents inherit their values unless set themeselves.

B.name = "John"
p B.name     #  =>  "John"
p C.name     #  =>  "John"

It's crucuial to see that C changes too

At any point in time we can add nodes anywhere on the tree:

class B2 < A
end

class C2 < B2
end

class D < C
end

Our simple example now looks like the following:

                                           _______  D
                                         /
                                        /
                         ________   C   --------  ..
                       /    
                      /
        ________  B   --------  ..
      /               \ 
     /                 \ ________  ..
    /        
A   --------  ..         ________  ..
    \                  / 
     \                /                   ________  ..
      \ ________  B2  --------  ..       /
                      \                 /
                       \ ________   C2  -------- ..

Now we have six nodes that span four generations (depth=4). Things have gotten considerably more complex. That's the point. It's easy for the complexity to get out of hand, but you need a way to apply a set of properties on a tree in a predictible way. We can continue adding (subclassing) nodes and watch each descendent come to life endowed with a sensible values for name and city.

We can also, at any point, introduce new properties onto the tree, or any subtree of the tree for that matter. And you can expect each descendent to inherit from its parent in real time. For example, let's add the state property to all descendents of B.

B.cascade do
  state default: "MA"
end

p B.state     #  =>  "MA"
p C.state     #  =>  "MA"
p D.state     #  =>  "MA"

Another simple example

Let's try another example. This time we'll let properties cascade to instances (requires use of include keyword). We'll begin with three properties: color, width, and height.

class Parent
  include CC

  cascade do
    color default: "red"
    width default: 100
    height default: 50
  end
end

class Div1 < Parent; end
class Div2 < Parent; end

div1 = Div1.new
div2 = Div2.new

Every instance and class in this hierarchy has a color, height, and width.

p Div1.color     #  =>  "red"
p div1.color     #  =>  "red"

Set the color property

Div1.color = "blue"
div1.color = "black"

We can collect cascaded properties values by calling to_hash

p Div1.to_hash     #  =>  {:color=>"blue", :width=>100, :height=>50}

To obtain the hash that includes only properties that have been set (not inherited) pass false

p Div1.to_hash            #  =>  {:color=>"blue", :width=>100, :height=>50}
p Div1.to_hash(false)     #  =>  {:color=>"blue"}

only the color has been set on Div1. The others inherit from Parent

Div1.height = 75
p Div1.to_hash(false)     #  =>  {:color=>"blue", :height=>75}

what is default

Consider this simple example:

Class A
  extend CC

  cascade do
    color default: "red"
  end
end

B = Class.new(A)
C = Class.new(B)

The default value is always available to any descendent, even if the property has since been set.

C.color = "black"
p C.color(:default)     #  =>  "red"

Arrays and Hashes follow different inheritance rules

Container types actually behave differently. Descendents don't inherit from their ancestors. They start out with empty containers.

Try it with an array:

A.cascade do
  list default: []
end

A.list << 4 << 1
B.list << 5 << 1

p A.list     #  =>  [4, 1]
p B.list     #  =>  [5, 3]

And a hash:

A.cascade do
  dict default: {}
end

A.dict[:width] = 400
B.dict[:height] = 20

p A.dict     #  =>  {:width=>400}
p B.dict     #  =>  {:height=>20}

Proc properties

A property can also be a Proc. The object passed to it is a copy of the current class. This allows you to create properties that are derived, dynamic

A.cascade do
  score default: 89
  passed default: Proc.new{|me| me.score > 75}
end

Here we've created two new properties. The passed property gets its value from the score property. Note that calling passed evaluates the Proc in the context of the receiver. It doesn't return the Proc itself.

A.passed     #  =>  true

Now we can change the value on score on any descendent and passed will change too

B.score = 72
p B.passed     #  => false

Blocks

You can pass blocks to any property. The parameters passed to the block are the property value and an array of ancestors. Here is an example:

A.cascade do
  color default: "red"
end

B = Class.new(A)
C = Class.new(B)

B.color = "blue"
C.color = "white"

p C.color{|color| color}                #  =>  "red"
p C.color{|color, parents| parents}     #  => [B, A]

For example you could print out a list of C and its ancestors' colors

C.color{|c, parents| [c] + 
  parents.map{|x| x.color}
}                                 #  =>  ["white", "blue", "red"]

Or you can set the color on every ancestor:

C.color{|c, parents| parents.each{|p| p.color = c}}

p A.color     #  =>  "white"
p B.color     #  =>  "white"
p C.color     #  =>  "white"

Ancestor chain

To obtain an array of parents call ancestor_chain

A.ancestor_chain     #  => []
B.ancestor_chain     #  => [A]
C.ancestor_chain     #  => [B, A]

}