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]
}