The Startup
Published in

The Startup

OOP 101

Not-So-Basic Basics of OOP

More on object-oriented programming in Ruby

Photo by The New Yorker

This is the last article in a four-part series on the basics of object-oriented programming in Ruby. Read the previous article here.

The gears turned once we introduced inheritance. And now that we understand the core concepts of object-oriented programming, not only can we start to build durable and versatile programs, or think at a high level of abstraction and solve problems in a clear, systematic way, we can start to understand more nuanced concepts in this article and beyond.

More On…

Variables: Constants

We’ve seen when an instance variable is initialized in a superclass (or mix-in module), it can be accessed in a subclass. The same goes for a class variable, but just like a class and its objects share one copy of a class variable, a subclass and its objects share the same copy.

And just like a subclass can override a superclass’s method, it can override a class variable. If we have a Spider class, we’d reassign @@legs to 8.

But since there’s only one copy of a class variable, this reassigns the class variable in the superclass and any other subclasses too!

For this reason, it’s recommended to avoid class variables in inheritance. If we have variables we never want to change, though, we can define constants with uppercase letters. Like class variables, constants are available across the class it’s defined and its subclasses.

We can’t change constants, but we can override them.

Now we can expect the heterotroph? method to return true when we call it on an Animal object, but 'What is that?' when we call it on a Dog object.

But it seems like by inheriting the Animal class’s heterotroph? method, the Dog object inherits the HETEROTROPH constant even after we overrode it!

When we call heterotroph? on the Animal object, Ruby finds the method in the object’s class. The method references HETEROTROPH, so Ruby checks the object’s class for the constant too. But when we call heterotroph? on the Dog object, Ruby doesn’t find the method in the object’s class. It finds the method in the Animal class, so it checks Animal for the constant rather than the object’s class. This is because constants are scoped lexically.

Since class variables are scoped at the class level, it doesn’t really matter where they’re defined or where we reference them. There’s only one copy of each class variable anyway. On the other hand, constants are scoped by the block they’re defined: when we reference a constant in a method of a class, we reference the one defined in that class definition, in that lexical scope.

So, if we want heterotroph? to reference the HETEROTROPH constant defined in the Dog class (when we call it on a Dog object), we have to define a heterotroph? method in Dog, where the constant is in lexical scope.

Now when we call heterotroph? on a Dog object, it references the HETEROTROPH constant defined in the expected lexical scope. But what happens if the constant isn’t defined there?

If we call heterotroph? on a Dog object now, Ruby still finds the method in the Dog class and checks Dog for the HETEROTROPH constant.

The constant is no longer in lexical scope, so Ruby traverses the inheritance hierarchy to look for it. In this case, Ruby finds it in the Animal class. (This is different from the method lookup path!)

A constant is encapsulated in such a way it can only be implicitly referenced in the class it’s defined or its subclasses. In another class, though, we can explicitly reference where to find it with the scope resolution operator.

We can also explicitly reference the calling object’s class by calling the class method on self. This way, we tell Ruby to check that class for the constant rather than the default lexical scope and inheritance hierarchy.

Earlier, we defined a heterotroph? method in the Dog class in order to reference the HETEROTROPH constant defined in Dog. But now we can simply inherit the method from the Animal class!

This is particularly useful when we want to extract methods to a mix-in module without having to worry about scope.

As a result, we bring constant scoping rules in line with how we normally expect to resolve variables!

Methods: Fake Operators

In the previous article, we said we inherit a myriad of methods in both custom and built-in classes. Some are of more value when we override them, like to_s, but particularly those we can call fake operators.

Many “operators” are actually method calls in Ruby. When we call a setter method like fluffy.name = 'Floofy', it looks like an assignment operation but is merely a more natural syntax for fluffy.name=('Floofy'). In a similar vein, when we add two objects, we call the + method. When we compare them, we call the == method. The latter is defined in BasicObject, so it’s available in every class, but most built-in classes override it as they do to_s.

Let’s see how it works by default in a custom class.

Since we haven’t redefined a == method, we call the BasicObject class’s that checks if two variables reference the same object or space in memory. It returns false because first_fluffy and second_fluffy don’t.

Calling the equal? method or comparing object IDs has the same behaviour.

We call the == method to compare object IDs, but since object IDs are integers, we refer to the Integer class’s == method. It checks if the integers have the same numeric value rather than reference the same object.

Likewise, the String class’s == method checks for value.

We call the equal? method and compare object IDs to verify first_string and second_string don’t reference the same object, but the == method returns true because they have the same value. This is a more meaningful way to compare them than if they reference the same object.

There are also more meaningful ways to compare first_fluffy and second_fluffy. We’ll override the BasicObject class’s == method to define what it means for Dog objects to be “equal”.

Now when we call the == method on first_fluffy and second_fluffy, it checks if they have the same name. If their names are String collaborator objects, the method calls the String class’s == method to compare them.

We verify they don’t reference the same object, but they have the same name! And by defining a == method, we get a != method as a bonus.

Some fake operators aren’t defined in BasicObject or Object, like the + method. Since they’re only available in a custom class if we define them, we should only define them if their expected behaviour makes sense there.

In any case, we should follow the conventions established in Ruby’s built-in classes. For example: the Integer class’s + method increments the value of an integer by the value of an argument and returns a new integer. The String class’s + method concatenates the value of a string with the value of an argument and returns a new string. The Array class’s + method concatenates the value of an array with the value of an argument and returns a new array. If we define a + method in the Dog class, then, it should increment or concatenate the value of a Dog object with the value of an argument and return a new Dog object.

We can define fake operators however we want, but it’s certainly more meaningful for the + method to increment or concatenate rather than, say, scramble a calling object’s name. In turn, this allows us to build more meaningful classes and objects in our programs.

Relationships: Collaborator Objects

From variable scopes to fake operators, we’ve seen everything interrelate in explicit and implicit class inheritance. If no class is an island entire of itself, no object is either. Let’s take a closer look at an object’s encapsulation of state and behaviours in another Dog class.

Like objects of any class, Dog objects assign state to instance variables and define behaviours by instance methods. But if state is only assigned to instance variables, how is it defined?

We’ve seen this example enough to know when we instantiate the Dog object, we assign a state 'Fluffy' to the @name instance variable. We can call the getter method for @name to verify.

Good! But the state looks like a string. We’ll call the class method on it.

So, the state is defined by the String class after all. In other words, the Dog object’s state is a String object! In-stance-ception.

That object, 'Fluffy', is a collaborator object since it acts as the state for an object of a different class. An object of any class, even a custom class, can be a collaborator object. That includes the Dog object!

Say we define a class for pet owners. Should a pet be a subclass, mix-in module, or collaborator object? It may help to think of collaboration as similar to mix-in modules in that it models has-a relationships rather than is-a relationships, but it models them between objects of different classes rather than objects and behaviours. A pet owner is a person (class inheritance), has an ability to groom (mix-in module), and has a pet (collaboration).

Let’s add a pet as a collaborator object, then.

When we instantiate an Owner object, we can assign any collaborator objects to the @name and @pet instance variables. If the owner has a dog, though, it’d make sense to assign a Dog object to @pet.

In this case, we do just that and assign a String object to @name. So, the getter methods for @name and @pet will return a String collaborator object and a Dog collaborator object respectively.

Recall when we call puts on an object, it calls to_s on it too. For the Dog collaborator object, by default, to_s returns the name of its class and the encoding of its object ID. But for the String collaborator object, the String class’s to_s method is called on it and returns a high-level string representation of the object.

Since the pet getter method returns the Dog collaborator object, we can chain any Dog instance method onto it as if we call the method on the collaborator object itself.

It’s likely none of this is very surprising. That’s because we’ve been using collaborator objects all along! But closely understanding them helps us to be more precise in how we break up and make up actors in our programs. Since object-oriented programs are not only the interaction of classes but that of objects, collaboration is a powerful tool to study under the hood.

The End

What a long way we’ve come! Take a look at the first article to review the benefits and core concepts of object-oriented programming; we should be able to understand them more clearly and practically now.

Since we have a strong foundation of the basics, we also have the tools to continue building upon it from here on out. That And I OOP! meme may be covered in dust, but object-oriented programming is here to stay.

Unlisted

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Rebecca Nguyen

Rebecca Nguyen

Software engineer @ GitHub. Co-creator @ Haven. Passionately curious. I like to read books, make ice cream, and learn things. Chinese-Vietnamese. She/her.