Today’s post is on something that was confusing to me and is probably confusing to other budding ruby developers. 100% of the credit for any understanding is owed to a combination of Avi, Jeffrey Baird, Sandi Metz, a definition I found online from the Dave Thomas ruby book, and osmosis from classmates. Unless something’s wrong, in which case it’s 100% me.
Ah Befuddlement, I Know ye Well
Anyone who’s dabbled in ruby and tried to understand Object Oriented Programming must understand scope. Everything in ruby is an object, and there are limitations (or at least should be) to what kinds of other things each object has access to and can affect. One way to visualize this is as an electrical outlet.
If you only have one thing plugged into an outlet, like a drill, it’s simple to remove that plug or put it in a different socket if you subsequently want to plug in a chainsaw. (People use electric chainsaws, right?) On the other hand, if your outlet is chocked with a million plugs and extension cords it get trickier to find the cord you want, and if you try to change the setup later it’s easy to unplug your monitor when you thought you were pulling on the lamp chord.
Proper Ignorance vs Entanglements
Ruby object are somewhat similar and a million times more elegant. A ruby object needs to know/have access to only the stuff it should, in order to do its job, nothing more, nothing less. This is what prevents unnecessary entanglements, and allows you (and me) to build awesome, powerful modularity into programs, and also what inclines rubyists towards the convention of building numerous small, single-purpose, clearly defined and clearly labeled methods instead of larger, more-comprehensive ones. Eluminating this way of building is Metz’s ballywick; check out her talk: http://www.poodr.info/blog. (Also a super nice and interesting lady.)
One first speedbump in learning to build and think this way is both obvious and surprizing when you first arrive at it: if I’m building wonderful, clean, isolated methods and single-purpose objects, how can I get anything done? If I have an object/message in a Spaceship class, how can I get it over to an object of the GroundControl class? Elegantly, of course, and many different ways. Ruby’s about choice! However you choose to do it though, the process relies on the concept of approriately ignorant object, and scope.
Your Ruby is Haunted
One key to understanding scope is understanding self. (…or finding Dave Thomas’ explanations of self somewhere online.) In ruby the idea of ‘self’ is attached to whatever part of your program is currently being processed. If you create an instance of a class and give it an attribute, the only one who will know the value of that attribute is the instance itself.
How to Pass a Pirate
To demo this way of getting messages between classes I’ll walk through an example I found very helpful, given to me by the super-fantabulous Jeffrey Baird – a teacher here at the Flatiron School. Definitely good for beginners, but probably not absolute beginners. I’m also going to skip ahead to the relevant part.
Setup: make 4 files, pirate_test.rb, ship_test.rb, and pirate.rb and ship.rb. (download the gem if you like, or run a different test-suite.)Copy the test content into the appropriate file, run, add code to the appropriate file, run, repeat until you get the tests to pass.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
1 2 3 4 5 6 7 8 9 10 11
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
At this point your pirate tests should be passing, and running your ship_test.rb file is giving you this:
ruby ship_test.rb pass: should instantiate a ship fail: a ship should know all the pirates on it
So. How do you get a ship to know all of the pirates on it? Here’s where we need to rexamine the notion of self and scope. We want to be able to ask a ship, ‘Hey ship, who are all of your pirates?’ So first we’ll want the Ship class to keep a list of all the pirates available to the class. Let’s use a constant to store an array of all pirates. Then we’ll give every instance a(n instance) method that will simply show all the pirates.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Now our Ship class has an empty list setup to store pirates, and you can call pirates on any ship instance and get that list. But how do we populate the list? The Ship class doesn’t have access to any pirates, doesn’t know their name, but we need to get them on the ships’ instances. Instances of Ship also don’t know any of the pirates. I need to gets me some pirates into the PIRATES constant in the ship class, hopefully without creating any new unnecessary connections between the classes. Let’s check out the Pirate class again.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
What references do we have to ship? Well, we have a lovely getter and setter for adding a ship as an attribute for pirate instances, ship= and ship. But what do these methods “know about?” If we’ve made a pirate instance called johnny, when we call johnny.ship = “The Sea Bitch”, self is johnny and we’re passing the ship name “The Sea Bitch” to the johnny instance. At that moment we’ve connected some information from the Ship class (a ship.name) and an object from the Pirate class. The argument passed to ship= is an instance of Ship that was already created and given a name. At the same ‘moment’, ie. while self is passing through the ship= method you can piggy-back on the values present. How?
The key is realizing that we have a ship.pirates method, and remembering that when you call pirate.ship=, self is a pirate instance and the passed-argument is a ship instance. Key: as soon as that ship instance arrives into the method and its reciever, the method has the ability to call the appropriate Ship class’s methods on the ship instance. You want to give it the current instance of pirate, so give it SELF! Modify the ship= method:
1 2 3 4
It’s that clean! What’s going on? Basically we alter the method to allow us to do pirate.ship=. So we can set the ship for a pirate. Then, assigning the ship instance passed in as an argument to the @ship instance variable allows us to set the name of the ship for the instance. ship.pirates << self allows us to add the instance that ship= is being called on (an instance of pirate) to THE RESULT of callin the pirates method on an instance of ship.
What can get confusing is that it seems like we’re playing around in the Pirate class to add something to the Ship class, but what we’re actually doing is telling an instance of the Pirate class to add itself to the PIRATES list inside the ship class, as soon as it is assigned a ship attribute. We’re using objects and their attributes to communicate with other objects and atributes. And we’re keeping objects decently ignorant about all things besides their appropriate attributes.