A Beginner’s Intro to Object Oriented Thinking: When a Bicycle Is Not Just a Bicycle

The goal of programming — one of the goals, at least — is to create tools that help solve problems rooted in real life. Need to be entertained? Program an entertainment tool (but call it a “game” so people don’t think you’re a weirdo). Need to organize your files? Program a file organizer tool. Need a way to access your favorite movies from pretty much anywhere? Program an application that stores digital copies of thousands of movies on a server, displays them as thumbnails, lets you browse them, search them, mark them to be watched later, and, most importantly, display them on your computer screen for your viewing pleasure. You get the picture. The foundational purpose of programming is to be used to develop tools that make life easier — and more convenient — to navigate. And, as such, the ways in which programmers think about problems should be rooted in the real world, too.

This thinking comes in handy when working with object orientation in which responsibility is encapsulated in classes and methods are used to modify and utilize the information within those classes and to connect classes to one another so that they can utilize the information that each one contains. When deciding what should be encapsulated in a class, there are several questions that a programmer should ask his/herself, but for the most part, those questions can be boiled down to one basic inquiry: “Does the organization (and behaviors/knowledge) of classes within program X reflect the way those things interact/behave in real life?”

#amiright?

In Sandi Metz’s Practical Object-Oriented Design In Ruby, she tackles this concept in chapter two by using the simple example of a bicycle, its gears, and its wheels. When determining how to structure classes for a program that is softwarifying the interactions one might have with a bicycle, the “real life” question is an easy one to apply, though to a non-programmer, it could seem a little nit-picky, perhaps. To the layman, a bicycle is, well, just a bicycle. Sure, it has gears and handlebars and pedals, but it’s a bicycle. Sandi Metz teaches that to the programmer, however, a bicycle is not just a bicycle, or at least, it shouldn’t be. To the programmer, a bicycle is an object that is actually a collection of other objects that exist independently of one another but that, when connected properly, operate as a transportation tool that society refers to as a single ‘bicycle’ object.

“‘Please Mr. Gear, what is your tire (size)?’ is just downright ridiculous.”
— Sandi Metz

Is a bicycle a bicycle without gears? No. But, could a gear be a gear without a bicycle? Absolutely. Is a bicycle a bicycle without wheels? No, again. 0-for-2. But, can a wheel be a wheel without a bicycle? You betcha. This sort of reasoning is how a programmer determines what gets its own class vs what simply exists within another. But, this is only the first step in the process of object oriented design…

A programmer must also determine what each class should do, or better put, what the responsibility(s) of each class should be and what it should know and what and how it should communicate with other classes (and with which ones). Consider the VERY basic framework of something like Instagram (as I was asked to do in a recent review assignment as a student at Flatiron School). Instagram, in a nutshell, has users and photos and comments. (Yes, it has lots of other things, but they aren’t necessarily relevant here.) The classes in this skeleton-model of an Instagram-like program might look like this:

class User
attr_accessor :name

ALL_USERS = []
  def initialize(name)
@name = name
ALL_USERS << self
end
end
class Photo
end
class Comment
end

Each of these entities, users, photos, and comments, exists independently of the others in real life — even if they are very interconnected — and, thus, should exist independently of one another within software, as well.

Just like in real life, the User here has a name, and for practical purposes, the class also includes a way to store all of the users. But, what about the Photo and Comment classes? What should they have when they are created? How should they be created? Can a comment create itself? A photo? It seems that this is the perfect time to apply the principles of the “real life” question in order to get some answers…

Does a photo exist as an independent entity in real life? Yes, which means that Photo as a standalone class is the right structural decision. Does a photo create itself? No? Who creates it? A user does, of course, when s/he adds it to their profile. So, perhaps there should be an #add_photo method in the User class. But, should it be an instance method or a class method? Well, who creates a photo, an individual user or the collective of users? The answer is clearly that an individual user creates photos when s/he adds them to their profile, so this should be an instance method because it is the responsibility of a single User instance. It could look like this:

class User
attr_accessor :name

ALL_USERS = []
  def initialize(name)
@name = name
ALL_USERS << self
end
  def add_photo(file)
new_photo = Photo.new(file, self)
end
end
class Photo
end
class Comment
end

Creating this #add_photo method also has a perhaps-unexpected side effect: it starts to answer some other design questions that are lingering within this hypothetical scenario like, for instance, what an individual photo should consist of when it is created. In real life, a photo is a file and is associated with a user, so why not do that in the code so that it mirrors real life? The hypothetical #add_photo instance method for the User class already includes this information as arguments that are passed into the new instance of Photo — because it just made logical sense to do so — but it has to be added to the Photo class, as well…

class User
attr_accessor :name

ALL_USERS = []
  def initialize(name)
@name = name
ALL_USERS << self
end
  def add_photo(file)
Photo.new(file, self)
end
end
class Photo
attr_reader :file, :user
  ALL_PHOTOS = []
  def initialize(file, user)
@file = file
@user = user
ALL_PHOTOS << self
end
end
class Comment
end

Now, when a photo is added to a user’s profile, it has a file, it is associated with a user, and it is added to a list of all the other photos. Good. But, before moving on, there is one other thing to note in the spirit of thinking about the “real life” question and its principles: photos should not be able to modify themselves because modifying themselves is not something that photos can do in real life. They should know who their user is and what their file is, but they should not have any control over changing that information. For that reason, an instantiated photo object is equipped only with reader methods for its attributes, signified by attr_reader, not reader and writer methods like the User class’s attr_accessor, which gives instances of that class both.

As for the hitherto-neglected Comments class, it becomes quite easy to design now that prior questions within other classes have been answered. Using the principles within the “real life” question as a guide, saying that a user creates a comment and associates that comment with a particular photo but that a comment should not be able to create or modify itself seems like a safe truth to run with. With that in mind, the User class probably needs a #make_comment method:

class User
attr_accessor :name

ALL_USERS = []
  def initialize(name)
@name = name
ALL_USERS << self
end
  def add_photo(file)
Photo.new(file, self)
end
  def make_comment(message, photo)
Comment.new(message, photo, self)
end
end
class Photo
attr_reader :file, :user
  ALL_PHOTOS = []
  def initialize(file, user)
@file = file
@user = user
ALL_PHOTOS << self
end
end
class Comment

end

Now, just like with the Photos example, having the #make_comment method in the User class makes filling out the basic info of what a comment consists of when it is created rather easy:

class User
attr_accessor :name

ALL_USERS = []
  def initialize(name)
@name = name
ALL_USERS << self
end
  def add_photo(file)
Photo.new(file, self)
end
  def make_comment(message, photo)
Comment.new(message, photo, self)
end
end
class Photo
attr_reader :file, :user
  ALL_PHOTOS = []
  def initialize(file, user)
@file = file
@user = user
ALL_PHOTOS << self
end
end
class Comment
attr_reader :message, :photo, :user
  ALL_COMMENTS = []
  def initialize(message, photo, user)
@message = message
@photo = photo
@user = user
ALL_COMMENTS << self
end
end

This hypothetical program could go on and on forever with new features (and classes and methods) sprouting as more real life “problems” are thought about, but the principles applied to those problems would be the same as above, so rambling on seems a bit superfluous. The point is that software is a real life tool to solve real life problems and should be utilized within that framework. Making introductory design decisions (establishing classes and what they know and do) can be done safely and, most importantly, realistically by asking one’s self the “real life” question. Doing so will help ensure that whatever code is produced does exactly what code is supposed to do, act as a tool to help solve a problem rooted in real life.

My attempt at whiteboarding while working through this problem.
“…with proper design, the features come cheaply. This approach is arduous but continues to succeed.”
— Dennis Ritchie