OOP in Ruby

 
   

Ruby Way
By Hal Fulton
Slots : 1.0
Table of Contents
 


Ruby has all the elements more generally associated with OOP languages, such as objects with encapsulation and data hiding, methods with polymorphism and overriding, and classes with hierarchy and inheritance. It goes farther and adds limited metaclass features, singleton methods, modules, and mixins.

Similar concepts are known by other names in other OOP languages, but concepts of the same name may have subtle differences from one language to another. This section elaborates on the Ruby understanding and usage of these elements of OOP.

Objects

In Ruby, all numbers, strings, arrays, regular expressions, and many other entities are actually objects. Work is done by executing the methods belonging to the object:

 

 3.succ                  # 4  "abc".upcase           # "ABC"  [2,1,5,3,4].sort       # [1,2,3,4,5] someObject.someMethod   # some result 

In Ruby, every object is an instance of some class; the class contains the implementation of the methods. The object's class is essentially its type:

 

  "abc".type    # String  "abc".class   # String 

In addition to encapsulating its own attributes and operations, an object in Ruby has an identity.

 

  "abc".id      #  53744407 

Built-in Classes

More than 30 built-in classes are predefined in the Ruby class hierarchy. Like many other OOP languages, Ruby does not allow multiple inheritance, but that does not necessarily make it any less powerful. Modern object-oriented languages frequently follow the single-inheritance model. Ruby does support modules and mixins, which are discussed in the next section. It also implements object IDs, which support the implementation of persistent, distributed, and relocatable objects.

To create an object from an existing class, the new method is typically used:

 

 myFile = File.new("textfile.txt","w") myString = String.new("this is a string object") 

This is not always explicitly required, however, as shown here:

 

 yourString = "this is also a string object" aNumber = 5 

Variables are used to hold references to objects. As previously mentioned, variables themselves have no type, nor are they objects themselves; they are simply references to objects. Here's an example:

 

 x = "abc" 

An exception to this is that small immutable objects of some built-in classes, such as Fixnum, are copied directly into the variables that refer to them. (These objects are no bigger than pointers, and it is more efficient to deal with them in this way.) In this case, the assignment makes a copy of the object, and the heap (memory allocation area) is not used.

Variable assignment causes object references to be shared:

 

 y = "abc" x = y x           # "abc" 

After x = y is executed, variables x and y both refer to the same object:

 

 x.id        # 53732208 y.id        # 53732208 

If the object is mutable, a modification done to one variable will be reflected in the other one:

 

 x.gsub!(/a/,"x") y                  # "xbc" 

Reassigning one of these variables has no effect on the other, however:

 

 x = "abc" y                  # still has value "xbc" 

A mutable object can be made immutable using the freeze method:

 

 x.freeze x.gsub!(/b/,"y")   #  error 

A symbol in Ruby refers to a variable by ID rather than by reference. When we say :x, we are saying basically the same as x.id (which you saw previously). A colon applied to an identifier results in a symbol; if the identifier does not already exist, it is created. Among other uses, a symbol may be used when we want to mention an identifier as opposed to using it (the classical use/mention distinction); for example, the special method method_missing, called when a method is not found, gets passed a symbol corresponding to the unknown method. Any Symbol object has a method called id2name that returns a string corresponding to the identifier name. Here are examples:

 

 Hearts    = :Hearts   # This is one way of assigning Clubs     = :Clubs    #   unique values to constants, Diamonds  = :Diamonds #   somewhat like an enumeration Spades    = :Spades   #   in Pascal or C. print Hearts.id2name  # Prints "Hearts" 

Modules and Mixins

Many built-in methods are available from class ancestors. Of special note are the Kernel methods mixed in to the Object superclass; because Object is universally available, the methods that are added to it from Kernel are also universally available. These methods form a very important part of Ruby.

The terms module and mixin are nearly synonymous. A module is a collection of methods and constants that is external to the Ruby program. It can be used simply for namespace management, but the most common use of a module is to have its features "mixed" in to a class (by using include). In this case, it is used as a mixin. (This term, apparently borrowed from Python, is sometimes written as mix-in, but we write it as a single word.)

An example of using a module for namespace management is the frequent use of the Math module. To make use of the definition of pi, for example, it is not necessary to include the Math module; you can simply use Math::PI as the constant.

A mixin provides a way of getting some of the benefits of multiple inheritance without dealing with all the difficulties. It can be considered a restricted form of multiple inheritance, but the language creator Matz has called it "single inheritance with implementation sharing."

Note that include appends features of a namespace (a module) to the current space. The extend method appends functions of a module to an object. With include, the module's methods become available as instance methods; with extend, they become available as class methods.

We should mention that load and require do not really relate to modules but rather to non-module Ruby sources and binaries (statically or dynamically loadable). A load operation essentially reads a file and inserts it at the current point in the source file so that its definitions become available at that point. A require operation is similar to a load, but it will not load a file if it has already been loaded.

Creating Classes

Ruby has numerous built-in classes, and additional classes may be defined in a Ruby program. To define a new class, the following construct is used:

 

 class ClassName   # ... end 

The name of the class is itself a global constant and therefore must begin with an uppercase letter. The class definition can contain class constants, class variables, class methods, instance variables, and instance methods. Class data is available to all objects of the class, whereas instance data is only available to the one object. Here's an example:

 

 class Friend   @@myname = "Fred"             # a class variable   def initialize(name, sex, phone)     @name, @sex, @phone = name, sex, phone     # These are instance variables   end   def hello                     # an instance method     print "Hi, I'm #{ @name} .\n"   end   def Friend.our_common_friend  # a class method     print "We are all friends of #{ @@myname} .\n"   end end f1 = Friend.new("Susan","F","555-0123") f2 = Friend.new("Tom","M","555-4567") f1.hello                     # Hi, I'm Susan. f2.hello                     # Hi, I'm Tom. Friend.our_common_friend     # We are all friends of Fred. 

Because class-level data is accessible throughout the class, it can be initialized at the time the class is defined. If a method named initialize is defined, it is guaranteed to be executed right after an instance is allocated. The initialize method is similar to the traditional concept of a constructor, but it does not have to handle memory allocation. Allocation is handled internally by new, and deallocation is handled transparently by the garbage collector.

Now consider this fragment, paying attention to the getmyvar, setmyvar, and myvar= methods:

 

 class MyClass   NAME = "Class Name"  # class constant   def initialize       # called when object is allocated     @@count += 1     @myvar = 10   end   def MyClass.getcount # class method     @@count            # class variable   end   def getcount         # instance returns class variable!     @@count            # class variable   end   def getmyvar         # instance method     @myvar             # instance variable   end   def setmyvar(val)    # instance method sets @myvar     @myvar = val   end   def myvar=(val)      # Another way to set @myvar     @myvar = val   end end foo = MyClass.new      # @myvar is 10 foo.setmyvar 20        # @myvar is 20 foo.myvar = 30         # @myvar is 30 

Here, you see that getmyvar returns the value of @myvar, and setmyvar sets it. (In the terminology of many programmers, these would be referred to as a getter and a setter, respectively.) These work fine, but they do not exemplify the Ruby way of doing things. The method myvar= looks like assignment overloading (although strictly speaking, it isn't); it is a better replacement for setmyvar, but there is a better way yet.

The class Module contains methods called attr, attr_accessor, attr_reader, and attr_writer. These can be used (with symbols as parameters) to automatically handle controlled access to the instance data. For example, the three methods named previously can be replaced by a single line in the class definition:

 

 attr_accessor :myvar 

This will create the method myvar, which returns the value of @myvar, and the method myvar=, which enables the setting of the same variable. Methods attr_reader and attr_writer create read-only and write-only versions of an attribute, respectively. For more details, consult a Ruby reference.

Within the instance methods of a class, the pseudo-variable self can be used as needed. This is only a reference to the current receiver, the object on which the instance method is invoked.

The modifying methods private, protected, and public can be used to control the visibility of methods in a class. (Instance variables are always private and inaccessible from outside the class except by means of accessors.) Each of these modifiers takes a symbol such as :foo as a parameter; if this is omitted, the modifier applies to all subsequent definitions in the class. Here's an example:

 

 class MyClass   def method1     # ...   end   def method2     # ...   end   def method3     # ...   end   private :method1   public  :method2   protected :method3   private   def my_method     # ...   end   def another_method     # ...   end end 

In this example, method1 will be private, method2 will be public, and method3 will be protected. Because of the private method with no parameters, both my_method and another_method will be private.

The public access level is self-explanatory; there are no restrictions on access or visibility. The private level means that the method is accessible only within the class or its subclasses, and it is callable only in "function form," with self (implicit or explicit) as a receiver. The protected level means that a method is callable only from within its class, but unlike a private method, it can be called with a receiver other than self, such as another instance of the same class.

The default visibility for the methods defined in a class is public. The exception is the instance-initializing method initialize, which is private because it is intended to be called only from the new method. Methods defined at the top level are also public by default; if they are private, they can be called only in function form (as, for example, the methods defined in Object).

Ruby classes are themselves objects, being instances of the metaclass Class. Ruby classes are always concrete; there are no abstract classes. However, it is theoretically possible to implement abstract classes in Ruby if you really wish to do so.

The class Object is at the root of the hierarchy. It provides all the methods defined in the built-in Kernel module.

To create a class that inherits from another class, define it in this way:

 

 class MyClass < OtherClass   # ... end 

In addition to using built-in methods, it is only natural to define your own and also to redefine and override existing ones. When you define a method with the same name as an existing one, the previous method is overridden. If a method needs to call the "parent" method that it overrides (a frequent occurrence), the keyword super can be used for this purpose.

Operator overloading is not strictly an OOP feature, but it is very familiar to C++ programmers and certain others. Because most operators in Ruby are simply methods anyway, it should come as no surprise that these operators can be overridden or defined for user-defined classes. Overriding the meaning of an operator for an existing class may be rare, but it is common to want to define operators for new classes.

It is possible to create aliases or synonyms for methods. The syntax (used inside a class definition) is as follows:

 

 alias newname oldname 

The number of parameters will be the same as for the old name, and it will be called in the same way.

Methods and Attributes

In a previous section, methods were used with simple class instances and variables by separating the receivers from the methods with a period (receiver.method). In the case of method names that are punctuation, the period is omitted. Methods can take arguments:

 

 Time.mktime( 2000, "Aug", 24, 16, 0 ) 

Because method calls return objects, method calls may typically be chained or stacked:

 

 3.succ.to_s       /(x.z).*?(x.z).*?/.match("x1z_1a3_x2z_1b3_").to_a[1..3]       3+2.succ 

Note that problems can arise if the cumulative expression is of a type that does not support that particular method. Specifically, some methods return nil under certain conditions, and this will usually cause any methods tacked onto that result to fail.

Certain methods may have blocks passed to them. This is true of all iterators, whether built in or user defined. A block is usually passed as a do-end block or a brace-delimited block; it is not treated like the other parameters preceding it, if any. See especially the File.open example:

 

 my_array.each do |x|   some_action end File.open(filename) {  |f| some_action } 

Named parameters will be supported in the future but are not supported at the time of this writing. These are called keyword arguments in the Python realm.

Methods may take a variable number of arguments:

 

 receiver.method(arg1, *more_args) 

In this case, the method called will treat more_args as an array that it deals with as it would any other array. In fact, an asterisk in the list of formal parameters (on the last or only parameter) can likewise "collapse" a sequence of actual parameters into an array:

 

 def mymethod(a, b, *c)   print a, b   c.each do |x| print x end end mymethod(1,2,3,4,5,6,7)    # a=1, b=2, c=[3,4,5,6,7] 

Ruby has the ability to define methods on a per-object basis (rather than per class). Such methods are called singletons; they belong solely to that object and have no effect on its class or superclasses. As an example, this might be useful in programming a GUI; you can define a button action for a widget by defining a singleton method for the button object.

It is theoretically possible to create a prototype-based object system using singleton methods. This is a less traditional form of OOP without classes. The basic structuring mechanism is to construct a new object using an existing object as a delegate; the new object is exactly like old except for the items that are overridden. This enables you to build prototype/delegation-based systems rather than inheritance based. Although we do not have experience in this area, we do feel that this demonstrates the power of Ruby.


   

 

 



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