Object Oriented Ruby
I have been seeing a lot influence in ruby from Functional programming (short: FP), and not as such in ruby itself but in code written in ruby. Developers try new things, they are fascinated by other languages, how they solve problems. They try to change ruby into functional language, they loose sight of how problems can be solved in the Object Oriented Programming (short: OOP). Let’s do a refresher of some most important aspects of OOP.
Encapsulation
Technical details aside encapsulation is the most important concept in OOP. At the roots OOP was meant to be a simplified representation of real life things/objects/people, think a car, an animal, a plant. For an example we will take an calculator, it’s a fairly complex thing. An calculator has a display, some buttons and some magic insights. The buttons are the input for calculator, the display is it’s output. We do not know what happens inside. Well some of us do, the point is only those interested in creating calculators know how to build one. For anyone else calculator has an interface with input and output allowing anyone knowing it’s language interaction with the calculator. This is encapsulation, most calculator users will use it’s interface, they do not need to know what happens inside. Is it an old kind of machine with gears, is it a computer, or maybe a new invention protein based? As long as it works as expected – nobody cares.
Mixins
When looking around we see a lot of repeating patterns. As software developers we do not want to repeat code, we write reusable methods, we put them in modules to group them. Mixins is where writing functional style code is important, we do not mutate any state, we take inputs and give outputs. I will illustrate on few examples what to avoid and how to write mixins code.
Example 1.
module ConfigReader def config @config ||= File.readlines(@config_file_name) end end
This code breaks encapsulation, it assumes there is @config_file_name
, it puts requirements on people that will use it, it does not define interface, it’s not functional. How would that be bad? Any mixin author could use @config
, maybe there is Options
module that also defines @config
– you see how that could be bad? Authors of both modules would need to know of each other and would need to make sure they variables do not overlap.
Example 2.
module ConfigReader def read_config(file_name) File.readlines(file_name) end end
In this example we do not assume existence of any variables, we do not define any variables. What we have here is a pure functional interface, no side effects (for the object).
If you notice the need to reference/write variables in mixins you should reconsider, you might need composition or inheritance.
Example 3.
module Age attr_accessor :birth_date def age (Date.today - @birth_date)/365 end end
It looks pretty contained, right? The thing is – it touches again on instance variables. This is definitively where composition would come in handy.
Composition
When we think of the world we know it consists of multiple objects, those objects can be often decomposed further into smaller objects down to atoms. In OOP composition helps us move between high level overview and implementation details, but as always it can be understood wrong, it’s very important to not forget about encapsulation. Similarly to mixins I will show some examples here too:
Example 4.
class Person class Barber attr_reader :person def initialize(person) @person = person end def dye_hair(color) person.name = color end end attr_accessor :name, :hair_color def initialize(name, hair_color) @name, @hair_color = name, hair_color end def barber Barber.new(self) end end michal = Person.new("Michal", "brown") michal.name => "Michal" michal.barber.dye_hair("blue") michal.name => "blue"
In this example we show the importance of encapsulation in composition, we can not allow anybody to change everything inside of a class. The biggest mistake in this example is passing the instance to the other object with assumption it will operate on the passed instance. If we pass the parent it should only be for reading. How this could have been done differently?
Example 5.
class Hair < Struct.new(:color) end class Barber def dye_hair(hair, color) hair.color = "#{hair.color} #{color}" end end class Person attr_reader :name, :hair private :hair def initialize(name, hair) @name, @hair = name, Hair.new(hair) end def dye_hair(barer, color) barer.dye_hair(hair, color) end def hair_color hair.color end end barber = Barber.new michal = Person.new("Michal", "brown") michal.hair_color => "brown" michal.dye_hair(barber, "blue") michal.hair_color => "brown blue"
With this new example Barber
can not change anything else but Hair
. In composition it’s important to not break the encapsulation, using other objects, blocks or return values we can be much safer, we will not manipulate objects that are not our concern.
Inheritance
In OOP inheritance is where we extend the functionality in each inherited(child) class. Let a example show what inheritance gives us:
Example 6.
class Person attr_reader :strength def initialize(name, strength) @name, @strength = name, strength end # @returns NUMBER overly simplified distance how far it's thrown def throw(weight) strength / weight end end class SuperHuman < Person def initialize(name, strength) super(name, strength * 1_000_000) end # @returns NUMBER overly simplified distance flown in given time def fly(time) strength * time end end
In above example we see two traits of inheritance:
- We have access to the variables / methods of the class we are inheriting.
- We can add new functionality extending capabilities of the inherited class.
So inheritance crosses the encapsulation boundary … does it? Not really, inheritance happens before we instantiate the object. Think of inheritance like of onions or ogres ;) – it has layers, each child class builds on top of the parent class, it adds extra functions, it improves previous implementation, it does not reach between objects to change variables.
When thinking of using inheritance you need to distinguish between “is a kind of” and “has the same methods”. Inheritance only works when it’s “kind of” relation, in other cases consider mixins or composition depending on the use case.
Summary
It’s hard to pick proper way of structuring our code. With the above examples I tried to show where each of the described methods fits in. For me the biggest differentiatior is encapsulation, is the object maintaining it’s own state or let’s others do that?
Writing OOP code does not mean that you need to forget about FP, actually it’s reverse, it all connects together. OOP does not require you to change state, you can actually write most of your methods in functional way, making sure the code has strictly defined interface and uses only it’s input and produces output. It’s more mater of your codding culture, the language can not restrict or force you to write good code, it falls on the developer to structure code. This is why it’s good to go out of your comfort zone and learn other languages, why you need to experiment. But before you go out on an adventure with other concepts make sure you know your own backyard.
Conference talk / follow ups
I’m working on extending this topic in further posts and also started creating conference talk from it, if you are interested in hosting the talk contact me directly.
I’m also searching for new Ruby job, I had a great time with StackBuilders but the company is focusing on Haskell and I want to continue my work in Ruby world. So if you heard of somebody in need of good Ruby developer let me know.