Classes and objects |
Ruby is OO. You can define classes that have instance variables and methods.
A Ruby object has a set of instance variables and an associated class.
A Ruby class is an instance of the class Class. It has everything that an object has, plus a set of method definitions and
a reference to the superclass.
When you design a DSL using Ruby, it’s common to model the entities of your domain as classes.
|
class Account def initialize(no, name) @no = no @name = name end def to_s "Account no: #{@no} name: #{@name}" end end
initialize is a special method that’s called when you invoke Account.new. It’s used to set the state of your object after
the initial memory allocation.
@no and @name set up the instance variables of the class. You don’t have to declare variables before using them.
|
Singletons |
You can define a method specific to a particular object.
The set of method definitions within a Ruby class (as I’ve already mentioned) are nothing but singleton methods that are defined for the instance of the class Class. Singletons are also known as class methods in Ruby.
|
accnt = Account.new(12, "john p. ") def accnt.do_special ## end accnt.do_special ## runs acc = Account.new(23, "peter s. ") acc.do_special ## error!
do_special is a method that’s defined only for the instance accnt. It’s an example of a singleton method in Ruby, where self refers to the instance accnt
|
Metaprogramming |
Metaprogramming is the secret sauce behind Ruby DSLs. Ruby is a reflective language that lets you peek into the runtime meta-objects
and change their behaviors.
|
class Account attr_accessor :no, :name end
attr_accessor is a class method that uses reflective metaprogramming during runtime. It generates accessor methods for the attributes that are supplied as
parameters. Note how concise the surface syntax is; the boilerplates are generated during runtime.
class Trade < ActiveRecord::Base has_many :tax_fees end
This example is from the ActiveRecord library of Rails. The one-to-many relation between entities is expressed through a class method in Ruby and used with reflective metaprogramming.
|
Open classes |
Ruby lets you open any class and add or change attributes, methods, and properties during runtime.
This feature is popularly known as monkey patching, and is considered to be one of Ruby’s most dangerously powerful features.
Because monkey patching works in the global namespace, you need to use this feature judiciously.
|
class Integer def shares ## end end
You can design DSL-friendly features using Ruby’s open classes. For example, you can open the class Integer and add a method
named shares, so that your DSL user can write code like 2 shares. On the down side, all users who’ll be using the class Integer
are affected by this monkey patching. Be careful!
|
Evals |
In Ruby, you can evaluate a string or a block of code on the fly. This is one of the most powerful features of Ruby metaprogramming.
In designing DSLs, you can use the evals to set up the appropriate context. Then you can pass a block that invokes methods,
without explicitly specifying the context. This makes your DSL syntax less verbose.
You can set up a different context in which to evaluate the code by using one of the following flavors of evals that are available:
- class_eval—evaluate a string or a block of code in the context of a class or a module
- instance_eval—evaluate a string or a block of code in the context of a class instance
- eval—evaluate a string or a block of code in the current context
|
class Account end Account.class_eval do def open ## end end
The context is the class Account. class_eval creates an instance method for the class Account.
Account.instance_eval do def open ## end end
The context is the singleton class of self.instance_eval produces a singleton method (or class method) for Account.
|
Modules |
Modules offer a way to group related artifacts like methods, classes, and so on, so that they can be included in your class
as a mixin component.
Modules also offer a namespace facility in Ruby.
|
module Audit def record ## end end
The module defines a new namespace for grouping together all the audit-related methods. Now you can use it as a mixin and
include it in your class to make it auditable:
class Account include Audit ## can use the method record here end
|
Blocks |
Ruby blocks are chunks of code, somewhat similar to anonymous methods, that can be reified for later execution. Just like
methods, a Ruby block can take parameters.
A block is used synonymously with lambdas, which can implement higher-order functions in Ruby.
|
sum = 0 [1, 2, 3, 4].each do |value| sum += (value * value) end puts sum
|value| in horizontal bars is the parameter that gets passed to the block.
The method each which belongs to a Ruby array, accepts a block as a parameter
|
Hash as a variable-length argument list |
Ruby has a user-friendly way to implement a variable-length argument list to a method. You can simply pass a hash, then access
it as key/value pairs.
Doing this adds to the readability of the DSL code and makes implementing the Builder pattern a trivial task.
|
def foo(values) ## values is a hash end
Calling the function:
foo(:a => 1, :b => 2)
Example application of this idiom from Rails:
class Trade has_many :tax_fees, :class_name => "TaxFee", :conditions => "valid_flag = 1", :order => "name" end
|
Duck typing |
In Ruby, you design an abstraction NOT based on types, but based on the set of messages it responds to. If an object responds
to a quack message, then it’s a duck!
This kind of coding wouldn’t work in Java. In Java, you need to specify the type signature of the argument when it’s passed
in a method.
|
class Duck def quack ## end end class DummyDuck def quack ## end end def check_if_quack(duck) duck.quack end
The method check_if_quack will work with instances of Duck as well as DummyDuck, because both of them respond to the message
quack.
|