Object-Oriented Programming: Encapsulation and Inheritance
- 03 January 2017
- Ruby Design
- Object-Oriented Programming: Encapsulation and Inheritance
Ruby - it's an object-oriented language. If we want to understand ideas that Matz put into Ruby - we should understand basics of object-oriented programming (OOP). In this post I'll cover encapsulation and inheritance. I'll devote separate post for polymorphism.
Encapsulation
Encapsulation is an Object Oriented Programming concept that binds together the data and functions that manipulate the data, and that keeps both safe from outside interference and misuse. Data encapsulation led to the important OOP concept of data hiding.
Object - it's just a state and behavior. Each class has interface - set of public methods
Encapsulation tells us that we should know just an interface of class to be able to use it. We don't have to know anything about internals of class to be able to use it.
Let's check following real-world example. The best example of encapsulation could be a calculator. We understand its interface from first sight and we don't have to know how it works inside. We know that we can press 2+2 then = and see the result on display.
In Ruby we can easily check interface of class - we should look for its public methods. Then we can find method that fits for our needs and check which arguments to pass to get result.
In statically typed languages you also have to define a type of arguments which method has as well as type for returning value.
Ruby it's a dynamically typed language, so all we can learn about method from first sights it's a name and params it accepts.
I must admit that naming for public methods and params is so important because it serves as a some sort of documentation.
Also, we should remember that class should have as less public methods as possible. It will allow to simplify using of that class and protect data from undesired changes from outer world. I have a blogpost about public, private and protected methods in Ruby. It allows you to control access level to methods. Also having less methods will help you not to break Single Responsibility rule.
Good public interface will allow you to change inner implementation of method, but keep the same interface for outer world. It's especially important if you develop open-sourced gem or application.
Inheritance
Inheritance (OOP) is when an object or class is based on another object or class, using the same implementation specifying implementation to maintain the same behavior.
Let's consider this example:
class Human
def walk
"Walking"
end
def breathe
"Breathing"
end
end
class Man < Human
end
class Woman < Human
end
adam = Man.new
adam.breathe # => "Breathing"
eve = Woman.new
eve.breathe # => "Breathing"
Class Human
is a parent class for Man
and Woman
. Because Man
and Woman
are inherited from Human
, they have all methods that Human
has: walk
and breathe
.
There is a easy way to define if you should use inheritance or composition. It's called is-a vs has-a
. We use inheritance when you can describe relation between parent and child class as is-a
, for example:
- Man - is a Human
- Woman - is a Human
This statement we expressed in code as:
Man < Human
Woman < Human
Let's see another example. Superman - it's still human, but it has (has-a
) superpower. In this case we can use composition:
module Superpower
def fly
"Flying!"
end
end
class Superman < Human
include Superpower
end
superman = Superman.new
superman.breathe # => Breathing
superman.fly # => Flying!
Superman is still human, he walks (walk
) and breathes (breathe
), but now he has (has-a
) an ability to fly. That's because we included Superpower
module into class.
Another example of inheritance is ApplicationController
in Ruby On Rails applications. Usually developers create one controller and inherit other controllers from it. It allows to pull shared methods into parent class and don't copy/paste those methods into child controllers. This relation also fits perfectly to is-a
relations. For example UsersController
it's a controller of our application - ApplicationController
.
Couple more words about inheritance. In some languages multiple inheritance is possible. One class can be inherited from couple more parent classes. Multiple inheritance isn't possible in Ruby. We can inherit class just from one parent class.
Imho, it was right decision by Matz, because in many cases multiple inheritance it's a bad idea which reveals problem with proper design of application. If we want to extend class by some methods, we always can use mixins and include module with those methods.
Speaking about inheritance, we have to discuss method super
, which allows to call method from parent inside child class.
Example:
class Man
def power
10
end
end
class Superman < Man
def power
super * 1.5
end
end
Superman.new.power # => 15
Superman is inherited from Man
class. Let's imagine that for regular man power is equals to 10 points. We want Superman to have x1.5 power of regular man.
In power
method for Superman we want to get value of power
from Man
class. In this case we can use method super
, which calls method in parent class. We have this chain of calls:
- we called method
power
in object of classSuperman
, which inherited fromMan
. - inside method
power
, we called methodsuper
which called following method in parent class (power
forMan
returns 10) - multiplied returned value by 1.5
Here is one more example:
class Man
attr_reader :name
def initialize(name)
@name = name
end
end
class Hero < Man
attr_reader :hero_name
def initialize(real_name, hero_name)
@hero_name = hero_name
super(real_name)
end
end
superman = Hero.new("Clark", "Superman")
superman.name # => Clark
superman.hero_name # => Superman
Class Man
accepts one argument - name
. Also it has attr_reader
, which makes this argument accessible . For Hero
class we also have hero's name param.
So when we create new instance we pass two params. Also we should call initialize
for parent class Man
. If we don't do that - then @name
variable will not be set, and superman.name
will return nil
. As we know, to call method in parent class we use super
.
In interviews people like to ask this tricky question: what's the difference between super
and super()
?
Example:
class Man
def initialize(name)
puts name.inspect
end
end
class Hero < Man
def initialize(name)
super
end
end
Hero.new("Clark") # => Clark
If we call super
in child-class and don't pass any params - it will use params from current method call and will pass it into parent class. In our case super
call is equal to super(name)
. By result of execution we see that name
was passed to Man#initialize
.
If we want to call parent method, but don't want to pass all params from child method - we can use this call super()
. This method will call parent method, but will not pass any params into that.
class Man
def foo
puts "I don't need params"
end
end
class Hero < Man
def foo(param)
puts "I need a param"
super()
end
end
Hero.new.foo("Param")
# => I need a param
# => I don't need params
If we call super
without ()
, it will try to pass param
into method in parent class which will cause an error: foo': wrong number of arguments (1 for 0) (ArgumentError)
That's it on encapsulation and inheritance in Ruby. In next article I'll cover polymorphism.
PS: Thanks everyone for comments and new ideas for blogposts. I really appreciate any feedback. I'm really trying to provide value for you so let me know if you have something you want me to cover. Sometimes in discussion you get much more insights than in blogpost itself. Stay tuned!