Objects, Ruby Classes and Mixins are the topics today. Ruby is a dynamic language with a focus on simplicity and productivity. Object Oriented programming in Ruby is at once elegant and simple. Ready for more power and flexibility? Let’s get started…
Most academics approach object oriented discussions with Tonka toys and furry kittens. Well, academia can take a backseat. From here on out, dump the Tonka toys and put the kittens to bed - we’re talking Lightsabers and Jedi baby!
An object is an instance of a class. Each instance having its own unique attributes and state.
Here, the anakin object is an instance of the Jedi class. Apparently, Anakin is having trouble with some galactic fraktards and fires up his Ruby lightsaber using the lightsaber method. Then Anakin proceeds to bring the hurt using a lightsaber combat form called Juyo.
Using the Jedi class is deceivingly simple. That simplicity is possible because of a well designed domain model (collection of ruby classes that model a particular system).
In this article, I’ll take you behind the scenes and show you the techniques you need to write clean, maintainable and awesome Ruby code.
In Ruby, an class (object) encapsulates a group of attributes (state) and methods (operations). A developer manipulates an object’s state (instance variables) only through class accessors and methods.
Here’s how a Jedi class might look:
And the Padawan class might look like this:
Padawan would be derived from maybe a Sentient class. But for simplicity I’m including in the Padawan class attributes you might have seen in a class like Sentient. For example, the @name attribute.
The initialized method is Ruby’s constructor method for class implementations. It’s called whenever a new instance of a class is created. For example, here a new instance of the Jedi class is called. The result is the anakin object:
This example illustrates a beautiful feature of Ruby - Flexible Initialization. This means you have the option to pass a parameter or not:
You can choose not to pass a parameter. In that case, creating an instance of the Jedi object might look like this:
And the default value of the @name attribute would be Unknown.
You’ve noticed by now that when we try to look at an object directly, we are shown something cryptic like <#Jedi:0x101237498 @name=”Anakin”>. This is just a default behavior, and we are free to change it.
All we need to do is add a method named inspect. You should return a string that describes the object in some meaningful way - including the states of some or all of its instance attributes.
Here, we return the value of the @name attribute on the Padawan class. In this instance of the Jedi object, the value of the @name attribute is Anakin. So I setup the inspect method to return Force-Sensitive: Anakin.
Run irb from Terminal. Then load ‘dojo.rb’. And type Anakin. Type Exit to leave irb.
A class may have many attributes. It really all depends on the domain you’re trying to model. Accessing those attributes directly is not a good practice. Instead, you should create accessors to read and write to those attributes.
To accomplish this in some languages is a pain. You would have to define a getter method and setter method for each class attribute. Imagine having 10 attributes. That’s 20 different accessors you would have to write - in addition to writing your attributes!
Ruby has a more elegant solution - the attr_accessor shortcut:
This gets you the following Ruby class methods for FREE:
Awesome! This is at once elegant and beautiful. You can add as much attribute accessors as you like: attr_accessor :name, :weight, :etc.
You’re not going to always want a getter and setter. Sometimes you might only need one. You can do that in Ruby:
You get the picture!
Methods are how we interact with our Ruby classes. Methods allow us to encapsulate activities specific to the particular class:
Methods sometimes perform work on a Ruby classes’ attributes. Sometimes a method just performs work specific to the class. In our Jedi class, we have a duel method. This method is used to convey some Jedi axiom just before battle begins.
Like an initialize constructor, a method can take parameters. A Ruby method can even have a default parameter. So our duel method could have looked like:
Coming from static languages, Ruby’s minimalistic approach to object oriented programming is at once refreshing and inspiring.
Still with me? Great! Let’s move on to more advanced topics.
Eventually, you’ll reach a situation where different Ruby classes have the same attribute and perform the same basic actions.
We know that a Jedi evolved from a Padawan. Therefore, a Jedi should posses the skills of a Padawan with the further developed skills of a Jedi Knight.
In Ruby, such relationships can be expressed using inheritance. Here, the Padawan class is called the parent class:
In this way we can reuse the Padawan class for other force-sensitives that we wouldn’t consider Jedi. For example, we could have a Sith class:
Darth Vader was once a Padawan until he turned to the Dark Side and became a Sith Lord. As such, you would expect him to have the knowledge and training of a Padawan with the learning of a Dark Knight.
There will be cases where certain methods of the parent class will need to be implemented differently in the inheriting class. For example:
Here, the inspect method from the Sith class will be used instead of the inspect method from the Padawan class. Object oriented languages (like Ruby) that facilitate this type of behavior are said to be polymorphic. Therefore, the above is an example of Polymorphism.
Rather than exhaustively define every characteristic of every new class, we only need to redefine the differences between the parent class and the child class.
Does Ruby support multiple inheritance?
No. Ruby was designed with single inheritance. This was on purpose. Single inheritance encourages you to develop a deeper understanding of your domain model.
That said, there are times when a domain model could benefit from sharing methods that do not require a full blown class. That’s where Ruby Mixins come in.
Mixins are Ruby modules. Modules are a collection of methods. You cannot create an instance of a module. Therefore, modules do not maintain state.
After requiring a Ruby module, you would then include that module in your Ruby class. This is called a Mixin:
Here is the Mixin Lightsaber module:
So when I instantiate an instance of the Jedi class, I get access to the Lightsaber module - pure Mixin love:
Modules are for sharing behavior (methods), while classes are for modeling relationships between objects. Ruby classes can Mixin a module and receive all its methods for free.
Active Record - Base Class or Mixin?
We might think Active Record should have been included rather than extended by a subclass. You can use your own parent class at that point right?
So should Active Record have been implemented as a Mixin instead of a Base Class? Hell No!
Saying it should is a half-truth at best. What never follows such statements is a disclosure that you will spend exorbitant amounts of time mapping your class attributes to a database DSL (domain specific language).
This behavior really adds up to your detriment when dealing with larger domain models. But this is never said. It’s that little gotcha that, not surprisingly, never gets mentioned.
I believe for the best speed, simplicity and maintainability, Active Record is best implemented as a base class that you inherit with a single line.
Compare that to the many lines you will spend to map your class attributes to database types. It really is a no-brainer.
I could stop here, but I believe you’ll be able to make better judgments when you have a fuller picture of the issue. So I’m going to tell you when implementing Active Record as a mixin makes sense.
It makes sense when you’re dealing with legacy code that already has a base class coupled with an inefficient, custom data access layer. Then I recommend using a Ruby ORM like Datamapper.
You will spend a lot of time wiring up your domain models, but you’ll get a mature and flexible data layer - just the type of sprucing up any legacy app could use.