This is just a stub chapter. I have an example project half-started to demonstrate the principles of object-oriented design but haven't gotten time to it yet. So far, I've only written a few incomplete explanatory sections.
The design paradigm known as "object-oriented programming" (OOP) is so at the core of Ruby (and modern programming) that it's kind of glaring that I've gotten this far in the book without really explaining to it.
In a nutshell, object-oriented programming sees the world as data, modeled in code by "objects." In OOP, the programmer focuses on the content of that object and how that object behaves (i.e. methods).
So it's worth becoming acquainted with OOP because it is a design pattern especially suited for programming with data. The practical benefit is that it can vastly reduce the amount of code you have to write and the number of errors of inconsistency to debug.
A Cinematic Metaphor
OOP is such an abstract concept that the topic is notorious for spawning off a plentitude of real-life metaphors to explain it, involving ducks, dictators, cars.
To introduce you slowly to OOP, I will use the movie industry as a metaphor. It's about as bad as all of the other metaphors ever used to explain OOP. But I plan to implement it in code for the latter part of this chapter. And it gives me an excuse to decorate the page with movie images.
This section will include the basic code to set up classes in Ruby. But don't worry about memorizing it now.
Actors and Actresses (Classes)
We'll base our first class on the data and behavior of an "actor."
What is an actor? A human being, for starters. Like every other human being, actors have the attributes of name, age, sex, birthdate, and birthplace.
And of course, there are characteristics unique to the field of acting. Actors have a filmography, for example.
In Ruby code, these attributes of an Actor might be implemented like this:
class Actor
attr_accessor :name, :age, :sex, :birth_date, :birthplace, :filmography
end
Just a quick segue into the technical details: Writing a class like this is not so much different than defining a method. And now that we've defined it, we can execute this code:
an_actor = Actor.new
an_actor.name = "Paul Newman"
an_actor.age = 83
an_actor.filmography = ["Cool Hand Luke", "Butch Cassidy and the Sundance Kid"]
puts an_actor.inspect
#=> #<Actor:0x100189ec0 @filmography=["Cool Hand Luke", "Butch Cassidy and the Sundance Kid"], @age=83, @name="Paul Newman">
Actors are objects (Inheritance)
The first few lines of that snippet should make some abstract sense. We've made a new Actor object and have set a few of the attributes that we had decided that all actors have.
But where did that an_actor.inspect method come from? We didn't mention anything called inspect anywhere in the Actor class definition.
Well, the Actor class inherited from the Object class. By default, every class you create in Ruby is a subclass of Object. Think of "subclass" as a "child class": Actor – and every other class in Ruby – being the child of Object inherits behavior from its parent. So, there are two consequences of this:
- Everything we use in Ruby (besides special keywords such as def, if, end) is an Object.
- Everything that Object can do can be done by every Ruby object, whether they be instances of Float, String, Array, or Actor.
Actresses
OK, enough technical talk, back to the real-life metaphor. Let's think about how to design the Actress class. Well, this is easy:
class Actress
attr_accessor :name, :age, :sex, :birth_date, :birthplace, :filmography
end
It's always nice when you can copy-and-paste code. But that doesn't seem like the programming way, does it? Programming is supposed to reduce repetition. And yet here we have two classes that are exactly the same. And when we add more functionality to the Actor class, we have to copy and paste it over to the Actress class.
What OOP concept did we just learn about? Inheritance. If Actress inherits from Actor, then it automatically gets all the functionality of Actor. That's so brilliant let's just implement it right now:
class Actress < Actor
end
And that's it. Testing it out gets us this:
an_actress = Actress.new
an_actress.name = "Catherine Keener"
an_actress.age = 52
an_actress.filmography = ["Being John Malkovich", "Capote"]
puts an_actress.inspect
#=> #<Actress:0x100188b60 @age="52", @name="Catherine Keener", @filmography=["Being John Malkovich", "Capote"]>
Look at that. We've created two whole, working classes with such minimal effort and code. That is the power of object-oriented programming. Let's look again at the Actress definition to appreciate its simplicity:
class Actress < Actor
end
We already know that all classes are children of the Object class. That "less than" sign, <, is how we signify that Actress inherits its behavior from the Actor class.
Not that we're saying actresses are children of actors. It's more accurate to say that actresses are sub...actors. Er...make that...subordinate to...ummm...Well, guess this is just the limit of trying to use a real-life metaphor to explain programming, right?
Wrong. Not only is this a nonsensical way to describe the logical relationship between actors and actresses, it's just poor design.
What's the characteristic that distinguishes between actors and actresses? We already have it implemented in code: sex
Instead of creating two classes in which the only difference is the value of the sex attribute, just use a single class definition. If in a program, you want to select the actresses from a collection of Actor objects, use a conditional filter:
actresses = the_cast_of_being_john_malkovich.select{|a| a.sex=='F'}
puts actresses.map{|a| a.name}.join(', ')
#=> Cameron Diaz, Catherine Keener
So we'll just have an Actor class, with the assumption that "actor" is an acceptable unisex term for both male and females except for during award shows.
Death (Instance methods)
Speaking of filtering between male and female actors, we might want to be able to choose from the living and the dead in a collection. That means we need to add death_date and is_alive attributes to the Actor class. The latter attribute is either true or false.
So we do that. And after entering in 20 names of dead actors, we realize: wait a darn minute. Every actor who has something for death_date seems to also have their is_alive attribute set to false.
That seems like a redundancy. Let's take out is_alive since that provides no useful information other than that a death_date exists for an actor. So to filter for living actors, we just need to do something like:
actors.select{ |a| a.death_date.nil?}
That works fine. But it's kind of ugly, code-wise, to do a double reference check: get the actor's death_date and check if it is nil?
There isn't an easier way to define life and death in our Actor class. But we can at least hide the implementation details from anyone using our class (and save them a little typing) by adding an instance method:
class Actor
def alive?
death_date.nil?
end
end
# to find living actors
actors.select{ |a| a.alive?}
That's a little nicer looking. And easier to type.
Aging (Abstraction)
While we're on the topic of when actors died, it also seems that the age attribute is directly related to birth and death dates. Seems kind of redundant to enter in all three attributes.
More importantly, there's the issue that while entering in actor data, there's one whose birthday is tomorrow. Do I enter her age in now, knowing that I'd have to update it tomorrow? And what about all the other actors with upcoming birthdays?
Seems like age is a prime example of an attribute that should be a method. We can even use the alive? method that we just defined:
require 'time'
class Actor
def age
if alive?
a = Time.now - Time.parse(birth_date)
else
a = Time.parse(death_date) - Time.parse(birth_date)
end
return (a / 60 / 60 / 24 / 365).to_i
end
end
an_actor = Actor.new
an_actor.name = "Paul Newman"
an_actor.birth_date = "1/26/1925"
an_actor.death_date = "12/26/2008"
puts an_actor.age
#=> 83
Using the ternary operator, we can clean age up:
def age
((alive? ? Time.now : Time.parse(death_date)) - Time.parse(birth_date)).to_i / 60 / 60 / 24 / 365
end
OK, this isn't pretty, but it gets the job done. And for this case in particular, that's all that matters. The end user – anyone who has to access the data using the classes you've created – doesn't care how age is calculated. The end user just wants it accurate, whether you derive it from birth and death dates, or whether you choose to constantly update the ages every day.
Creation (class methods)
We've been using plenty of class methods without really thinking about the concept. For example, File.open and RestClient.get
x = File.open("something.txt", "w")
puts x.class
#=> File
x.write("The write method is an *instance* method.")
x.close
It makes sense that the methods write and close will belong to an actual file. But how could open belong to a file? Every file's existence kind of depends on it being opened first, right?
In the Actor class, let's define a class method named load_with_info. It will take in three arguments for name and birth_date and death_date (which will be optional) and return a new Actor instance (Actor.new is also a class method) with those attributes set:
class Actor
def Actor.load_with_info(n, bdate, ddate=nil)
a = Actor.new
a.name = n
a.birth_date = bdate
a.death_date = ddate
return a
end
end
a = Actor.load_with_info("Rosalind Russell", "1907-06-04", "1976-11-28")
puts a.name
#=> Rosalind Russell
puts a.age
#=> 69
puts a.alive?
#=> false
And to drive home the point, since Actor.load_with_info is a class method, this will raise an error:
b = Actor.new
b.load_with_info("Cary Grant", "1904-01-18", "1986-11-29")
#=> NoMethodError: undefined method ‘load_with_info’ for #<Actor:0x10030bd98>
Adding Writers (Refactoring)
There's of course more that we can do with the Actor class (I plan to eventually complete this cinematic metaphor with actual project code). But I want to cover just one more example of how OOP can save us code-writing time.
Let's say we wanted to add a Writer class. This class shares a lot of attributes with Actor, yet you can't really say that it inherits from Actor.
Well, all of the shared attributes happen to be things that humans generally have: birthdays, names and sex. So, let's create a new class called Person from which Actor and Writer inherit from. Here's all the code for defining the classes so far (generally, though, you'd separate each class into its own file):
require 'time'
class Person
attr_accessor :name, :age, :sex, :birth_date, :birth_place, :death_date
## class methods
def Person.load_with_info(n, bdate, ddate=nil)
a = Person.new
a.name = n
a.birth_date = bdate
a.death_date = ddate
return a
end
## instance methods
def age
((alive? ? Time.now : Time.parse(death_date)) - Time.parse(birth_date)).to_i / 60 / 60 / 24 / 365
end
def alive?
death_date.nil?
end
end
class Actor < Person
attr_accessor :filmography
end
class Writer < Person
attr_accessor :published_works
end
Using require
Let's say the file with the class definitions is called people_classes_0.rb. To use it in an actual program, you can include it with require:
require 'people_classes_0.rb'
w = Writer.load_with_info("F. Scott Fitzgerald", "September 24, 1896", "December 21, 1940")
puts "#{w.name} lived to be #{w.age}"
#=> F. Scott Fitzgerald lived to be 44
Actor? Writer? Who cares?
Here's a practical effect of the basic OO-design we've done so far. Let's say we have an array full of actors and writers and want to just list the ones who have passed away:
array_of_folks = []
array_of_folks << Actor.load_with_info("Meryl Streep", "1949-06-22")
array_of_folks << Actor.load_with_info("Paul Newman", "1925-01-26", "2008-12-26")
array_of_folks << Writer.load_with_info("Jane Austen", "1775-12-16", "1817-07-18")
array_of_folks << Actor.load_with_info("Cary Grant", "1904-01-18", "1986-11-29")
array_of_folks << Actor.load_with_info("Kate Winslet", "1975-10-05")
array_of_folks << Writer.load_with_info("F. Scott Fitzgerald", "September 24, 1896", "December 21, 1940")
array_of_folks << Actor.load_with_info("Nicholas Cage", "1964-01-07")
puts "The following people have passed away: " + array_of_folks.select{|a| !a.alive?}.map{|a| "#{a.name}, #{a.age}"}.join('; ')
The output:
The following people have passed away: Paul Newman, 83; Jane Austen, 41; Cary Grant, 82; F. Scott Fitzgerald, 44
Notice how the select method happily iterates through each Person, not caring whether he/she is an Actor or a Writer. All that matters is that the object have a name, age and alive? methods.
This is part of the joy joy that is object-oriented design. With a little planning and organization, you have code that is flexible, clean, and short.
This was just an introduction into OOP through example. The next section of this chapter will formally introduce the concepts. And the final chapter will implement a project using data classes based on the movie industry. So stay tuned.
Object-oriented syntax
I wrote some of this awhile ago and don't remember how much of it is even useful or accurate. Will revise later, so read with caution.
Classes and Instances
I've sometimes used the word "data type" to describe the String, Fixnum, Array, and all the other classes we've used so far. A class is the blueprint that describes the properties and behavior of a type of data.
An instance of a class is an actual object of that class. So, "dog" is an instance of the String class.
Inside the class definition of String we see a list of methods (under the subhed Public Instance methods) that instances of String can call, such as: "dog".upcase.
There are also a couple of class methods. These are methods that only String can call, such as new:
my_string = String.new("hello doggy")
puts my_string #=> hello doggy
The new method, which creates a new String instance, only makes sense to be done at the class level. Why would "a doggy" need the ability to create new strings itself? Conversely, it doesn't make sense for the String class being able to upcase itself. It's not an actual string, remember, it's a description of strings.
Note that we've never made a call to String.new before, just because new strings are simple enough to instantiate by doing something like: str = "hello world"
See the Wikipedia definition for classes.
Inheritance
Classes have a sort of familial relationship to each other. The Object class is essentially the common ancestor class of every other Ruby class. In practical terms, this means that all of its descendants – whether they be strings, numbers, arrays, etc. – have access to Object's public methods (more about public and private later).
The Object class has the methods clone, eql? and equal?, which, respectively, makes a new copy of the receiver object, tests if the receiver has the same value as another object, and tests if the receiver has the same identity (regardless of value) of another object. All Ruby objects inherit these methods:
sheep = "Dolly"
sheep2 = sheep.clone
puts sheep2
#=> Dolly
puts sheep.eql? sheep2
#=> true
puts sheep.equal? sheep2
#=> true
Lets move up the classes family tree and look at the two number types that we're familiar with: Float and Fixnum. If you use the ancestors method (a basic class method), you can see their respective family trees:
puts Fixnum.ancestors.join(', ')
#=> Fixnum, Integer, Precision, Numeric, Comparable, Object, Kernel
puts Float.ancestors.join(', ')
#=> Float, Precision, Numeric, Comparable, Object, Kernel
So Numeric is their closest common ancestor (Precision is something called a Module, which is a collection of methods and constants that a class can include. But modules are not inherited from; I won't cover this distinction in detail.)
Instances of the Numeric class – which includes instances of the Numeric subclasses Float and Fixnum have methods such as + and abs. Furthermore, you can add
Attributes (to-do)
Methods (to-do)
Instances and Classes (to-do)
The self (to-do)
Being super (to-do)
Privacy (to-do)
Further reading
Fleshing out the OOP with movies
This section will contain a walkthrough for a data-scraping project. The classes we built at the beginning of the chapter were pretty bare and not terribly good examples of OOP design. Also, there was no functionality defined in terms of how to collect such data (there's no way I'm typing in a hundred actors biographical info by hand!). Coming soon to a programming book near you...