Very often in software projects we end up repeating code that is functionally identical, but which differs in ways not easy to absorb via helper functions. Systems based on compiled languages like C or Java attack this problem by generating massive amounts of code and data structures off-line. The system is effectively partitioned in at least two systems (using different languages and techniques to problem-solving).
Scripting languages offer a revolutionary solution: the capability of writing methods which generate methods at run-time. What was done off-line becomes an integral part of the normal code; there is One System.
In Ruby this feature is implemented in the most natural way: to create a method dynamically, we call a method called ... define_method (could it be more logical?). There is also a less elegant (but still useful in some cases) solution: using class_eval to wrap the familiar def statement used for static method definition.
Here, we discuss the API and we conclude with an example from a real application.-
Ruby uses two ways to generate methods dynamically; define_method
(on the left) and the (older)
class_eval + def (right box). First, a quick snapshot of the API:
define_method(symbol, method) define_method(symbol) { block }class_eval <<-"END" def #{name} # body end END- Note:
-
define_method is a private class method (it must be
invoked without specifying the receiver, which requires it to be
be invoked from within the class context). The parameters are:
- symbol specifies the method name; in spite of its name, it can be either a symbol, a string, or a variable whose value contains the method name.
- method is a lambda or Proc object containing the method logic. In the second form shown, the logic is explicited in the code block.
- class_eval evaluates (in the context of a class) a string containing the method definition; the name of the method is interpolated from the environment.
-
define_method is a private class method (it must be
invoked without specifying the receiver, which requires it to be
be invoked from within the class context). The parameters are:
-
The first example is simple but useful to get us going: a method that generates
opening and
closing html tags. We implement 2 different versions of it, always with
define_method. It is worth to examine them for a few seconds to spot the
difference:
class C define_method(:tag) do |v| puts "<#{v}><#{v}>" end end C.new.tag('div') => <div></div>class C class << self define_method(:tag) do |v| puts "<#{v}><#{v}>" end end end C.tag('div') => <div></div>- The implementation in the left box creates an instance; therefore, we need to create an object in order to create a tag, what seems a bit cumbersome.
-
The code in the right box simplifies things by creating the method as a
class method.
[Note: the statement class << self opens the metaclass of the current class, allowing creation of class methods].
-
-
Now, a more interesting example, taken from an existing
application (note: the original was implemented in Perl, for British Telecom):
suppose we need to
process phone
billing records from a log, where each line lists the items for a
call (day, number
called, rate, etc) separated by blanks (for simplicity, we assume that
each item does not contain spaces). We want:
- a CallRecord class must return an object whose accessors represent each field of the call (eg, the application programmer will be able to write: obj.number to access the number called).
- the accessors must be created automatically from a specification which lists the order of the fields in the record; the key requirement was that nothing else has to be changed in the program if the record specification changes.
Here is the implementation:
class CallRecord # spec FIELDS= %w(day date time number rate) # generate accessors from spec FIELDS.each_index do |i| define_method(FIELDS[i]) do @call_rec[i] end end def initialize(record) @call_rec = record.lstrip.split(/\s+/) end end# The billing application # one record record = "SUN 07/15 12:26PM 439-4038 DFMR" call_rec= CallRecord.new(record) puts call_rec.day # => SUN puts call_rec.rate # => DFMR # to print all fields CallRecord::FIELDS.each do |name| puts call_rec.send(name) end
- Remark:
- the specification (with the order of fields) is stated by the constant FIELDS in the CallRecord class (this, for simplicity; in a real app, it could be a class variable configured via a method call, to have the class totally independent from the specification).
- the loop around define_method automates (see 3 lines in blue) the generation of accessors. Notice that the name of the method to be created is retrieved from the FIELDS array (ie, when we are writing the class, we do not know the name of the method that will be created).
- the order and names of the fields is centralized in one location in the program. New fields can be added/deleted or their order altered just editing one line (FIELDS).
-
As we can see from the last example, dynamic methods are an extraordinary tool
to achieve DRY design.
In the next page, we will discuss the creation of accessors, generalizing the
example that we just saw.
[URL: ; Last updated: ]