Classes As Objects And Message-Receivers
Join the DZone community and get the full member experience.
Join For FreeClasses are special objects: they’re the only kind of object that has the power to spawn new objects (instances). Nonetheless, they’re objects. When you create a class, like Ticket, you can send messages to it, add methods to it, pass it around to other objects as a method argument, and generally do anything to it you would another object. Like other objects, classes can be created—indeed, in more than one way.
Creating class objects
This article is based on chapter 3 from The Well-Grounded Rubyist by David Black, to be published January 2009. It is being reproduced here by permission from Manning Publications. Manning early access books and ebooks are sold exclusively through Manning. Visit the book's page for more information
[img_assist|nid=6662|title=|desc=|link=url|url=http://www.manning.com/black2/|align=right|width=146|height=182]Every class—Object, Person, Ticket—is an instance of a class called Class. As you’ve already seen, you can create a class object with the special class keyword formula:
class Ticket
# your code here
end
That formula is a special provision by Ruby—a way to make nice-looking, easily accessed class-definition block. But you can also create a class the same way you create most other objects, by sending the message new to the class object Class:
my_class = Class.new
In this case, the variable my_class is assigned a new class object. Class.new corresponds precisely to other constructor calls like Object.new and Ticket.new. When you nstantiate the class Class, you create a class. That class, in turn, can create instances of its own:
instance_of_my_class = my_class.new
Class objects are usually represented by constants (like Ticket or Object). In the scenario in the previous
example, the class object is bound to a regular variable (my_class). Calling the new method sends the message new to the class through that variable.
TIP: DEFINING INSTANCE METHODS IN CONNECTION WITH CLASS.NEW
If you want to create an anonymous class using Class.new, and also want to add instance methods to it at the time you create it, you can do so by appending a code block after the call to new. A code block is a fragment of code that you supply as part of a method call, which can be executed from the method itself. Here’s a small example of Class.new with a block:
c = Class.new do
def say_hello
puts "Hello!"
end
end
If you now create an instance of the class (with c.new), you’ll be able to call the method say_hello on that
instance.
And yes, there is a paradox here....
The Class/Object Chicken-And-Egg Paradox
The class Class is an instance of itself; that is, it’s a Class object. And there’s more. Remember the class
Object? Well, Object is a class... but classes are objects. So, Object is an object. And Class is a class. And
Object is a class, and Class is an object.
Which came first? How can the class Class be created unless the class Object already exists? But how can
there be a class Object (or any other class) until there’s a class Class of which there can be instances?
The best way to deal with this paradox, at least for now, is to ignore it. Ruby has to do some of this chicken-or-egg stuff in order to get the class and object system up and running—and then, the circularity and paradoxes don’t matter. In the course of programming, you just need to know that classes are objects, instances of the class called Class.
(If you want to know in brief how it works, it’s like this: every object has an internal record of what class it’s an instance of, and the internal record inside the object Class points back to Class.) Classes are objects, and objects receive messages and execute methods. How exactly does the method-calling
process play out in the case of class objects?
How class objects call methods
When you send a message to a class object, it looks like this:
Ticket.some_message
Or, if you’re inside a class definition body, and the class is playing the role of the default object self, it looks like this:
class Ticket
some_message # such as "attr_accessor"!
That’s how the class object gets messages. But where do the methods come from to which the messages
correspond? To understand where classes get their methods, think about where objects in general get their methods:
- from their class
- from the superclass and earlier ancestors of their class
- from their own store of singleton methods (the “talk” in def obj.talk)
The situation is basically the same for classes. There are some, but very few, special cases or bells and whistles for class objects. Mostly they behave like other objects. Let’s look at the three scenarios for method-calling listed above, in the case of class objects.
Instances of Class can call methods that are defined as instance methods in their class. Ticket, for example, is an instance of Class, and Class defines an instance method called new. That’s why we can write:
Ticket.new
That takes care of scenario 1. Now, scenario 2. The superclass of Class is Module. Instances of Class therefore have access to the instance methods defined in Module; among these are the attr_accessor family of methods. That’s why we can write:
class Ticket
attr_reader :venue, :date
attr_accessor :price
with those method calls going directly to the class object Ticket, which is in the role of the the default object self at the point when the calls are made. That leaves just scenario 3: calling a singleton method of a class object.
A singleton method by any other name...
Here’s an example. Let’s say we’ve created our Ticket class. At this point, Ticket isn’t only a class from which objects (ticket instances) can arise. Ticket (the class) is also an object in its own right. As we’ve done with other objects, let’s add a singleton method to it.
Our method will tell us which ticket, from a list of ticket objects, is the most expensive. There’s some black-box code here. Don’t worry about the details; the basic idea is that the max_by operation will find the ticket whose price is highest:
def Ticket.most_expensive(*tickets)
tickets.max_by(&:price)
end
Now we can use the Ticket.most_expensive method to tell us which of several tickets is the most expensive. (We’ll avoid having two tickets with the same price, because our method doesn’t deal gracefully with that situation.)
th = Ticket.new("Town Hall","11/12/13")
cc = Ticket.new("Convention Center","12/13/14/")
fg = Ticket.new("Fairgrounds", "13/14/15/")
th.price = 12.55
cc.price = 10.00
fg.price = 18.00
highest = Ticket.most_expensive(th,cc,fg)
puts "The highest-priced ticket is the one for #{highest.venue}."
The output from this example is:
The highest-priced ticket is the one for Fairgrounds.
The method most_expensive is defined directly on the class object Ticket, in singleton-method style. A
singleton method defined on a class object is commonly referred to as a class method of the class on which it is defined. The idea of a class method is that you send a message to the object that is the class rather than to one of the class’s instances. The message most_expensive goes to the class Ticket, not to a particular ticket.
Why would you want to do that? Doesn’t it mess up the underlying order: the creation of ticket objects and the sending of messages to those objects?
When, and why, to write a class method
Class methods serve a purpose. Some operations pertaining to a class can’t be performed by individual instances of that class. The new method is an excellent example. We call Ticket.new because, until we’ve created an individual ticket, we can’t send it any messages! Besides, the job of spawning a new object logically belongs to the class. It doesn’t make sense for instances of Ticket to spawn each other. It does make sense, however, for the instance-creation process to be centralized as an activity of the class Ticket.
Another similar case is the built-in Ruby method File.open—a method which opens a file for reading and/or
writing. The open operation is a bit like new; it initiates file input and/or output and returns a File object. It
makes sense for open to be a class method of File: you’re requesting the creation of an individual object from the class. The class is acting as a point of departure for the objects it creates.
Ticket.most_expensive is a different case—it doesn’t create a new object—but it’s still a method that
belongs logically to the class. Finding the most expensive ticket in a list of tickets can be viewed as an operation from above, something that's done collectively with respect to the tickets, rather than something that's done by an individual ticket object. Writing most_expensive as a class method of Ticket lets us keep the method in the ticket family, so to speak, while assigning it to the abstract, supervisory level represented by the class.
It’s not unheard of to create a class only for the purpose of giving it class methods. Our earlier temperature-conversion exercises offer an opportunity for using this approach.
Converting The Converter
Let’s convert the converter to a converter class, adding two class methods for conversion in both directions. We’ll do a floating-point number version. (Using 9.0 instead of 9 will force floating-point results.)
class Temperature
def Temperature.c2f(f)
f * 9.0 / 5 + 32
end
def Temperature.f2c(c)
(c - 32) * 5 / 9.0
end
end
And let’s try it out:
puts Temperature.c2f(100) #A
#A 212.0
The idea is that we have temperature-related utility methods—methods pertaining to temperature as a concept but not to a specific temperature. The Temperature class is a good choice of object to own those methods. We could get fancier and have Temperature instances that knew whether they were C or F and could convert themselves; but practically speaking, having a Temperature class with class methods to perform the conversions is adequate and is an acceptable design. (Even better, since we don’t need instances of Temperature at all, would be to use a module, which is similar to a class but can’t create instances.)
Class methods and instance methods aren’t radically different from each other; they’re all methods, and their execution is always triggered by sending a message to an object. It’s just that the object getting the message may be a class object. Still, there are differences and important points to keep in mind as you start writing methods at various levels.
Class Methods vs. Instance Methods
By defining Ticket.most_expensive, we’ve defined a method that we can access through the class object
Ticket but not through its instances. Individual ticket objects (instances of the class Ticket) don’t have this
method. You can test this easily. Try adding this to the code where the variable fg referred to a Ticket object (for an event at the fairgrounds):
puts "Testing the response of a ticket instance...."
wrong = fg.most_expensive
You get an error message, because fg has no method called most_expensive. The class of fg—Ticket—has
such a method. But fg, which is an instance of Ticket, doesn’t. Remember:
- Classes are objects.
- Instances of classes are objects, too.
- A class object (like Ticket) has its own methods, its own state, its own identity. It doesn’t share these things with instances of itself. Sending a message to Ticket isn’t the same thing as sending a message to fg or cc or any other instance of Ticket.
If you ever get tangled up over what’s a class method and what’s an instance method, you can usually sort out the confusion by going back to these three principles. Meanwhile, now that we’ve got class and instance methods on the radar, a word about a couple of notational conventions is in order.
A Note On Method Notation
In writing about and referring to Ruby methods (outside of code, that is), it’s customary to refer to instance
methods by naming the class (or module, as the case may be) in which they’re defined, followed by a hash mark (#) and the name of the method; and to refer to class methods with a similar construct but using a period instead of the hash mark. Sometimes you’ll see a double colon (::) instead of a period in the class-method case. Here are some examples of this notation:
Notation | Method referred to |
Ticket#price | The instance method price in the class Ticket |
Ticket.most_expensive | The class method most_expensive, in the class Ticket |
Ticket::most_expensive | Another way to refer to the class method most_expensive |
From now on, when you see this notation (in this book or elsewhere), you’ll know what it means. (The second example—class-method reference using a dot—looks the same as a call to the method, but you’ll know from the context whether it’s a method call or a reference to the method in a discussion.)
The term “class method”: more trouble than it’s worth?
Ruby lets objects have singleton methods, and classes are objects. So when you do def
Ticket.most_expensive, you’re basically creating a singleton method for Ticket. On the calling side,
when you see a method called on a class object—like Ticket.new—you can’t tell just by looking whether you’re dealing with a singleton method defined directly on this class (def Ticket.new) or an instance method of the class Class.
Just to make it even more fun, the class Class has both a class method version of new and an instance method version; the former is called when you write Class.new, and the latter when you write Ticket.new. Unless, of course, you override it by defining new for Ticket yourself....
Admittedly, new is a particularly thorny case. But in general, the term “class method” is not necessarily a great fit for Ruby. It’s a concept shared with other object-oriented languages, but in those languages there’s a greater difference between class methods and instance methods. In Ruby, when you send a message to a class object, you can’t tell where and how the corresponding method was defined.
So there’s a fuzzy meaning to “class method,” and a sharp meaning. Fuzzily, any method that gets called directly on a Class object is a class method. Sharply, a class method is defined, not just called, directly on a Class object. You’ll hear it used both these ways, and as long as you’re aware of the underlying engineering and can make the sharp distinctions when you need to, you’ll be fine.
Opinions expressed by DZone contributors are their own.
Trending
-
Integration Testing Tutorial: A Comprehensive Guide With Examples And Best Practices
-
Reducing Network Latency and Improving Read Performance With CockroachDB and PolyScale.ai
-
Micro Frontends on Monorepo With Remote State Management
-
Implementing a Serverless DevOps Pipeline With AWS Lambda and CodePipeline
Comments