Everyday OOP Tasks

 
   

Ruby Way
By Hal Fulton
Slots : 1.0
Table of Contents
 


Of his quick objects hath the mind no part,

Nor his own vision holds what it doth catch….

William Shakespeare, Sonnet 113

If you don't already understand OOP, you won't learn it here. And if you don't already understand OOP in Ruby, you probably won't learn it here, either. If you're rusty on those concepts, you can scan Chapter 1, "Ruby in Review," where we cover it rapidly (or you can look at another book).

On the other hand, much of this material is tutorial oriented and fairly elementary. So it will be of some value to the beginner and perhaps less value to the intermediate Ruby programmer. We maintain that a book is a random-access storage device so that you can easily skip the parts that don't interest you.

Using Multiple Constructors

There is no real constructor in Ruby as there is in C++ or Java. The concept is certainly there because objects have to be instantiated and initialized; but the behavior is somewhat different.

In Ruby, a class has a class method new, which is used to instantiate new objects. The new method calls the user-defined special method initialize, which then initializes the attributes of the object appropriately, and new returns a reference to the new object.

But what if we want to have multiple constructors for an object? How should we handle that?

Nothing prevents us from creating additional class methods that return new objects. Listing 5.1 shows a contrived example in which a rectangle can have two side lengths and three color values. We create additional class methods that assume certain defaults for some of the parameters. (For example, a square is a rectangle with all sides the same length.)

Listing 5.1 Multiple Constructors
 class ColoredRectangle   def initialize(r, g, b, s1, s2)     @r, @g, @b, @s1, @s2 = r, g, b, s1, s2   end   def ColoredRectangle.whiteRect(s1, s2)     new(0xff, 0xff, 0xff, s1, s2)   end    def ColoredRectangle.grayRect(s1, s2)      new(0x88, 0x88, 0x88, s1, s2)    end    def ColoredRectangle.coloredSquare(r, g, b, s)      new(r, g, b, s, s)    end    def ColoredRectangle.redSquare(s)      new(0xff, 0, 0, s, s)    end    def inspect      "#@r #@g #@b #@s1 #@s2"    end  end  a = ColoredRectangle.new(0x88, 0xaa, 0xff, 20, 30)  b = ColoredRectangle.whiteRect(15,25)  c = ColoredRectangle.redSquare(40) 

So we can define any number of methods we want that create objects according to various specifications. Whether the term constructor is appropriate here is a question that we will leave to the language lawyers.

Creating Instance Attributes

An instance attribute in Ruby is always prefixed by an @ sign. It is like an ordinary variable in that it springs into existence when it is first assigned.

In OO languages, we frequently create methods that access attributes to avoid issues of data hiding. We want to have control over how the internals of an object are accessed from the outside. Typically we use setter and getter methods for this purpose (although in Ruby we don't typically use these terms). These are simply methods used to assign (set) a value or retrieve (get) a value, respectively.

Of course, it is possible to create these functions by hand, as shown here.

 

 class Person   def name     @name   end   def name=(x)     @name = x   end   def age     @age   end   # ... end 

However, Ruby gives us a shorthand for creating these methods. The attr method takes a symbol as a parameter and creates the associated attribute. It also creates a getter of the same name. If the optional second parameter is true, it will create a setter as well.

 

 class Person   attr :name, true  # Create @name, name, name=   attr :age         # Create @age, age end 

The related methods attr_reader, attr_writer, and attr_accessor take any number of symbols as parameters. The first will only create read methods (to get the value of an attribute); the second will create only write methods (to set values); and the third will create both. For example,

 

 class SomeClass   attr_reader :a1, :a2  # Creates @a1, a1, @a2, a2   attr_writer :b1, :b2  # Creates @b1, a1=, @b2, b2=   attr_reader :c1, :c2  # Creates @c1, c1, c1=, @c2, c2, c2=   # ... end 

Recall that an assignment to a writer of this form can only be done with a receiver. So within a method, the receiver self must be used.

More Elaborate Constructors

As objects grow more complex, they accumulate more attributes that must be initialized when an object is created. The corresponding constructor can be long and cumbersome, forcing us to count parameters and wrap the line past the margin.

One good way to deal with this complexity is to pass in a block to the initialize method (see Listing 5.2). We can then evaluate the block in order to initialize the object. The trick is to use instance_eval instead of eval in order to evaluate the block in the context of the object rather than that of the caller.

Listing 5.2 A Fancy Constructor
 class PersonalComputer   attr_accessor :manufacturer,     :model, :processor, :clock,     :ram, :disk, :monitor,     :colors, :vres, :hres, :net   def initialize(&block)     instance_eval █    end    # Other methods... end desktop = PersonalComputer.new do    self.manufacturer = "Acme"    self.model = "THX-1138"    self.processor = "986"    self.clock = 2.4        # GHz    self.ram = 1024         # Mb    self.disk = 800         # Gb    self.monitor = 25       # inches    self.colors = 16777216    self.vres = 1280    self.hres = 1600    self.net = "T3" end p desktop 

Several things should be noted here. First of all, we're using accessors for our attributes so that we can assign values to them in an intuitive way. Second, the reference to self is necessary because a setter method always takes an explicit receiver to distinguish the method call from an ordinary assignment to a local variable. Of course, rather than define accessors, we could use setter functions.

Obviously, we could perform any arbitrary logic we want inside the body of this block. For example, we could derive certain fields from others by computation.

Also, what if you didn't really want an object to have accessors for each of the attributes? If you prefer, you can use undef (at the bottom of the constructor block) to get rid of any or all of these. At the very least, this could prevent accidental assignment of an attribute from outside the object.

Creating Class-level Attributes and Methods

A method or attribute isn't always associated with a specific instance of a class; it can be associated with the class itself. The typical example of a class method is the new method; it is always invoked in this way because it is called in order to create a new instance (and thus can't belong to any particular instance).

We can define class methods of our own if we want. We have already seen this in "Using Multiple Constructors." But their functionality certainly isn't limited to constructors; they can be used for any general-purpose task that makes sense at the class level.

In this next highly incomplete fragment, we assume that we are creating a class to play sound files. The play method can reasonably be implemented as an instance method; we can instantiate many objects referring to many different sound files. But the detectHardware method has a larger context; depending on our implementation, it might not even make sense to create new objects if this method fails. Its context is that of the whole sound-playing environment rather than any particular sound file.

 

 class SoundPlayer   MAX_SAMPLE = 192   def SoundPlayer.detectHardware     # ...   end   def play     # ...   end end 

Let's note that there is another way to declare this class method. The following fragment is essentially the same:

 

 class SoundPlayer   MAX_SAMPLE = 192   def play     # ...   end end def SoundPlayer.detectHardware   # ... end 

The only difference relates to constants declared in the class. When the class method is declared outside of its class declaration, these constants aren't in scope. For example, detectHardware in the first fragment can refer directly to MAX_SAMPLE if it needs to; in the second fragment, the notation SoundPlayer::MAX_SAMPLE would have to be used instead.

Not surprisingly, there are class variables as well as class methods. These begin with a double @ sign, and their scope is the class rather than any instance of the class.

The traditional example of using class variables is counting instances of the class as they are created. But they can actually be used for any purpose in which the information is meaningful in the context of the class rather than the object. For a different example, see Listing 5.3.

Listing 5.3 Class Variables and Methods
 class Metal   @@current_temp = 70   attr_accessor :atomic_number   def Metal.current_temp=(x)     @@current_temp = x   end    def Metal.current_temp      @@current_temp    end    def liquid?      @@current_temp >= @melting    end    def initialize(atnum, melt)      @atomic_number = atnum      @melting = melt    end end aluminum = Metal.new(13, 1236) copper = Metal.new(29, 1982) gold = Metal.new(79, 1948) Metal.current_temp = 1600 puts aluminum.liquid?        # true puts copper.liquid?          # false puts gold.liquid?            # false Metal.current_temp = 2100 puts aluminum.liquid?        # true puts copper.liquid?          # true puts gold.liquid?            # true 

Note here that the class variable is initialized at the class level before it is used in a class method. Note also that we can access a class variable from an instance method, but we can't access an instance variable from a class method. After a moment of thought, this makes sense.

But what happens if you try? What if we try to print the attribute @atomic_number from within the Metal.current_temp method? We find that it seems to existit doesn't cause an errorbut it has the value nil. What is happening here?

The answer is that we're not actually accessing the instance variable of class Metal at all. We're accessing an instance variable of class Class instead. (Remember that in Ruby, Class is a class!)

Such a beast is called a class instance variable. We would love to give you a creative example of how to use one, but we can't think of any use for it offhand. We summarize the situation in Listing 5.4.

Listing 5.4 Class and Instance Data
 class MyClass   SOME_CONST = "alpha"       # A class-level constant   @@var = "beta"             # A class variable   @var = "gamma"             # A class instance variable   def initialize     @var = "delta"           # An instance variable    end    def mymethod      puts SOME_CONST          # (the class constant)      puts @@var               # (the class variable)      puts @var                # (the instance variable)    end    def MyClass.classmeth1      puts SOME_CONST          # (the class constant)      puts @@var               # (the class variable)      puts @var                # (the class instance variable)    end  end  def MyClass.classmeth2    puts MyClass::SOME_CONST   # (the class constant)    puts @@var                 # (the class variable)    puts @var                  # (the class instance variable)  end  myobj = MyClass.new  MyClass.classmeth1           # alpha, beta, gamma  MyClass.classmeth2           # alpha, beta, gamma  myobj.mymethod               # alpha, beta, delta 

We should mention that a class method can be made private with the method private_class_method. This works the same way private works at the instance level.

For additional information refer to "Automatically Defining Class-level Readers and Writers."

Inheriting from a Superclass

We can inherit from a class by using the < symbol:

 

 class Boojum < Snark   # ... end 

Given this declaration, we can say that the class Boojum is a subclass of the class Snark, or in the same way, Snark is a superclass of Boojum. As we all know, every Boojum is a Snark, but not every Snark is a Boojum.

The purpose of inheritance, of course, is to add or enhance functionality. We are going from the more general to the more specific.

As an aside, many languages such as C++ implement multiple inheritance. Ruby (like Java and some others) doesn't allow MI, but the mixin facility can compensate for this; see the section "Working with Modules."

Let's look at a (slightly) more realistic example. Suppose that we have a Person class and want to create a Student class that derives from it. We'll define Person this way:

 

 class Person   attr_accessor :name, :age, :sex   def initialize(name, age, sex)     @name, @age, @sex = name, age, sex   end   # ... end 

And we'll then define Student in this way:

 

 class Student < Person   attr_accessor :idnum, :hours   def initialize(name, age, sex, idnum, hours)     super(name, age, sex)     @idnum = idnum     @hours = hours   end   # ... end # Create two objects a = Person.new("Dave Bowman", 37, "m") b = Student.new("Franklin Poole", 36, "m", "000-13-5031", 24) 

Now let's look at what we've done here. What is this super that we see called from Student's initialize method? It is simply a call to the corresponding method in the parent class. As such, we give it three parameters, whereas our own initialize method takes five.

It's not always necessary to use super in such a way, but it is often convenient. After all, the attributes of a class form a subset of the attributes of the parent class; so why not use the parent's constructor to initialize them?

Concerning what inheritance really means, it definitely represents the "is-a" relationship. A Student is-a Person, just as we expect. We'll make three other observations.

First, every attribute (and method) of the parent is reflected in the child. If Person had a height attribute, Student would inherit it; and if the parent had a method named say_hello, the child would inherit that, too.

Second, the child can have additional attributes and methods, as you have already seen. That is why the creation of a subclass is often referred to as extending a superclass.

Third, the child can override or redefine any of the attributes and methods of its parent. This brings up the question of how a method call is resolved. How do I know whether I'm calling the method of this particular class or its superclass?

The short answer is: You don't know, and you don't care. If we invoke a method on a Student object, the method for that class will be called if it exists. If it doesn't, the method in the superclass will be called, and so on. We say "and so on" because every class (except Object) has a superclass.

What if we specifically want to call a superclass method, but we don't happen to be in the corresponding method? We can always create an alias in the subclass before we do anything with it.

 

 class Student   # Assuming Person has a say_hello method...   alias :say_hi :say_hello   def say_hello     puts "Hi, there."   end   def formal_greeting     # Say hello the way my superclass would.     say_hi   end end 

There are various subtleties relating to inheritance that we don't discuss here, but this is essentially how it works. Be sure to refer to the next section.

Testing Types or Classes of Objects

Frequently we will want to know: What kind of object is this, or how does it relate to this class? There are many ways of making a determination like this.

First of all, the class method (that is to say, the instance method named class) will always return the class of an object. A synonym is the type method.

 

 s = "Hello" n = 237 sc = s.class    # String st = s.type     # String nc = n.class    # Fixnum 

Don't be misled into thinking that the thing returned by class or type is a string representing the class. It is an actual instance of the class Class. Thus if we wanted, we could call a class method of the target type as though it were an instance method of Class (which it is).

 

 s2 = "some string" var = s2.class             # String my_str = var.new("Hi...")  # A new string 

We could compare such a variable with a constant class name to see if they were equal; we could even use a variable as the superclass from which to define a subclass. Confused? Just remember that in Ruby, Class is an object and Object is a class.

Sometimes we want to compare an object with a class to see whether the object belongs to that class. The method instance_of? accomplishes this.

 

 puts (5.instance_of? Fixnum)        # true puts ("XYZZY".instance_of? Fixnum)  # false puts ("PLUGH".instance_of? String)  # true 

But what if we want to take inheritance relationships into account? The kind_of? method (similar to instance_of?) takes this issue into account. A synonym is is_a? naturally enough because we are describing the classic "is-a" relationship.

 

 n = 9876543210 flag1 = n.instance_of? Bignum     # true flag2 = n.kind_of? Bignum         # true flag3 = n.is_a? Bignum            # true flag3 = n.is_a? Integer           # true flag4 = n.is_a? Numeric           # true flag5 = n.is_a? Object            # true flag6 = n.is_a? String            # false flag7 = n.is_a? Array             # false 

Obviously, kind_of or is_a? is more generalized than the instance_of? relationship. For an example from everyday life, every dog is a mammal, but not every mammal is a dog.

There is one surprise here for the Ruby neophyte. Any module that is mixed in by a class will maintain the "is-a" relationship with the instances. For example, the Array class mixes in Enumerable; this means that any array is a kind of enumerable entity.

 

 x = [1, 2, 3] flag8 = x.kind_of? Enumerable     # true flag9 = x.is_a? Enumerable        # true 

We can also use the numeric relational operators in a fairly intuitive way to compare one class to another. We say intuitive because the less-than operator is used to denote inheritance from a superclass.

 

 flag1 = Integer < Numeric         # true flag2 = Integer < Object          # true flag3 = Object == Array           # false flag4 = IO >= File                # true flag5 = Float < Integer           # false 

Every class typically has a relationship operator === defined. The expression class === instance will be true if the instance belongs to the class. The relationship operator is also known as the case equality operator because it is used implicitly in a case statement. This is therefore a way to act on the type or class of an expression.

For additional information see the section "Testing Equality of Objects."

We should also mention the respond_to? method. This is used when we don't really care what the class is, but just want to know whether it implements a certain method. This, of course, is a rudimentary kind of type information. (In fact, we might say that this is the most important type information of all.) The method is passed a symbol and an optional flag (indicating whether to include private methods in the search).

 

 # Search public methods if wumpus.respond_to?(:bite)   puts "It's got teeth!" else   puts "Go ahead and taunt it." end # Optional second parameter will search # private methods also. if woozle.respond_to?(:bite,true)   puts "Woozles bite!" else   puts "Ah, the non-biting woozle." end 

Sometimes we want to know what class is the immediate parent of an object or class. The instance method superclass of class Class can be used for this.

 

 array_parent = Array.superclass    # Object fn_parent = 237.class.superclass   # Integer obj_parent = Object.superclass     # nil 

Every class except Object will have a superclass.

Testing Equality of Objects

All animals are equal, but some are more equal than others.

George Orwell, Animal Farm

When you write classes, it's convenient if the semantics for common operations are the same as for Ruby's built-in classes. For example, if your classes implement objects that can be ranked, it makes sense to implement the method <=> and mix in the Comparable module. Doing so means that all the normal comparison operators work with objects of your class.

However, the picture is less clear when it comes to dealing with object equality. Ruby objects implement five different methods that test for equality. Your classes might end up implementing some of these, so let's look at each in turn.

The most basic comparison is the equal? method (that comes from Object), which returns true if its receiver and parameter have the same object ID. This is a fundamental part of the semantics of objects, and shouldn't be overridden in your classes.

The most common test for equality uses our old friend ==, which tests the values of its receiver with its argument. This is probably the most intuitive test for equality.

Next on the scale of abstraction is the method eql?, which is part of Object. (Actually, eql? is implemented in the Kernel module, which is mixed in to Object.) Like ==, eql? compares its receiver and its argument, but is slightly stricter. For example, different numeric objects will be coerced into a common type when compared using ==, but numbers of different types will never test equal using eql?.

 

 flag1 = (1 == 1.0)     # true flag2 = (1.eql?(1.0))  # false 

The eql? method exists for one reason: It is used to compare the values of hash keys. If you want to override Ruby's default behavior when using your objects as hash keys, you'll need to override the methods eql? and hash for those objects.

Two more equality tests are implemented by every object. The === method is used to compare the target in a case statement against each of the selectors, using selector===target. Although apparently complex, this rule allows Ruby case statements to be very intuitive in practice. For example, you can switch based on the class of an object:

 

 case an_object   when String     puts "It's a String."   when Number     puts "It's a Number."   else     puts "It's something else entirely." end 

This works because class Module implements === to test whether its parameter is an instance of its receiver or the receiver's parents. So, if an_object is the string "cat", the expression String === an_object would be true, and the first clause in the case statement would fire.

Finally, Ruby implements the match operator =~. Conventionally, this is used by strings and regular expressions to implement pattern matching. However, if you find a use for it in some unrelated classes, you're free to overload it.

The equality tests == and =~ also have negated forms, != and !~, respectively. These are implemented internally by reversing the sense of the non-negated form. This means that if you implement (say) the method ==, you also get the method != for free.

Controlling Access to Methods

In Ruby, an object is pretty much defined by the interface it provides: the methods it makes available to others. However, when writing a class, you often need to write other, helper methods, used within your class but dangerous if available externally. That is where the private method of class Module comes in handy.

You can use private in two different ways. If you call private with no parameters in the body of a class or method definition, subsequent methods will be made private to that class or module. Alternatively, you can pass a list of method names (as symbols) to private, and these named methods will be made private. Listing 5.5 shows both forms.

Listing 5.5 Private Methods
 class Bank   def openSafe     # ...   end   def closeSafe     # ...   end   private :openSafe, :closeSafe   def makeWithdrawl(amount)     if accessAllowed       openSafe       getCash(amount)       closeSafe     end   end   # make the rest private   private   def getCash     # ...   end   def accessAllowed     # ...   end end 

Because the attr family of statements effectively just defines methods, attributes are affected by the access control statements such as private.

The implementation of private might seem strange, but is actually quite clever. Private methods cannot be called with an explicit receiver: They are always called with an implicit receiver of self. This means that you can never invoke a private method in another object: There is no way to specify that other object as the receiver of the method call. It also means that private methods are available to subclasses of the class that defines them, but again only in the same object.

The protected access modifier is less restrictive. Protected methods can only be accessed by instances of the defining class and its subclasses. You can specify a receiver with protected methods, so you can invoke those in different objects (as long as they are objects of the same class as the sender). A common use for protected methods is defining accessors to allow two objects of the same type to cooperate with each other. In the following example, objects of class Person can be compared based on the person's age, but that age isn't accessible outside the Person class.

 

 class Person   def initialize(name, age)     @name, @age = name, age   end   def <=>(other)     age <=> other.age   end   attr_reader :name, :age   protected   :age end p1 = Person.new("fred", 31) p2 = Person.new("agnes", 43) compare = (p1 <=> p2)         # -1 x = p1.age                    # Error!      

To complete the picture, the access modifier public is used to make methods public. This shouldn't be a surprise.

As a final twist, normal methods defined outside a class or module definition (that is, the methods defined at the top level) are made private by default. Because they are defined in class Object, they are globally available, but they cannot be called with a receiver.

Copying an Object

The Ruby built-in methods Object#clone and #dup produce copies of their receiver. They differ in the amount of context they copy. The dup method copies just the object's content, whereas clone also preserves things such as singleton classes associated with the object.

 

 s1 = "cat" def s1.upcase   "CaT" end s1_dup   = s1.dup s1_clone = s1.clone s1                    #=> "cat" s1_dup.upcase         #=> "CAT"  (singleton method not copied) s1_clone.upcase       #=> "CaT"  (uses singleton method) 

Both dup and clone are shallow copies: They copy the immediate contents of their receiver only. If the receiver contains references to other objects, those objects aren't in turn copied; the duplicate simply holds references to them. The following example illustrates this. The object arr2 is a copy of arr1, so changing entire elements, such as arr2[2] has no effect on arr1. However, both the original array and the duplicate contain a reference to the same String object, so changing its contents via arr2 also affects the value referenced by arr1.

 

 arr1 = [ 1, "flipper", 3 ] arr2 = arr1.dup arr2[2] = 99 arr2[1][2] = 'a' arr1              # [1, "flapper", 3] arr2              # [1, "flapper", 99] 

Sometimes, you want a deep copy, where the entire object tree rooted in one object is copied to create the second object. This way, there is guaranteed to be no interaction between the two. Ruby provides no built-in method to perform a deep copy, but there are a couple of techniques you can use to implement one.

The pure way to do it is to have your classes implement a deep_copy method. As part of its processing, this method calls deep_copy recursively on all the objects referenced by the receiver. You then add a deep_copy method to all the Ruby built-in classes that you use.

Fortunately, there's a quicker hack using the Marshal module. If you use marshaling to dump an object into a string and then load it back into a new object, that new object will be a deep copy of the original.

 

 arr1 = [ 1, "flipper", 3 ] arr2 = Marshal.load(Marshal.dump(arr1)) arr2[2] = 99 arr2[1][2] = 'a' arr1              # [1, "flipper", 3] arr2              # [1, "flapper", 99] 

In this case, notice how changing the string via arr2 doesn't affect the string referenced by arr1.

Working with Modules

There are two basic reasons to use modules in Ruby. The first is simply namespace management; we'll have fewer name collisions if we store constants and methods in modules. A method stored in this way (a module method) is called with the module name; that is, without a real receiver. This is analogous to the way a class method is called. If we see calls such as File.ctime and FileTest.exist?, we can't tell just from context that File is a class and FileTest is a module.

The second reason is more interesting: We can use a module as a mixin. A mixin is similar to a specialized implementation of multiple inheritance in which only the interface portion is inherited. We've talked about module methods, but what about instance methods? A module isn't a class, so it can't have instances; and an instance method can't be called without a receiver.

As it turns out, a module can have instance methods. These become part of whatever class does the include of the module.

 

 module MyMod   def meth1     puts "This is method 1"   end end class MyClass   include MyMod   # ... end x = MyClass.new a.meth1                # This is method 1 

Here MyMod is mixed into MyClass, and the instance method meth1 is inherited. You have also seen an include done at the top level; in that case, the module is mixed into Object as you might expect.

But what happens to our module methods, if there are any? You might think they would be included as class methods, but for whatever reason, Ruby doesn't behave that way. The module methods aren't mixed in.

But we have a trick we can use if we want that behavior. There is a hook called append_features that we can override. It is called with a parameter that is the destination class or module (into which this module is being included). For an example of its use, see Listing 5.6.

Listing 5.6 Including a Module with append_features
 module MyMod   def MyMod.append_features(someClass)     def someClass.modmeth       puts "Module (class) method"     end     super   # This call is necessary!   end    def meth1      puts "Method 1"    end  end  class MyClass    include MyMod    def MyClass.classmeth      puts "Class method"    end    def meth2      puts "Method 2"    end  end  x = MyClass.new                        # Output:  MyClass.classmeth     #   Class method  x.meth1               #   Method 1  MyClass.modmeth       #   Module (class) method  x.meth2               #   Method 2 

This example is worth examining in detail. First of all, you should understand that append_features isn't just a hook that is called when an include happens; it actually does the work of the include operation. That's why the call to super is needed; without it, the rest of the module (in this case, meth1) wouldn't be included at all.

Also note that within the append_features call, there is a method definition. This looks unusual, but it works because the inner method definition is a singleton method (class-level or module-level). An attempt to define an instance method in the same way would result in a Nested method error.

Conceivably a module might want to determine the initiator of a mixin. The append_features method can also be used for this because the class is passed in as a parameter.

It is also possible to mix in the instance methods of a module as class methods. An example is shown in Listing 5.7.

Listing 5.7 Module Instance Methods Becoming Class Methods
 class MyMod   def meth3     puts "Module instance method meth3"     puts "can become a class method."   end end class MyClass    class << self    # Here, self is MyClass      include MyMod    end end MyClass.meth3 # Output: #   Module instance method meth3 #   can become a class method. 

We've been talking about methods. What about instance variables? Although it is certainly possible for modules to have their own instance data, it usually isn't done. However, if you find a need for this capability, nothing is stopping you from using it.

It is possible to mix a module into an object rather than a class (for example, with the extend method). See the section "Specializing an Individual Object."

It's important to understand one more fact about modules. It is possible to define methods in your class that will be called by the mixin. This is a very powerful technique that will seem familiar to those who have used Java interfaces.

The classic example (which we've seen elsewhere) is mixing in the Comparable module and defining a <=> method. Because the mixed-in methods can call the comparison method, we now have such operators as <, >, <=, and so on.

Another example is mixing in the Enumerable module and defining <=> and an iterator each. This will give us numerous useful methods such as collect, sort, min, max, and select.

You can also define modules of your own to be used in the same way. The principal limitation is the programmer's imagination.

Transforming or Converting Objects

Sometimes an object comes in exactly the right form at the right time, but sometimes we need to convert it to something else or pretend it's something it isn't. A good example is the well-known to_s method.

Every object can be converted to a string representation in some fashion. But not every object can successfully masquerade as a string. That in essence is the difference between the to_s and to_str methods. Let's elaborate on that.

Methods such as puts and contexts such as #{...} interpolation in strings expect to receive a String as a parameter. If they don't, they ask the object they did receive to convert itself to a String by sending it a to_s message. This is where you can specify how your object will appear when displayed; simply implement a to_s method in your class that returns an appropriate String.

 

 class Pet   def initialize(name)     @name = name   end   # ...   def to_s     "Pet: #@name"   end end 

Other methods (such as the String concatenation operator +) are more picky; they expect you to pass in something that is really pretty close to a String. In this case, Matz decided not to have the interpreter call to_s to convert nonstring arguments because he felt this would lead to too many errors. Instead, the interpreter invokes a stricter method, to_str. Of the built-in classes, only String and Exception implement to_str, and only String, Regexp, and Marshal call it. Typically when you see the runtime error TypeError: Failed to convert xyz into String, you know that the interpreter tried to invoke to_str and failed.

You can implement to_str yourself. For example, you might want to allow numbers to be concatenated to strings:

 

 class Numeric   def to_str     to_s   end end label = "Number " + 9      # "Number 9" 

An analogous situation holds for arrays. The method to_a is called to convert an object to an array representation, and to_ary is called when an array is expected.

An example of when to_ary is called is with a multiple assignment. Suppose that we have a statement of this form:

 

 a, b, c = x 

Assuming that x were an array of three elements, this would behave in the expected way. But if it isn't an array, the interpreter will try to call to_ary to convert it to one. For what it's worth, the method we define can be a singleton (belonging to a specific object). The conversion can be completely arbitrary; here we show an (unrealistic) example in which a string is converted to an array of strings:

 

 class String   def to_ary     return self.split("")   end end str = "UFO" a, b, c = str     # ["U", "F", "O"] 

The inspect method implements another convention. Debuggers, utilities such as irb, and the debug print method p use the inspect method to convert an object to a printable representation. If you want classes to reveal internal details when being debugged, you should override inspect.

There is another situation in which we'd like to be able to do conversions of this sort under the hood. As a language user, you'd expect to be able to add a Fixnum to a Float, or divide a Complex number by a rational number. However, this is a problem for a language designer. If the Fixnum method + receives a Float as an argument, what can it do? It only knows how to add Fixnum values. Ruby implements the coerce mechanism to deal with this.

When (for example) + is passed an argument it doesn't understand, it tries to coerce the receiver and the argument to compatible types and then do the addition based on those types. The pattern for using coerce in a class you write is straightforward:

 

 class MyNumberSystem   def +(other)     if other.kind_of?(MyNumberSystem)       result = some_calculation_between_self_and_other       MyNumberSystem.new(result)     else       n1, n2 = other.coerce(self)       n1 + n2     end   end end 

The value returned by coerce is a two-element array containing its argument and its receiver converted to compatible types.

In this example, we're relying on the type of our argument to perform some kind of coercion for us. If we want to be good citizens, we also need to implement coercion in our class, allowing other types of numbers to work with us. To do this, we need to know the specific types that we can work with directly, and convert ourselves to those types when appropriate. When we can't do that, we fall back on asking our parent.

 

 def coerce(other)   if other.kind_of?(Float)     return other, self.to_f   elsif other.kind_of?(Integer)     return other, self.to_i   else     super   end end 

Of course, for this to work, our object must implement to_i and to_f.

You can use coerce as part of the solution for implementing a Perl-like auto-conversion of strings to numbers:

 

 class String   def coerce(n)     if self['.']       [n, Float(self)]     else       [n, Integer(self)]     end   end end x = 1 + "23"        # 24 y = 23 * "1.23"     # 29.29 

We don't necessarily recommend this. But we do recommend that you implement a coerce method whenever you are creating some kind of numeric class.

Creating Data-only Classes (Structs)

Sometimes you need to group together a bunch of related data with no other associated processing. You could do this by defining a class:

 

 class Address   attr_accessor :street, :city, :state   def initialize(street1, city, state)     @street, @city, @state = street, city, state   end end books = Address.new("411 Elm St", "Dallas", "TX") 

This works, but it's tedious, with a fair amount of repetition. That's why the built-in class Struct comes in handy. In the same way that convenience methods such as attr_accessor define methods to access attributes, the class Struct defines classes that contain just attributes. These classes are structure templates.

 

 Address = Struct.new("Address", :street, :city, :state) books = Address.new("411 Elm St", "Dallas", "TX") 

So, why do we pass the name of the structure to be created in as the first parameter of the constructor, and also assign the result to a constant (Address, in this case)?

When we create a new structure template by calling Struct.new, a new class is created within class Struct itself. This class is given the name passed in as the first parameter, and the attributes given as the rest of the parameters. This means that if we wanted, we could access this newly created class within the namespace of class Struct.

 

 Struct.new("Address", :street, :city, :state) books = Struct::Address.new("411 Elm St", "Dallas", "TX") 

After you've created a structure template, you call its new method to create new instances of that particular structure. You don't have to assign values to all the attributes in the constructor: Those that you omit will be initialized to nil. Once created, you can access the structure's attributes using normal syntax or by indexing the structure object as if it were a Hash. For more information, look up class Struct in any reference.

By the way, we advise against the creation of a Struct named Tms because there is already a predefined Struct::Tms class.

Freezing Objects

Sometimes we want to prevent an object from being changed. The freeze method (in Object) will allow us to do this, effectively turning an object into a constant.

After we freeze an object, an attempt to modify it results in a TypeError. Listing 5.8 shows a pair of examples.

Listing 5.8 Freezing an Object
 str = "This is a test. " str.freeze begin   str << " Don't be alarmed."   # Attempting to modify rescue => err   puts "#{ err.class}  #{ err} " end  arr = [1, 2, 3]  arr.freeze  begin    arr << 4                      # Attempting to modify  rescue => err    puts "#{ err.class}  #{ err} "  end # Output: #   TypeError: can't modify frozen string #   TypeError: can't modify frozen array 

However, bear in mind that freeze operates on an object reference, not on a variable! This means that any operation resulting in a new object will work. Sometimes this isn't intuitive.

 

 str = "counter-" str.freeze str += "intuitive"       # "counter-intuitive" arr = [8, 6, 7] arr.freeze arr += [5, 3, 0, 9]      # [8, 6, 7, 5, 3, 0, 9] 

Why does this happen? A statement a += x is semantically equivalent to a = a + x. The expression a + x is evaluated to a new object, which is then assigned to a. The object isn't changed, but the variable now refers to a new object. All the reflexive assignment operators will exhibit this behavior, as will some other methods. Always ask yourself whether you are creating a new object or modifying an existing one; then freeze won't surprise you.

There is a method frozen?, which will tell you whether an object is frozen.

 

 hash = {  1 => 1, 2 => 4, 3 => 9 } hash.freeze arr = hash.to_a puts hash.frozen?                   # true puts arr.frozen?                    # false hash2 = hash puts hash2.frozen?                  # true 

As we see here (with hash2), it is the object, not the variable, that is frozen.


   

 

 



The Ruby Way
The Ruby Way, Second Edition: Solutions and Techniques in Ruby Programming (2nd Edition)
ISBN: 0672328844
EAN: 2147483647
Year: 2000
Pages: 119
Authors: Hal Fulton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net