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.
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.
@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
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
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
@speed are hidden as they do not need to be known outside the class since they are just used by the methods
move. The methods
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
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
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 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.
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.
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.