Object Oriented Programming in Ruby by Example
There is lots of information out there about object oriented programming (OOP), but if your like me I find most of this information to be much too abstract. So, rather than make one more academic blog post about the four principles of OOP I will take you through a hands on example written in Ruby.
Data Abstraction
The first principle of OOP we are going to cover is data abstraction. Data abstraction is just a fancy way of saying that we are going to model our data to look like it's real life equivalent. So, how do we model a ship? Well a ship should be an object that has a position and an acceleration constant. It should also be able to accelerate and move. Translating this into Ruby and we get something like this.
This is what is called a class. A class is just a blueprint that is use to create objects that represent some structured data which in this case is a ship. Without getting into too much detail ACCELERATION
is a constant that is global in scope and can be accessed using Ship::ACCELERATION
outside the class or just ACCELERATION
within the class. @position
and @speed
are instance variables which have local scope and therefore can only be accessed from within the class and don't actually exist until an object is created. initialize
is a constructor method which creates an object when called with Ship.new
. Finally, acceleration
and move
are instance methods that are global in scope and can therefore be accessed from outside a class as well as within a class. From outside the class they are accessed using objectInstanceName.methodName
and from within the class using methodName
for example ship.move
and move
.
Encapsulation
Encapsulation just means that all of the implementation details of the object being modeled are hidden and only the methods and properties required to use the object are exposed. For our Ship
class @position
and @speed
are hidden as they do not need to be known outside the class since they are just used by the methods acceleration
and move
. The methods acceleration
and move
on the other hand are public since the main body of the script will need to call these methods to cause the ship to accelerate or move. Finally, the ACCELERATION
constant is public, but it cannot be changed. This is encapsulation at work. The ship has a clearly defined interface for it's use namely the acceleration
and move
methods and the rest of it's implementation is either hidden from view or cannot be altered. This is important because it allows for us to alter the class definition for example the move
or acceleration
methods, but it will not affect any other part of the program which uses Ship
since the interface remains the same. Call accelerate
to accelerate and move
to move.
Inheritance
Inheritance is a real time saver as it will allow us to create classes, or new object blueprints, which will inherit constants, properties, and methods from it's parent class. This allows for us to create a class hierarchy or taxonomy if you will. In our example we will add a new class which will represent a special kind of space ship; a fighter. Fighters are just like any other spaceship except they can also fire missiles! So, instead of having to add all of the same kinds of constants, properties, and methods we will just make Fighter a subclass of Ship. Once this is done the only thing for us to do is add a method to fire missiles.
Pretty cool right? You should note, however, that a class can only inherit from one other class. Classes can be chained together though creating an inheritance tree. For example Stealth_Fighter < Fighter < Ship. Modules are used in Ruby to simulate the multiple inheritance of other OOP languages, but we are not going to cover these here.
Polymorphism
Where do they come up with these names? Latin of course! Polymorphism means many forms which aptly describes the ability of OOP languages to use the same identifier to cause different behavior. So far in our program Fighter
has inherited all of it's implementation details from it's parent class Ship
except for the fire_missle
method, but this doesn't seem quite right. Fighters should have a greater acceleration and the ability to keep track of how many missiles they currently have. So lets make these changes using polymorphism.
Using polymorphism we can alter or completely replace inherited features, but we don't have to change their identifiers, or names, or the interface that is used to use or access them! In this example we have replaced the original acceleration constant with one that is twice as much. We have also extended the original initialize
method to include a new instance variable @missles
which will keep track of how many missiles our fighter has at any given time. The super()
method calls the wrapping method, initialize
in this case, of the parent class. This is needed for inheritance to be maintained because when a method is redefined in a child class it completely replaces the inherited method. A single call to super()
fixes this issue. With polymorphism it doesn't matter if we are using a ship or a fighter or that they are implemented differently we just tell them to move or accelerate and their classes take care of all of the details and that is the power of polymorphism and OOP.
Conclusion
That's it. Well not really. OOP is a large field with many nuances for you to explore, but hopefully this little tutorial has helped you better understand OOP and Ruby. Take it easy and happy coding.