Let's build a simple students database. Each University has a name and address. Each student has a name, address and an associated university.
We are using a Struct
to build our classes in an easy way. This provides
all getters, setters and an easy constructor setting all the instance
variables.
Note: In order to get a nice output in our program we override the #to_s method which is used in many cases by ruby, e.g. in Kernel#puts or in String interpolation.
In most of the cases, the name is sufficient to represent each entity, i.e. a student or a university.
class University < Struct.new(:name, :address)
def to_s
name
end
end
class Student < Struct.new(:name, :address, :university)
def to_s
name
end
end
Under certain circumstances we would like to have a more verbose output. This could mean print the university a student belongs to or attach the address to the output.
In a plain old Ruby project, this would result in additional methods, probably encapsulated in modules, that will be included into our classes. This allows reuse and better encapsulation.
module AddressOutput
def to_s_with_address
"#{self} (#{self.address})"
end
end
class University
include AddressOutput
end
Now each university got a to_s_with_address method that could be called instead of to_s if you would like to have additional information.
class Student
include AddressOutput
def to_s_with_university
"#{self}; #{self.unversity}"
end
def to_s_with_university_and_address
"#{self.to_s_with_address}; #{self.unversity.to_s_with_address}"
end
end
The same for each student. #to_s_with_unversity and #to_s_with_university_and_address give as well additional output.
So how can you use it. Let's create some instances first.
$hpi = University.new("HPI", "Potsdam")
$gregor = Student.new("Gregor", "Berlin", $hpi)
An now some output.
Note: This could live inside an erb template, a graphical user interface or printed to the command line. In all these cases to_s is called automatically by the standard library to receive a good representation of the object. The output method defined in test_helper.rb simulates this behaviour. All examples are converted to test class automatically, so we can be sure, that this document stays in sync with the library.
puts $gregor # => prints "Gregor" "#{$gregor}" # => evaluates to "Gregor" <%= $gregor %> => as well as this {:execute=false}
output_of($gregor) == "Gregor"
output_of($hpi) == "HPI"
{:execute=false}
Assume, we would like to print an address list now.
example do
output_of($gregor.to_s_with_address) == "Gregor (Berlin)"
end
If you want a list with university and addresses, you would use #to_s_with_university_and_address. No automatic call to to_s anymore. If you have your layout in an erb template, you have to change each and every occurrence of your variables.
To solve this problem you could redefine to_s on demand. I will demonstrate this with some meta programming in a fresh class.
module GenericToS
def to_s
self.class.included_vars.collect do |var|
self.send(var)
end.join("; ")
end
module ClassMethods
attr_accessor :included_vars
def set_to_s(*included_vars)
self.included_vars = included_vars
end
end
def self.included(base_class)
base_class.send(:extend, ClassMethods)
end
end
class Company < Struct.new(:name, :address)
include GenericToS
end
class Employee < Struct.new(:name, :address, :company)
include GenericToS
end
I will not go into detail how this code works, but I will show you how to use it. Let's get some instances first.
$ms = Company.new("Microsoft", "Redmond")
$bill = Employee.new("Bill", "Redmond", $ms)
And now use these instances.
example do
Company.set_to_s(:name)
Employee.set_to_s(:name)
output_of($ms) == "Microsoft"
output_of($bill) == "Bill"
end
Let's get the output including the addresses
example do
Employee.set_to_s(:name, :address)
output_of($bill) == "Bill; Redmond"
end
And including the employer
example do
Employee.set_to_s(:name, :address, :company)
output_of($bill) == "Bill; Redmond; Microsoft"
end
But hey. I wanted to have a list with all addresses, not just the employee's. This should be an address list, right? But we did not tell the Company class to print the address, but just the Employee class.
So in our first approach, we had to change each place, where we use the object. In the second approach we have to know all places where an address is stored and apply the changes in there.
By the way, what happens, if I was using a multi-threaded application and one user request a simple name list, and the other switches to an address list in the meantime. Then the output will be mixed - with and without addresses. This is not exactly what we want. So there has to be an easier, thread safe solution.
This is were context-oriented programming comes into play. I will again start from the scratch. It is not much and we all know the problem space now.
The same setup, just another setting. First the basic implementation, just like we did it in our first approach.
class Religion < Struct.new(:name, :origin)
def to_s
name
end
end
class Believer < Struct.new(:name, :origin, :religion)
def to_s
name
end
end
Now define the additional behaviour in separate modules. Please don't be scared because of the strange syntax and method calls. yield(:receiver) refers to the "normal" self when these modules are included.
Future versions of ContextR will hopefully provide a nicer syntax here.
Finally we need to link our additional behaviour to our basic classes. We also need to tell the framework, when this behaviour should be applied.
module OriginMethods
def to_s
"#{super} (#{yield(:receiver).origin})"
end
end
class Religion
in_layer :location do
include OriginMethods
end
end
class Believer
in_layer :location do
include OriginMethods
end
in_layer :believe do
def to_s
"#{super}; #{yield(:receiver).religion}"
end
end
end
The additional context dependent behaviour is organised within layers. A single layer may span multiple classes - in this case the location layer does. To enable the additional code, the programmes shall activate layers. A layer activation is only effective within a block scope and within the current thread.
Let's see, how it looks like when we use it.
$christianity = Religion.new("Christianity", "Israel")
$the_pope = Believer.new("Benedikt XVI", "Bavaria", $christianity)
example do
output_of($christianity) == "Christianity"
output_of($the_pope) == "Benedikt XVI"
end
Would like to have an address? For this we have to activate the location layer. Now the additional behaviour defined within the layer, will be executed around the base method defined within the class.
example do
ContextR.with_layer :location do
output_of($christianity) == "Christianity (Israel)"
output_of($the_pope) == "Benedikt XVI (Bavaria)"
end
end
Of course the additional behaviour is deactivated automatically after the blocks execution.
example do
output_of($christianity) == "Christianity"
output_of($the_pope) == "Benedikt XVI"
end
Everything back to normal.
Lets activate the believe layer:
example do
ContextR.with_layer :believe do
output_of($the_pope) == "Benedikt XVI; Christianity"
end
end
Now we need both, location and believe. How does it look like? You have to options. You may activate the two one after the other or all at once. It is just a matter of taste, the result remains the same.
example do
ContextR.with_layer :location, :believe do
output_of($the_pope) == "Benedikt XVI (Bavaria); Christianity (Israel)"
end
end
As you can see, the activation of the location layer is operative in the whole execution context of the block. Each religion prints its origin, whether to_s was called directly or indirectly.
If you change your mind within your call stack, you may of course deactivate layers again.
example do
ContextR::with_layer :location do
ContextR.with_layer :believe do
output_of($the_pope) ==
"Benedikt XVI (Bavaria); Christianity (Israel)"
ContextR.without_layer :believe do
output_of($the_pope) == "Benedikt XVI (Bavaria)"
end
output_of($the_pope) ==
"Benedikt XVI (Bavaria); Christianity (Israel)"
end
end
end
example do
assert_equal(["to_s"], Religion.in_layer(:location).instance_methods)
end
These encapsulations may be as complex as your application. ContextR will keep track of all activations and deactivations within the blocks and restore the settings after the block was executed.
This was just a short introduction on a problem case, that can be solved with context-oriented programming. You have seen, the advantages and how to use it. In other files in this folder, you can learn more on the dynamics and meta programming interfaces of ContextR.