Dynamic Classes
Author |
Message |
Cervantes
|
Posted: Mon Mar 20, 2006 8:24 pm Post subject: Dynamic Classes |
|
|
Classes in Ruby are dynamic. I say this for two reasons.
Re-opening Classes
First, we can declare a class, do something else, then reopen the class later. The most common example of this is reopening one of the standard library classes to add a method.
Ruby: | class Array
def each_group(n)
(length / n).times do |i|
yield self[i * n .. (i + 1) * n - 1]
end
end
end
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].each_group(3) do |trio|
p trio
end |
Output wrote: [1, 2, 3]
[4, 5, 6]
[7, 8, 9]
The Creation of a Class
The second reason classes are dynamic is because the interpreter goes through the class much as it would your main program.
Ruby: | class Test
puts "Hello from the Test Class!"
end |
Output wrote: Hello from the Test Class!
The Test Class is defined and the code within it executed. Notice that we did not have to create a new Test object to get the "Hello..." message. That happened upon defining the object - no more. We could write an entire program within a class, and it would run fine.
Wrap Your Program in a Class
We can wrap a standard program -- one that contains methods and input and output, for example -- inside a Class, and it will run fine. Our "standard program" can even involve Classes of its own. This means we would have a class within a class.
Ruby: | class MyProgram
class Foo
attr_reader :bar
def initialize (bar)
@bar = bar
end
end
puts "Hello, from the MyProgram Class, where we have"
puts "just defined the Foo class. Prepare yourselves,"
puts "for we are just about to create a Foo object"
puts "within the MyProgram class!\n"
inside_foo = Foo.new(42)
puts "The value of 'bar' within the 'inside_foo' object is: #{inside_foo.bar}"
puts "\nSuccess! We have created a Foo object and accessed"
puts "it, all as we would normally do, but from within"
puts "the MyProgram Class."
end
puts "\nThat was some journey; but we've made it to the"
puts "outside world alive! Now that we're out here,"
puts "let's do a little testing.\n"
puts "First, let's try to create a Foo object from the outside"
outside_foo = MyProgram::Foo.new(4)
puts "The value of 'bar' within the 'outside_foo' object is: #{outside_foo.bar}"
puts "\nSuccess! Once again, we have created and"
puts "accessed a Foo object." |
The output to this program is all detailed by the program itself. It works as expected. 42 is output as the value of bar for the inside_foo object, and 4 is output as the value of bar for the outside_foo object.
Living within "Object"
In the first few minutes of learning Ruby, you don't see any object orientation.
But there's OOP just below the surface.
code: | irb(main):001:0> self
=> main
irb(main):002:0> self.class
=> Object |
We're actually living within the Object class. It's as if we've got:
Ruby: | class Object
puts "Hello World"
end |
This brings new meaning to the phrase, "Everything in Ruby is an Object".
Eval
We can get more dynamic, though. We can use the power of eval and its many forms to do some awesome things. eval takes a string or block of code and interprets it. We can use instance_eval, class_eval, and module_eval, to evaluate code at different levels.
Ruby: | class Person
def initialize(name_str)
@names = name_str.split
if @names.length == 2
instance_eval {
def first
@names.first
end
def last
@names.last
end
}
end
end
end
clapton = Person.new("Eric Clapton")
puts clapton.first
puts clapton.last
elvis = Person.new("Elvis")
puts elvis.first
puts elvis.last |
output wrote: Eric
Clapton
test2.rb:27: undefined method `first' for #<Person:0xb7d4f5ac @names=["Elvis"]> (NoMethodError)
For Eric Clapton, we gave the initialize method a string that contained two names, "Eric" and "Clapton". When we determined that this Person object has two names, we create methods first and last to get the first and last name of the Person. This is done dynamically by passing a block of code to instance_eval. However, Elvis Presley only gave his first name, so the first and last methods were never created.
The code you pass to eval can be dynamic, itself. Let's look at another example, this time using class_eval. Let's say I have a module that defines a bunch of methods that I need to use in a class. However, I can't use them quite as they are. I want to change them slightly, but all in the same way. What should we do?
For starters, let's make the assumption that I'm programming an IRC client in Ruby. The client needs to accept lines of input from the server, parse them, then display and log them, all nicely formatted. The format for the displayed and logged line will be the same, so we shouldn't have to rewrite the code to parse the string. Don't Repeat Yourself; DRY.
Let's create a module that will parse the input. It would define a lot of methods (one method for each IRC command), but we'll simplify this example to only show the PRIVMSG command.
Ruby: | module FromIRC
def privmsg(irc_input)
# Example irc_input: "Minsc PRIVMSG wtd Hey wtd!"
# The above string is a simplified version
# of what the IRC servers use. It means,
# "Minsc" sends the "PRIVMSG" command with two
# arguments. The first is the receiver, "wtd".
# The second is the text, "Hey wtd!"
words = irc_input.split
"#{words.first}: #{words[3 .. -1].join(" ")}"
end
end
class Channel
include FromIRC
def initialize
# Open the log file for appending
@log_file = File.open("log.txt", "a")
end
def log(text)
@log_file.puts text
end
private :log
# Create a slightly modified version of all
# the FromIRC methods.
# Create them as instance methods, though we
# use class_eval to do this, because we're
# working at the class level (notice we're not
# within any instance methods, like in the last
# example).
FromIRC.public_instance_methods.each do |method_str|
class_eval "def #{method_str}(irc_input); log super(irc_input); end"
end
end
Channel.new.privmsg("Minsc PRIVMSG wtd Hey wtd!") |
This will write "Minsc: Hey wtd!" in the log file.
We could then use the same trick in the Display class, where we need to modify the methods just slightly to add the name of the server to the beginning of the string, and we need to 'puts' that new line. Now we've used one module to format a line for two classes and created all the methods we need in those classes. We have avoided repeating ourselves. Without the class_eval trick, we would have had to manually create one method for each IRC command for each of our two classes. |
|
|
|
|
|
Sponsor Sponsor
|
|
|
Cervantes
|
Posted: Sun Apr 02, 2006 4:35 pm Post subject: (No subject) |
|
|
We've seen some pretty dynamic things happening, thus far. But there's more.
Renaming Methods
The names of methods can be changed using alias or alias_method. Let's look at a simple example of alias.
Ruby: |
class Foo
def bar
puts "Barrrrry"
end
alias qux bar
end
foo = Foo.new
foo.qux
foo.bar
|
Output wrote:
Barrrrry
Barrrrry
Notice the use of alias: alias new_name old_name. This suggests that alias is a keyword. alias_method, on the other hand, is a method.
Ruby: |
class Foo
def bar
puts "Barrrrry"
end
alias_method(:qux, :bar)
end
foo = Foo.new
foo.qux
foo.bar
|
Output wrote:
Barrrrry
Barrrrry
Aliasing is often used when a method needs to be slightly redefined, though it still needs to use the basis of the old method.
Ruby: |
class String
# Rename the 'inspect' method to 'old_inspect'
alias old_inspect inspect
# Overwrite the 'inspect' method with our definition
def inspect
"Value at ID #{self.object_id} is #{old_inspect}"
end
end
foo = "The Wind Cries Mary"
puts foo.inspect
|
Output wrote:
Value at ID -605548906 is "The Wind Cries Mary"
Aliasing is also used when a method in a child class needs to be redefined, but the original version of the method needs to stick around. Let's look at an abstract example.
Ruby: |
class Foo
def baz
puts "Barrry"
end
end
class Bar < Foo
alias foo_baz baz
def baz
super
puts "From the Bar class"
end
end
Bar.new.foo_baz
puts "----"
Bar.new.baz
|
Output wrote:
Barrry
----
Barrry
From the Bar class
Notice that neither alias nor alias_method remove the original method. To do that, we must use undef.
Removing Methods
We can dynamically create methods using class_eval and instance_eval, but what do we do with those methods if we don't want them later? If we dynamically created them, the condition which was true when they were created may no longer be true later. To remove a method, we can use the undef keyword. Let's look at a basic example.
Ruby: |
class Foo
def bar
puts "Barrrrry"
end
undef bar
end
Foo.new.bar
|
Output wrote:
test.rb:8: undefined method `bar' for #<Foo:0xb7d18e58> (NoMethodError)
Remember that the code inside a class is executed line-by-line. The bar method is defined, then it is undefined. Calling the bar method on a Foo object gives us a NoMethodError.
Let's go back to our Person class, where we dynamically created two instance methods, first and last, if the name we were given had two names. Let's create an attr_writer of sorts for @name. However, we must undefine the first and last methods for the Person object if the new name does not have two names. To do this, we need to undef singleton methods. We cannot simply do undef first, last because that would try to undefine regular instance methods -- methods that exist for all Person objects. first and last are singleton methods -- they are instance methods that only exist for some Person objects. To undefine singleton methods, we must use instance_eval to undefine the methods in the context of the current instance.
Ruby: |
class Person
def initialize(name_str)
@name = name_str.split
if @name.length == 2
instance_eval {
def first() @name.first end
def last() @name.last end
}
end
end
def name=(new_name)
@name = new_name.split
if @name.length != 2
instance_eval { undef first, last }
end
end
end
musician = Person.new("Eric Clapton")
puts musician.first
puts musician.last
musician.name = "John Lennon"
puts musician.first
puts musician.last
musician.name = "Sting"
puts musician.first
puts musician.last
|
output wrote:
Eric
Clapton
John
Lennon
test.rb:26: undefined method `first' for #<Person:0xb7cc3390 @name=["Simon"]> (NoMethodError)
As you can see, the first and last methods no longer exist for our musician object after he has been renamed to "Sting". And so they shouldn't.
The ability to rename and remove methods is quite poweful and incredibly dynamic. Ruby's classes cannot be thought of as static definitions of objects. They can change and evolve. Keep this thought in the back of your mind as you're programming (not necessarily in Ruby) and look for places where a dynamic class would help to solve a problem. |
|
|
|
|
|
Cervantes
|
Posted: Sat Apr 29, 2006 10:37 am Post subject: (No subject) |
|
|
Another way to remove methods
undef is one way of "removing" a method, but perhaps not the best. The Module class has a method called "remove_method" which actually removes the method. undef prevents the class from responding to its receiver. For example, assuming the Foo class already had a bar method, class Foo undef bar end prevents Foo objects from responding to calls to method bar. remove_method, on the other hand, does not act this way. It removes the method, which may or may not result the same NoMethodError being prodcued when that method is called.
But if we remove a method, then try to call it, how can that not produce a NoMethodError, you ask? Easy: Inheritance. If the parent class defines a method foo, then the child class overwrites foo, then using remove_method or unde will give different results. Using undef will produce or familiar NoMethodError. Using remove_method, however, will call the parent's version of foo. Here's an example:
Given the following base code:
Ruby: |
class Parent
def yar
puts "YARR!! ~Dad"
end
end
class Child < Parent
def yar
puts "Yar... ~Child"
end
end
|
If the following code (using undef) is placed after the base code, we get a NoMethodError:
Ruby: |
class Child
undef yar
end
Child.new.yar
|
output wrote: test.rb:16: undefined method `yar' for #<Child:0xb7d4bb74> (NoMethodError)
However, if the following (using remove_method) is placed after the base code, the Parent's version of yar is called:
Ruby: |
class Child
remove_method :yar
end
Child.new.yar
|
output wrote: YARR!! ~Dad
remove_method is a method, not a keyword (which raises the question, why am I bolding it?). Thus, it is called differently from undef, which can be called like this: undef foo, bar, baz. remove_method does not accept the method name directly. Rather, it accepts it as a Symbol or a String: remove_method :bar, :baz. |
|
|
|
|
|
|
|