Objects#

Overview#

In general, object is the a structure with its own properties (it can carry the data which represents and describes the object) and methods (functions or procedures associated with the object). For example, you may want to have object Server, represents the machine in the network you manage. In this case, this object is a model of reality: it represents the physical box standing deep in the vault of your server room. Such object may contain data fields like name, domain and methods like get_ip (the function returning IP of the machine, based on name and domain). Notice there is no need to pass the server name and domain to get_ip method, because the method itself has an access to the object properties.

Classes#

Object must have the defnition - it is called the class. You may think about the class as a type of the object. In Ruby class is a contant, so must begin with uppercase letter and, by convention, it is good to use CamelCase for class names.

class Server
  def get_ip
    'unknown IP'
  end
end

The class contains only one method. Note the get_ip is a mock method for now, it returns constant string, not the proper IP address.

In Ruby, there is no need to define class in one piece. You can extend definition of a class wherever you want, just remember that new method definition overwrites the previous one.

class Server
  def get_ip
    'unknown IP'
  end
  def name
    'unknown server name'
  end
end

class Server
  def domain
    'unknown domain'
  end
  def name
    'unknown name'
  end
end

This code defines class Server with three methods: get_ip, domain and name. Method name will return ‘unknown name’ string, as this version was defined later and overwrites the definition from lines 5 - 7.

Instance#

If Server is something like a type, can we create a variable with this type? First, we need to allocate the memory for the object and create an object there. This representation in the memory is called the instance of the object, and the process of constructing it by language interpreter is called object creation. To create a new, empty instance and assign it to variable we can use the class method called new:

server = Server.new()

Class Definitions in Irb#

For practice and debug, you can write a script, run it and observe the result. But you can use Irb as well - it is possible to define class or methods directly there:

class Server
  def get_ip
    'unknown ip'
  end
end
#=> nil
server = Server.new
#=> #<Server:0x007fc6bc1d4978>
server.get_ip
#=> "unknown ip"

Notice that irb is changing the continuation level number in a prompt (the last number), to help you remember you are inside a definition of class or a method.

Another way to debug a script is to load it to Irb. To do it, use load command - this will run the given file, so every definition of classes, methods, variables will be accessible from Irb session.

load 'server_object.rb'
#=> true
server = Server.new
#=> #<Server:0x007fb76bb0a548>
server.get_ip
#=> "unknown IP"

When you check the value of server variable in Irb, it will show human readable representation of the instance, such like #Server:0x007fb76bb0a548. This is the object’s class name with the instance identifier (hexadecimal representation of the memory address).

Instance variables#

For now, we defined the Server object with only few methods: it is not storing any data in it. The purpose of the object is to represent (to model) the real world. Real servers have some properties like the name and the domain. Such information should be stored in the instance variable, so every instance of Server class can have its own name properties. Instance variables syntax is the same as normal, local variables: lowercase, snake_case symbol but followed by character @. In the example below there are two new methods to set server name and server address:

class Server
  def set_name(name)
    @name = name
  end
  def set_domain(domain)
    @domain = domain
  end
  def get_name
    @name
  end
  def get_domain
    @domain
  end
end

server = Server.new
server.set_name 'vader'
server.set_domain '.starwars.com'

puts server.get_name + server.get_domain  # prints 'vader.starwars.com'

Methods to set and get instance variables are called setters and getters.

Try to construct an object with instance properties in Irb. When you inspect it, you will see the representation of the object changed. You will get now something like #<Server:0x007fc688e2ebd0 @name=”vader”, @domain=”.starwars.com”> - it is showing the instance variable content for better readability.

Setters and getters like above are not very elegant. It would be much better to just have a direct access to instance variable, to set its value with server.name = ‘vader’ and get it with simple server.name. In Ruby there is no direct access to the instance variables, but why not to write the setters and getters in more readable format:

class Server
  def name=(name)
    @name = name
  end
  def domain=(domain)
    @domain = domain
  end
  def name
    @name
  end
  def domain
    @domain
  end
end

server = Server.new
server.name = 'obi'
server.domain = '.starwars.com'

puts server.name + server.domain  # prints 'obi.starwars.com'

It looks like a direct access to object properties, but it is not - when calling object.some_method = … Ruby invokes method called some_method=, the one with equal sign at the end. For getter, it is much easier: the method name is just the same as the instance variable name.

attr_accessor, attr_writter and attr_reader#

In a big projects writing getters and setters for every instance variable in the way like above might be quite boring. Why don’t let Ruby do it for us? There are special functions for creating setters and getters for the given variable names: attr_writter creates the setter, attr_reader produces a getter and attr_accessor - both setter and getter.

class Server
  attr_reader :name     # this two lines could be replaced by one
  attr_writer 'name'    # -- attr_accessor :name
  attr_accessor :domain
  # you may replace the three lines above but just one,
  # passing all method names separated by commas
  #   attr_accessor :name, :domain
end

server = Server.new
server.name = 'yoda'
server.domain = '.starwars.com'

puts server.name + server.domain  # prints 'yoda.starwars.com'

attr_* methods are taking the list of variable names as the arguments. You can give this name as a string (like ‘name’ in the example), but much better is to use Ruby symbols. Symbols are the kind of strings, written with colon at the beginning (:name in the example), but unlike the strings they are not mutable - you can’t change it. They are commonly used to pass the method names, variable named etc.

Instance Constructor#

We now know how to create a new instance with new class method. It is possible to control the object creation code: if Ruby found a method called initialize it lauches it while creating an object. In the example below the instance variables are set to default values while constructing an object:

class Server
  attr_accessor :name, :domain
  def initialize
    @name = 'default'
    @domain = '.domain.com'
  end
end

server = Server.new
server.name = 'yoda'  # changing the value of @name

puts server.name + server.domain  # prints 'yoda.domain.com'

You can pass the arguments to new class method - Ruby will pass them to the initialize instance method. This is commonly use for passing the initialization values for the class variables. Notice that the number of arguments in new must equal the number of arguments in initialize:

class Server
  attr_reader :name, :domain
  def initialize(name, domain)
    @name = name
    @domain = domain
  end
end

server = Server.new 'kirk', '.startrek.com'
puts server.name + server.domain  # prints 'kirk.startrek.com'

Class Methods and Variables#

So far we have created the instance methods, which are functions running on object instance, having an access to all its properties - but only in the scope of the current object. Class methods are different - they are running on the class, not on the object instance, and they do not need any instance initialized. You know one of the class method - the object constructor, new. It is invoked on the class, like Server.new. This method must be a class method - before the first initialization with new there is no instance to run the function on. To define class method, use self. syntax before method name.

Class variables are like the instance variables, but with the scope on the class, not on the specific object. That means all of the objects of specific class have an access to this values - it is shared between them. Class variables are defined with double ‘at’ symbol @@ before the variable name.

Class methods are commonly used for object initialization, besides Object.new there are many of this kind, for example File.open(filename) which creates an object representing the file with the given path. Class variables might be useful when you need to do something on all the object of a kind, for example to count all defined servers:

class Server
  @@server_count = 0      # initial value for class method
  attr_accessor :name, :domain

  def initialize(name, domain)
    @name = name
    @domain = domain
    @@server_count += 1   # increment the class method
  end

  def self.count          # definition of class method
    @@server_count        # returns the value of @@server_count class variable
  end
end

puts Server.count   # prints '0'
yoda = Server.new 'yoda', '.starwars.com'
kirk = Server.new 'kirk', '.startrek.com'
puts Server.count   # prints '2'

Self#

self is a keyword to represent a current object instance. You can use it in the class definition to access the instance itself. In the example below, method url calls the method full_name on the same object.

class Server
  def initialize(name, domain)
    @name = name
    @domain = domain
  end
  def full_name     # returns name and domain of the object
    @name + @domain
  end
  def url           # returns full name followed by 'http://'
    'http://' + self.full_name
  end
end

yoda = Server.new 'yoda', '.starwars.com'
puts yoda.full_name   # prints 'yoda.starwars.com'
puts yoda.url         # prints 'http://yoda.starwars.com'

puts Server.new('kirk', '.startrek.com').url   # prints 'http://kirk.startrek.com'

Just to call the method in the example above you may skip the self keyword. Ruby is searching for a method in the current object instance first, so this will work as well:

def url
  'http://' + full_name
end

Object Inheritance#

Inheritance is a very important concept in the object-oriented programming. When the class (we will call it subclass) inherits another class (called superclass or ancestor), it is getting the whole structure of the parent class, all the code: variables, methods etc. This provides the class hierarchy, where most general classes are at the top, and the detailed ones at the bottom of hierarchy tree.

The syntax for inheritance in Ruby is class SubClassName < SuperClassName.
Assume we want to have two new classes, adding more details to our servers model. We can create two classes, WindowsServer and UnixServer, which we want to behave the same as a general Server class:

class Server                  # Server is a superclass for both classes below
  def initialize(name, domain)
    @name = name
    @domain = domain
  end
  def full_name
    @name + @domain
  end
end

class WindowsServer < Server  # WindowsServer inherits from Server
end

class UnixServer < Server     # UnixServer inherits from Server as well
  def execute_via_ssh         # to be implemented in a future
    'not implemented yet'
  end
end

win = WindowsServer.new('yoda', '.starwars.com')
unix = UnixServer.new('kirk', '.startrek.com')

puts unix.full_name         # prints 'kirk.startrek.com'
puts win.full_name          # prints 'yoda.starwars.com'
puts unix.class             # prints 'UnixServer'
puts win.class              # prints 'WindowsServer'
puts unix.execute_via_ssh   # prints 'not implemented yet'
puts win.execute_via_ssh    # raises exception (an error): "undefined method `execute_via_ssh'"

Lines 1 - 9 provides us the well know implementation of the Server object. Then comes the definition of WindowsServer class comes, which is a subclass of the Server. That means this class now have the same implementation like its superclass - it has the same methods and variables like the Server. So we can create it with arguments (line 20) just like the Server, run the method from the superclass (line 24) - all of this code WindowsServer inherits from the ancestor.

The similar is for UnixServer class - it has all the methods and variables inherited from Server. In addition, this class have one more instance method - execute_via_ssh (now it is not doing nothing, except the nasty information). You can call this method, but only on UnixServer object! It is not available in objects of WindowsServer or even Server class.

The method from the superclass can be overwritten in the subclass. For example, you may want to change behaviour of the full_name method - in case it is Windows server, you want to be the server name only, without a domain, upper-case.

class WindowsServer < Server
  def full_name
    @name.upcase
  end
end

upcase is an instance method of String object to change all the characters of the string to uppercase.

Ruby searches for a method first in the current object and only when not found the interpreter start searching in the superclasses. So after the changes above the method gives the desired server name for WindowsServer object:

irb(main):025:0> win = WindowsServer.new('yoda', '.starwars.com')
#=>#<WindowsServer:0x007fb83a0c7318 @name="yoda", @domain=".starwars.com">
irb(main):027:0> win.full_name
#=>"YODA"

Inheritance (and similar techniques, like mixins) is really important in Ruby. Remember the method new to create a new instance? It magically appears in the newly defined Server class. Well, this particular method comes from Class class. Refer to following documentation http://ruby-doc.org/core-2.0.0/Class.html to learn more.

Object Operators#

In Ruby, most operators are actually the method calls on the objects. Thus a + b means run the method called + on object a with argument b - this is another way to write a.+(b) or a.+ b. You can try it in Irb, instead of typing 2 + 2 you can calculate this by directly calling the method + on object instance 2 with the argument of object instance 2: 2.+(2). It works!

Sometimes the operator does not makes sense for the specified kind of object. We can add numbers, strings (to concatenate), but we cannot add servers one to each other. Thats why we need to find out another operator to play with the examples. Lets discuss the equality operator ==. Can we check if two servers are equal? Yes, if the servers have the same name and domain, they must be the same machines. By default, we have the == defined for every object, but it does not work as we expect:

server1 = Server.new('yoda','.starwars.com')
#=> #<Server:0x007fdf22830af0 @name="yoda", @domain=".starwars.com">
server2 = Server.new('yoda','.starwars.com')
#=> #<Server:0x007fdf219f34d8 @name="yoda", @domain=".starwars.com">
server1 == server2
#=> false

This is because we defined two object instances and, from the Ruby point of view, these are a different constructs. Just observe the identificator (the memory address) of server1 and server2. To fix this behaviour it is time to override the equality operator:

class Server
  attr_accessor :name, :domain
  def initialize(name, domain)
    @name = name
    @domain = domain
  end
  def ==(other)
    name == other.name && domain == other.domain  # && is a logical AND operator
                                                  # it is true if both arguments are true
  end
end

s1 = Server.new('yoda', '.starwars.com')
s2 = Server.new('yoda', '.starwars.com')
s3 = Server.new('kirk', '.starwars.com')

puts s1 == s2   # true !
puts s2 == s3   # false, different names

Line 7 defines operator as an instance method on object Server. It is the only one line of code (line 8), which means: “return true if my name equals the other name AND if my domain equals the other domain”. In this line operator == checks equality of String objects (@name and @domain are the character strings). To better understanding, we can write this line in different way: (self.name == other.name) && (self.domain == other.domain) or with if-then-else statement:

if name == other.name && domain == other.domain
  return true
else
  return false
end

But because the method always returns the value of the last statement, there is no need to bother with if-then-else. Simplicity first!

Inspect#

By default objects in irb are shown by the class name, the instance identifier (the unique identifier for all objects) and the instance variables, if any. This is quite handy, but we can do better. To show this value, irb runs method inspect on the specified instance. If you define your own, you can output better human-readable description of the object. In this case, we want our Server object to be shown as * [class name]: [server and domain] *

server = Server.new('yoda', '.starwars.com')
#=> #<Server:0x007ff231124fa0 @name="yoda", @domain=".starwars.com">  # default inspect value
class Server
  def inspect                                              # our own inspect
    "* " + self.class.to_s + ": " + @name + @domain + " *" # * [class name]: [server and domain] *
  end
end
server
#=> * Server: yoda.starwars.com *          # works as expected!