The only easy way to change an object’s state in Ruby is by calling one of its methods. Control access to the methods, and you have controlled access to the object.
A good rule of the thumb is never to expose methods that could leave an object in an invalid state.
The last sentence in the above quote can be achieved by properly defining an interface. When looking for a way to explain what this could look like in a ‘real-life’ scenario, I was only too happy to reach for my copy of Sandi Metz’s book Practical Object-Oriented Design in Ruby.
In chapter 4 she gives a detail filled, ‘real-life’ analogy about defining interfaces. Here’s a quick excerpt about a restaurant kitchen. You’ll have to buy your own copy of her book in order to receive her entire analogy, and the other gems of understanding she dispenses throughout its entirety.
The kitchen does many things but does not, thankfully, expose them all to it’s customers. It has a public interface that customers are expected to use; the menu. Within the kitchen many things happen, many other messages gets passed, but these messages are private, and thus invisible to customers.
The idea behind this is to base programming logic on the interfaces of the objects used, rather than on internal implementation details.
Wikipedia says the following about software interfaces in object-oriented languages.
Lets pull up our Automobile class that we’ve been slowly building over the past two posts.
I’ll start with a class definition, an initializer method, and two instance methods:
The two instance methods defined in our Automobile class are methods that perform work on how to start and stop our Automobile object. From my understanding of Satish Talim’s excerpt at the beginning of this post. I will be better served (I’ll describe what I’ve read about technical debt a little later on) by hiding any moving parts, or methods that perform calculations, in a public interface. In hopes of a slightly better demonstration, I’m going to add a few more methods to our Automobile class.
Now that I’ve added a few more methods to our Automobile class, hopefully its becoming evident that there are a couple bundles of functionality that exist. In Ruby there are three levels of protection in regards to method access control.
These three levels of access control are:
Public methods – can be called by any object, as there is no enforced access control at this level.
Lets take a quick peak at an Automobile object calling any one of the methods, since as of now, they’re all listed with Ruby’s default access level, ‘public’.
I’ll use ‘start_engine’ to kick things off:
Ok, so Ruby defaults to ‘public’ access methods, how would we make some of our methods ‘protected’ or ‘private’?
Protected methods – Can only be invoked by an object’s class or subclass.
Private methods – Cannot be called with an explicit receiver (i.e. a variable containing an Automobile instance). With private methods the receiver is always self.
Turns out that there happens to be a couple of ways to label the access-level of our methods, here’s the first way:
Above I gave protected access to ‘start_engine’ and ‘stop_engine’. As shown above I passed symbols of both method names to the ‘protected’ keyword. I did the same with the methods that I gave ‘private’ access control over.
The second way to pull this off looks like so:
Here, I slightly re-arranged the order of my method definitions. I placed ‘drive’ just under my initializer so that the preceding keywords wouldn’t affect Ruby’s default public access that I want it to maintain. And then I placed the keyword just atop of the methods I wanted the keyword to cover, only adding another keyword to make the distinction that all following methods will be affected by it. That’s all there is to it!
The advantage of creating an interface is to decrease technical debt. Creating an interface consists of public descriptive methods that house protected or private implementation methods but hide their details. This way any alterations to implementation can be isolated to the tucked away implementation methods, and the interface can stand as is, to be used throughout the program.
In a future post I will elaborate on how to put all of this to work.