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
andname
. Methodname
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!