Chapter 7. Advanced Ruby

You learned the fundamentals of Ruby back in Chapter 1. This chapter covers some of the language’s advanced features, including modules, the Ruby object model, introspection, and a bit of metaprogramming.

Modules are used frequently in Rails applications to group similar functionality and share behavior between classes. The Ruby object model determines how methods are found and called in a hierarchy of inherited classes and shared code from modules. Introspection supports polymorphism by allowing you to look inside a class to see which methods it understands. Metaprogramming lets your classes respond to methods that don’t exist by defining methods at runtime.

Open a terminal window and launch IRB to get started. Several of the examples in this chapter are longer than normal. You may find it easier to type the example into your editor, save it as a file with the extension rb, and then run the example in your terminal by entering ruby filename.rb. Or you can simply copy and paste the code from your editor into IRB.

Modules

As you saw in Chapter 1, a module is a collection of methods and constants that cannot be instantiated. You define modules in basically the same way you define classes. Module definitions begin with the word module, followed by an uppercase name, and continue to the word end.

To demonstrate using modules, we first need a class definition. Let’s define a simple Person class:

   class Person
➊    attr_accessor :name

➋    def initialize(name)
       @name = name
     end
   end

This class uses attr_accessor ➊ to define getters and setters for the instance variable @name, and sets the value of @name when created ➋.

Class names are usually nouns because they represent objects. Module names are usually adjectives because they represent behavior. Many Ruby modules take this convention a step further and use adjective names ending with able, such as Comparable and Forwardable.

Here’s a silly example, just to show how it’s done:

module Distractable
  def distract
    puts "Ooh, kittens!"
  end
end

Enter this module in IRB, include it in the Person class you created earlier in this chapter, and see if you can distract someone:

irb(main):001:0> class Person
irb(main):002:1> include Distractable
irb(main):003:1> end
 => Person
irb(main):004:0> p = Person.new("Tony")
 => #<Person:0x007fceb1163de8 @name="Tony">
irb(main):005:0> p.distract
Ooh, kittens!
=> nil

In Chapter 5, you also defined a module method while working with Rails helpers. ApplicationHelper is a module that is automatically mixed into all controllers by Rails.

Modules serve two purposes in Ruby:

  • Modules are used to group related methods and prevent name conflicts.

  • Modules define methods that can be mixed in to classes to provide additional behavior.

Organizing your code becomes more important as your application grows. By providing namespaces and making it easy to share code between classes, modules help you break your code into manageable pieces. Let’s look at both of these purposes.

Modules as Namespaces

A Ruby module can be used as a namespace, a container for code such as constants or methods with related functionality.

The Math module is an example of a built-in Ruby module used as a namespace. It defines the constants E and PI as well as many common trigonometric and transcendental methods. The double-colon operator (::) is used to access constants in Ruby. The following example accesses the constant PI in the Math module:

irb(main):006:0> Math::PI
 => 3.141592653589793

Methods defined in a module are accessed with a dot (.), just like methods in a class:

irb(main):007:0> Math.sin(0)
 => 0.0

Modules as Mixins

A Ruby module can also be used as a mixin to provide additional functionality to a class. Ruby only supports single inheritance; that is, a class can only inherit from a single parent class. Modules allow you to implement something similar to multiple inheritance: a class can include several modules, adding each module’s methods to its own.

You can add a module’s methods to a class in three ways, using include, prepend, or extend. I discuss the effect of each of these keywords next.

include

The include statement adds the methods from a module to a class as instance methods and is the most common way of mixing a module into a class.

The Comparable module, included in Ruby, is commonly used as a mixin. It adds comparison operators and the between? method to classes when included. The class only needs to implement the <=> operator. This operator compares two objects and returns –1, 0, or 1, depending on whether the receiver is less than, equal to, or greater than the other object.

To use this module as a mixin, add it Person class you created previously:

   class Person
➊    include Comparable

➋    def <=>(other)
       name <=> other.name
     end
   end

This class now includes the Comparable module ➊ and defines the <=> operator ➋ to compare the name of this object with the name of another object.

After entering this in IRB, create a few people and see if they can be compared:

irb(main):008:0> p1 = Person.new("Tony")
 => #<Person:0x007f91b40140a8 @name="Tony">
irb(main):009:0> p2 = Person.new("Matt")
 => #<Person:0x007f91b285fea8 @name="Matt">
irb(main):010:0> p3 = Person.new("Wyatt")
 => #<Person:0x007f91b401fb88 @name="Wyatt">
irb(main):011:0> p1 > p2
 => true

Here p1 is greater then p2 because T is greater than M alphabetically. The between? method tells you whether an object falls between two others:

irb(main):012:0> p1.between? p2, p3
 => true

In this case, between? returns true since T is between M and W alphabetically, which means it works as expected.

prepend

The prepend statement also adds a module’s methods to a class, but prepend inserts the module’s methods before the class’s methods. This means if the module defines a method with the same name as the class, the module’s method will be executed instead of the class’s method. Using prepend, you can override a method in the class by writing a method in the module with the same name.

One practical use for prepend is memoization. Memoization is an optimization technique in which a program stores the result of a calculation to avoid repeating the same calculation multiple times.

For example, imagine you wanted to implement the Fibonacci sequence in Ruby. The first two numbers in the Fibonacci sequence are zero and one. Each subsequent number is the sum of the previous two. Here is a method to calculate the nth value of the Fibonacci sequence in Ruby:

  class Fibonacci
    def calc(n)
      return n if n < 2
➊     return calc(n - 1) + calc(n - 2)
    end
  end

Notice that the calc method is recursive. Every call to calc with a value of n greater than 1 results in two more calls to itself ➊. Try creating an instance of this class and calculating some small values of n:

irb(main):013:0> f = Fibonacci.new
 => #<Fibonacci:0x007fd8d3269518>
irb(main):014:0> f.calc 10
 => 55
irb(main):015:0> f.calc 30
 => 832040

As you call the method with larger values of n, the method takes noticeably longer to run. For values of n around 40, the method takes several seconds to return an answer.

The Fibonacci calc method is slow because it repeats the same calculations many times. But if you define a module to implement memoization, the calculations should take significantly less time. Let’s do that now:

   module Memoize
     def calc(n)
➊      @@memo ||= {}
➋      @@memo[n] ||= super
     end
   end

The Memoize module also defines a calc method. This method has a couple of interesting features. First, it initializes a class variable named @@memo ➊ with an empty hash if it is not already initialized. This hash stores the result of the calc method for each value of n. Next, it assigns the return value of super to @@memo at key n ➋ if that value is not already assigned. Because we are using prepend to add this module into Fibonacci, super calls the original calc method defined by the class.

Each time the calc method is called, @@memo stores the Fibonacci number for the value n. For example, after calling calc(3), the @@memo hash holds these keys and values:

{
  0 => 0,
  1 => 1,
  2 => 1,
  3 => 2
}

On each line, the key (the first number) is the value of n and the value (the second number) is the corresponding Fibonacci number. The Fibonacci number for 0 is 0, 1 is 1, 2 is 1, and 3 is 2. By storing these intermediate values, the calc method never needs to perform the same calculation more than once. Use prepend Memoize to add the Memoize module to the Fibonacci class and try it for yourself:

irb(main):016:0> class Fibonacci
irb(main):017:1> prepend Memoize
irb(main):018:1> end
 => Fibonacci
irb(main):019:0> f.calc 40
 => 102334155

Now that the values of calc are being memoized, you should be able to call calc for greater values of n and get an answer almost instantly. Try it with n = 100 or even n = 1000. Note that you didn’t have to restart IRB or instantiate a new Fibonacci object. Method lookup in Ruby is dynamic.

extend

When you use include or prepend to add a module to a class, the module’s methods are added to the class as instance methods. In Chapter 1, you learned that there are also class methods that are called on the class itself instead of on an instance of the class. The extend statement adds the methods from a module as class methods. Use extend to add behavior to the class itself instead of instances of the class.

The Ruby standard library includes a module named Forwardable, which you can use to extend a class. The Forwardable module contains methods useful for delegation. Delegation means relying on another object to handle a set of method calls. Delegation is a way to reuse code by assigning the responsibility of certain method calls to another class.

For example, imagine a class named Library that manages a collection of books. We store the books in an array named @books:

class Library
  def initialize(books)
    @books = books
  end
end

We can store our books, but we can’t do anything with them yet. We could use attr_accessor to make the @books array available outside of the class, but that would make all of the array’s methods available to users of our class. A user could then call methods such as clear or reject to remove all of the books from our library.

Instead, let’s delegate a few methods to the @books array to provide the functionality we need—a way to get the size of the library and add a book.

1 require 'forwardable'
  class Library
2   extend Forwardable
3   def_delegators :@books, :size, :push

    def initialize(books)
      @books = books
    end
  end

The Forwardable module is in the Ruby Standard Library, not the Ruby core, so we first need to require it ➊. Next, we use extend to add the Forwardable methods to our class as class methods ➋. Finally, we can call the def_delegators method ➌. The first argument to this method is a symbol representing the instance variable to which we’re delegating methods.

In this case, the instance variable is @books. The rest of the arguments are symbols representing the methods we want to delegate. The size method returns the number of elements in the array. The push method appends a new element to the end of an array.

In the following example, lib.size initially prints 2 because we have two books in our library. After adding a book, the size updates to 3.

irb(main):020:0> lib = Library.new ["Neuromancer", "Snow Crash"]
 => #<Library:0x007fe6c91854e0 @books=["Neuromancer", "Snow Crash"]>
irb(main):021:0> lib.size
 => 2
irb(main):022:0> lib.push "The Hobbit"
 => ["Neuromancer", "Snow Crash", "The Hobbit"]
irb(main):023:0> lib.size
 => 3

Ruby Object Model

The Ruby object model explains how Ruby locates a method when it is called. With inheritance and modules, you may find yourself wondering exactly where a particular method is defined or, in the case of multiple methods with the same name, which one is actually invoked by a particular call.

Ancestors

Continuing with the simple Person class defined previously, we can find out a lot about this class in IRB. First, let’s see which classes and modules define methods for the Person class:

irb(main):024:0> Person.ancestors
 => [Person, Distractable, Comparable, Object, Kernel, BasicObject]

The class method ancestors returns a list of classes that Person inherits from and the modules it includes. In this example, Person, Object, and BasicObject are classes, whereas Distractable, Comparable, and Kernel are modules. You can find out which of these are classes and which are modules by calling the class method as explained in the Class section below.

Object is the default root of all Ruby objects. Object inherits from BasicObject and mixes in the Kernel module. BasicObject is the parent class of all classes in Ruby. You can think of it as a blank class that all other classes build on. Kernel defines many of the Ruby methods that are called without a receiver, such as puts and exit. Every time you call puts, you’re actually calling the instance method puts in the Kernel module.

The order of this list indicates the order in which Ruby searches for a called method. Ruby first looks for a method definition in the class Person and then continues looking through the list until the method is found. If Ruby doesn’t find the method, it raises a NoMethodError exception.

Methods

You can see a list of the class methods and instance methods defined by a class by calling methods and instance_methods, respectively. These lists include methods defined by all parent classes by default. Pass the parameter false to leave out only these:

irb(main):025:0> Person.methods
 => [:allocate, :new, :superclass, :freeze, :===, :==, ... ]
irb(main):026:0> Person.methods(false)
 => []
irb(main):027:0> Person.instance_methods(false)
 => [:name, :name=, :<=>]

The Person class contains almost 100 different class methods from its ancestors, but it defines none of its own, so the call to methods(false) returns an empty array. The call to instance_methods returns the name and name= methods defined by attr_accessor and the <=> method that we defined in the body of the class.

Class

The last piece of the object model concerns the Person class itself. Everything in Ruby is an object, that is, an instance of a class. Therefore, the Person class must be an instance of some class.

irb(main):028:0> Person.class
 => Class

All Ruby classes are instances of the class Class. Defining a class, such as Person, creates an instance of the class Class and assigns it to a global constant, in this case Person. The most important method in Class is new, which is responsible for allocating memory for a new object and calling the initialize method.

Class has its own set of ancestors:

irb(main):029:0> Class.ancestors
 => [Class, Module, Object, Kernel, BasicObject]

Class inherits from the class Module, which inherits from Object as before. The Module class contains definitions of several of the methods used in this section such as ancestors and instance_methods.

Introspection

Introspection, also known as reflection, is the ability to examine an object’s type and other properties as a program is running. You’ve already seen how to determine an object’s type by calling class and how to get a list of methods defined by an object by calling methods and instance_methods, but Ruby’s Object class defines several more methods just for introspecting objects. For example, given an object, you may want to determine if it belongs to a particular class:

irb(main):030:0> p = Person.new("Tony")
 => #<Person:0x007fc0ca1a6278 @name="Tony">
irb(main):031:0> p.is_a? Person
 => true

The is_a? method returns true if the given class is the class of the receiving object. In this case, it returns true because the object p is a Person.

irb(main):032:0> p.is_a? Object
 => true

It also returns true if the given class or module is an ancestor of the receiving object. In this case, Object is an ancestor of Person, so is_a? returns true.

Use the instance_of? method if you need to determine exactly which class was used to create an object:

irb(main):033:0> p.instance_of? Person
 => true
irb(main):034:0> p.instance_of? Object
 => false

The instance_of? method returns true only if the receiving object is an instance of the given class. This method returns false for ancestors and classes inheriting from the given class. This type of introspection is helpful in some situations, but generally you don’t need to know the exact class used to create an object—just the object’s capabilities.

Duck Typing

In duck typing, you only need to know whether an object accepts the methods you need to call. If the object responds to the needed methods, you don’t have to worry about class names or inheritance. The name duck typing comes from the phrase, “If it walks like a duck and quacks like a duck, call it a duck.”

In Ruby, you can use the respond_to? method to see if an object responds to a particular method. If respond_to? returns false, then calling the method raises a NoMethodError exception as explained earlier.

For example, imagine a simple method to print some information to a file with a timestamp:

def write_with_time(file, info)
  file.puts "#{Time.now} - #{info}"
end

You can try this method in IRB.

➊ irb(main):001:0> f = File.open("temp.txt", "w")
   => #<File:temp.txt>
➋ irb(main):002:0> write_with_time(f, "Hello, World!")
   => nil
➌ irb(main):003:0> f.close
   => nil

First, open a File named temp.txt in the current directory and store the File instance in the variable f ➊. Then pass f and the message "Hello, World!" to the write_with_time method ➋. Finally, close the File with f.close ➌.

The file temp.txt in the current directory now contains a single line similar to the one here:

2014-05-21 16:52:07 -0500 - Hello, World!

This method works great until someone accidentally passes a value to it that isn’t a file, such as nil. Here’s a possible fix for that bug:

  def write_with_time(file, info)
➊  if file.instance_of? File
      file.puts "#{Time.now} - #{info}"
    else
      raise ArgumentError
    end
  end

This fix solves the problem by checking to see if file is an instance of the File class ➊, but it also limits the usefulness of this method. Now it only works with files. What if you want to write over the network using a Socket or write to the console using STDOUT?

Instead of testing the type of file, let’s test its capabilities:

  def write_with_time(file, info)
➊   if file.respond_to?(:puts)
      file.puts "#{Time.now} - #{info}"
    else
      raise ArgumentError
    end
  end

You know that the write_with_time method calls the method puts, so check to see if file responds to the puts method ➊. Now, write_with_time works with any data type that responds to the puts method.

Using duck typing leads to code that can be easily reused. Look for more opportunities to apply duck typing as you build applications.

Metaprogramming

Metaprogramming is the practice of writing code that works with code instead of data. With Ruby, you can write code that defines new behavior at runtime. The techniques in this section can save you time and remove duplication from your code by allowing Ruby to generate methods when your program is loaded or as it runs.

This section covers two different ways of dynamically defining methods: define_method and class_eval. It also covers method_missing, so you can respond to methods that haven’t been defined.

define_method

Let’s say we have an application with a list of features that can be enabled for users. The User class stores these features in a hash named @features. If a user has access to a feature, the corresponding hash value will be true.

We want to add methods of the form can_ feature! and can_ feature? to enable a feature and check if a feature is enabled, respectively. Rather than write several mostly identical methods, we can iterate over the list of available features and use define_method, as shown here, to define the individual methods:

  class User
➊   FEATURES = ['create', 'update', 'delete']

    FEATURES.each do |f|
➋     define_method "can_#{f}!" do
        @features[f] = true
      end

➌     define_method "can_#{f}?" do
➍        !!@features[f]
       end
     end
     def initialize
       @features = {}
     end
   end

The User class first creates a constant array ➊ of available features named FEATURES. It then iterates over FEATURES using each and calls define_method to create a method of the form can_ feature! ➋ to allow a user access to a feature. Still inside the each block, the class also defines a method of the form can_ feature? ➌ that determines whether a user has access to the feature. This method converts the value @features[f] to either true or false by using two NOT operators ➍.

Note

Using two NOT operators isn’t strictly necessary because the @features hash returns nil for keys without values and Ruby treats nil as false, but this technique is commonly used.

Now let’s create a new User and try the dynamically defined methods:

irb(main):001:0> user = User.new
 => #<User:0x007fc01b95abe0 @features={}>
irb(main):002:0> user.can_create!
 => true
irb(main):003:0> user.can_create?
 => true
irb(main):004:0> user.can_update?
 => false
irb(main):005:0> user.can_delete?
 => false

If you want more practice with define_method, see if you can add methods of the form cannot_feature!, which disables a feature for the user. More details are provided in Exercise 3 at the end of this chapter.

class_eval

The class_eval method evaluates a string of code as if it were typed directly into the class definition. Using class_eval is an easy way to add instance methods to a class at runtime.

When I discussed attr_accessor in Chapter 1, you learned that it defines getter and setter methods for instance variables in a class, but I didn’t discuss exactly how those methods were defined. The attr_accessor method is built in to Ruby. You don’t need to define it yourself, but you can learn about class_eval by implementing your own version of attr_accessor.

➊ class Accessor
➋   def self.accessor(attr)
      class_eval "
➌       def #{attr}
          @#{attr}
          end

➍         def #{attr}=(val)
            @#{attr} = val
          end
         "
       end
     end

Here, you define a class named Accessor ➊ with a single class method named accessor ➋. This method works like the built-in attr_accessor. It accepts a single parameter representing the attribute for which you’re creating getter and setter methods. Pass the string to class_eval, which uses string interpolation to insert the value of attr as needed to define two methods. The first method has the same name as the attribute and returns the value of the attribute ➌. The second method is the attribute name followed by an equal sign. It sets the attribute to a specified value val ➍.

For example, if attr is :name, then accessor defines the methods name and name= by replacing attr with name in the specified places. This is a little hard to follow without an example. The following code uses the accessor method in a class:

➊ class Element < Accessor
➋   accessor :name

    def initialize(name)
      @name = name
    end
  end

First, you have the Element class inherit from the Accessor class ➊ so the accessor method is available. Then, you pass the name of the instance variable to accessor ➋. Here, you pass the symbol :name. When the program runs, the call to class_eval automatically generates this code inside the Element class:

➊ def name
    @name
  end

➋ def name=(val)
    @name = val
  end

The name method returns the current value of the instance variable @name ➊. The name= method accepts a value and assigns it to @name ➋. Test this by creating an instance of the Element class and trying to get and set the value of name:

➊ irb(main):001:0> e = Element.new "lead"
   => #<Element:0x007fc01b840110 @name="lead">
➋ irb(main):002:0> e.name = "gold"
   => "gold"
➌ irb(main):003:0> puts e.name
  gold
   => nil

First, create a new Element and initialize its name with "lead" ➊. Next, use the name= method to assign the new name "gold" ➋. Finally, use the name method to display the value of @name ➌. There you have it. With a bit of metaprogramming magic, you turned lead into gold.

method_missing

Whenever Ruby can’t find a method, it calls method_missing on the receiver. This method receives the original method name as a symbol, an array of arguments, and any block passed to the method call.

By default, method_missing calls super, which passes the method up the ancestor chain until it finds an ancestor class containing the method. If the method reaches the BasicObject class, it raises a NoMethodError exception. You can override method_missing by defining your own implementation in a class to intercept these method calls and add your own behavior.

Let’s start with a simple example so you can see how it works. This class echoes any unknown method calls back to you three times:

class Echo
  def method_missing(name, *args, &block)
    word = name
    puts "#{word}, #{word}, #{word}"
  end
end

Now that method_missing is overridden, if you try to call a nonexistent method on an instance of this class, you’ll just see that method’s “echo” in the terminal:

irb(main):001:0> echo = Echo.new
 => #<Echo:0x007fa8131c9590>
irb(main):002:0> echo.hello
 => hello, hello, hello

A real-world use for method_missing is the Rails dynamic finder. Using dynamic finders, you can write Active Record queries like Post.find_by_title("First Post") instead of Post.where(title: "First Post").first.

Dynamic finders can be implemented using method_missing. Let’s define our own version of dynamic finders. Instead of method names like find_by_attribute, we’ll use query_by_attribute so we can avoid conflicts with the built-in methods.

Open the Post model at app/models/post.rb in your blog directory to follow along with this example:

  class Post < ActiveRecord::Base
    validates :title, :presence => true
    has_many :comments

➊   def self.method_missing(name, *args, &block)if name =~ /Aquery_by_(.+)z/where($1 => args[0]).first
      elsesuper
      end
    end
 end

First, define the method_missing class method ➊ because our query_by_attribute method will be called on the Post class. Next, test the name against a regular expression ➋.

Finally, call the built-in where method ➌ using the string captured by the regular expression and the first argument passed to the method. Be sure to call super ➍ if the string doesn’t match; this ensures that unknown methods will be sent to the parent class.

Note

The regular expression /Aquery_by_(.+)z/ matches strings that start with “query_by_” and then captures the rest of the string using parenthesis. A full discussion of regular expressions is beyond the scope of this book. The website http://rubular.com/ is a great way to edit and test regular expressions online.

The real dynamic finders also check to make sure the captured string matches an attribute of the model. If you try to call our query_by_attribute method with nonexistent column, it raises a SQLException.

irb(main):001:0> Post.query_by_title "First Post"
 => #<Post id: 1, ...>

Our implementation of query_by_attribute has one more problem:

irb(main):002:0> Post.respond_to? :query_by_title
 => false

Because we’re overriding method_missing to call this method, Ruby doesn’t know that the Post class can respond to it. To fix this, we need to also override the respond_to_missing? method in the Post model at app/models/post.rb.

   class Post < ActiveRecord::Base
     --snip--

     def self.respond_to_missing?(name, include_all=false)name.to_s.start_with?("query_by_") || super
    end
  end

Instead of the regular expression used in method_missing, we just check if the method name starts with "query_by_" ➊. If it does, this method returns true. Otherwise, super is called. Now restart the Rails console and try again:

irb(main):001:0> Post.respond_to? :query_by_title
 => true

With this change in place, respond_to? returns true as expected. Remember to always override respond_to_missing? when using method_missing. Otherwise, users of your class have no way of knowing which methods it accepts, and the duck typing techniques covered earlier will fail.

Summary

If you write enough Ruby, then you will eventually see all of the techniques covered in this chapter used in real-world programs. When that time comes, you can be confident that you’ll understand what the code does, instead of just assuming that metaprogramming is some kind of magic.

In the next chapter, you’ll start building a new Rails application from scratch. Along the way I’ll cover some advanced data-modeling techniques and you’ll learn even more about Active Record.

For now, try these exercises.

Exercises

Q:

1. The Rails framework makes extensive use of modules both as namespaces and to add behavior to classes. Open a Rails console inside your blog directory and look at the ancestors of Post. How many ancestors does it have? Based on their names, can you tell what some of them do?

Q:

2. Update the define_method sample by adding a cannot_ feature! method. This method should set the value corresponding to the correct key in the @features hash to false.

Q:

3. Verify that class_eval created the instance methods you expected inside the Element class by calling Element.instance_methods(false). Then reopen the Element class and call accessor :symbol to add two more methods for an instance variable named @symbol.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.61.170