Chapter 4. Initializing Instances: Off to a Great Start
Right now, your class is a time bomb. Every instance you create starts out as a clean slate. If you call certain instance methods before adding data, an error will be raised that will bring your whole program to a screeching halt.
In this chapter, we’re going to show you a couple of ways to create objects that are safe to use right away. We’ll start with the initialize
method, which lets you pass in a bunch of arguments to set up an object’s data at the time you create it. Then we’ll show you how to write class methods, which you can use to create and set up an object even more easily.
Payroll at Chargemore
You’ve been tasked with creating a payroll system for Chargemore, a new chain of department stores. They need a system that will print pay stubs for their employees.
Chargemore employees are paid for two-week pay periods. Some employees are paid a two-week portion of their annual salary, and some are paid for the number of hours they work within the two-week period. For starters, though, we’re just going to focus on the salaried employees.
A pay stub needs to include the following information:
The employee name
The amount of pay an employee received during a two-week pay period
So...here’s what the system will need to know for each employee:
Employee name
Employee salary
And here’s what it will need to do:
Calculate and print pay for a two-week period
This sounds like the ideal place to create an Employee
class! Let’s try it, using the same techniques that we covered back in Chapter 2.
We’ll set up attribute reader methods for @name
and @salary
instance variables, then add writer methods (with validation). Then we’ll add a print_pay_stub
instance method that prints the employee’s name and their pay for the period.
An Employee class
Here’s some code to implement our Employee
class...
(Yes, we realize that this doesn’t account for leap years and holidays and a host of other things that real payroll apps must consider. But we wanted a print_pay_stub
method that fits on one page.)
Creating new Employee instances
Now that we’ve defined an Employee
class, we can create new instances and assign to their name
and salary
attributes.
amy = Employee.new amy.name = "Amy Blake" amy.salary = 50000
Thanks to validation code in our name=
method, we have protection against the accidental assignment of blank names.
Our salary=
method has validation to ensure that negative numbers aren’t assigned as a salary.
And when an Employee
instance is properly set up, we can use the stored name and salary to print a summary of the employee’s pay period.
Hmmm... It’s typical to display two decimal places when showing currency, though. And did that calculation really come out to an even dollar amount?
Before we go on to perfect our Employee
class, it looks like we have a bug to fix. And that will require us to go on a couple of brief detours. (But you’ll learn some number formatting skills that you’ll need later—promise!)
Our employee pay is getting its decimal places chopped off. To fix this, we’ll need to look at the difference between Ruby’s
Float
andFixnum
numeric classes.We don’t want to display too many decimal places, either, so we’ll need to look at the
format
method to format our numbers properly.
A division problem
We’re working to make the perfect Employee
class to help us calculate payroll for the Chargemore department store. But there’s a little detail we have to take care of first...
That’s true. Doing the math on paper (or launching a calculator app, if that’s your thing) can confirm that Amy should be earning $1917.81, rounded to the nearest cent. So where did that other $13.81 go?
To find out, let’s launch irb and do the math ourselves, step by step.
First, let’s calculate a day’s pay.
That’s nearly a dollar a day missing, compared to doing the math by hand:
50,000 ÷ 365 = 136.9863...
This error is then compounded when we calculate fourteen days’ pay:
Compare that to the answer we’d get if we multiplied the full daily pay:
136.9863 x 14 = 1917.8082...
So we’re nearly $14 off. Multiply that by many paychecks and many employees, and you’ve got yourself an angry workforce. We’re going to have to fix this, and soon...
Division with Ruby’s Fixnum class
The result of our Ruby expression to calculate two weeks of an employee’s pay doesn’t match up with doing the math by hand...
The problem here is that when dividing instances of the Fixnum
class (a Ruby class that represents integers), Ruby rounds fractional numbers down to the nearest whole number.
It rounds the number because Fixnum
instances aren’t meant to store numbers with decimal places. They’re intended for use in contexts where only whole numbers make sense, like counting employees in a department or the number of items in a shopping cart. When you create a Fixnum
, you’re telling Ruby: “I expect to only be working with whole numbers here. If anyone does math with you that results in a fraction, I want you to throw those pesky decimal places away.”
How can we know whether we’re working with Fixnum
instances? We can call the class
instance method on them. (Remember we talked about the Object
class back in Chapter 3? The class
method is one of the instance methods inherited from Object
.)
Or, if you’d rather save yourself the trouble, just remember that any number in your code that doesn’t have a decimal point in it will be treated as a Fixnum
by Ruby.
Any number in your code that does have a decimal point in it gets treated as a Float
(the Ruby class that represents floating-point decimal numbers):
If it’s got a decimal point, it’s a
Float
. If it doesn’t, it’s aFixnum
.
Division with Ruby’s Float class
We loaded up irb and saw that if we divide one Fixnum
(integer) instance by another Fixnum
, Ruby rounds the result down.
The solution, then, is to use Float
instances in the operation, which we can get by including a decimal point in our numbers. If you do, Ruby will give you a Float
instance back:
It doesn’t even matter whether both the dividend and divisor are Float
instances; Ruby will give you a Float
back as long as either operand is a Float
.
It holds true for addition, subtraction, and multiplication as well: Ruby will give you a Float
if either operand is a Float
:
When the first operand is a... | And the second operand is a... | The result is a... |
|
|
|
|
|
|
|
|
|
|
|
|
And of course, with addition, subtraction, and multiplication, it doesn’t matter whether both operands are Fixnum
instances, because there’s no fractional number to lose in the result. The only operation where it really matters is division. So, remember this rule:
When doing division, make sure at least one operand is a
Float
.
Let’s see if we can use this hard-won knowledge to fix our Employee
class.
Fixing the salary rounding error in Employee
As long as one of the operands is a Float
, Ruby won’t truncate the decimals from our division operation.
With this rule in mind, we can revise our Employee
class to stop truncating the decimals from employees’ pay:
Now we have a new problem, though: look what happens to the output!
We’re showing a little too much precision! Currency is generally expected to be shown with just two decimal places, after all. So, before we can go back to building the perfect Employee
class, we need to go on one more detour...
Formatting numbers for printing
Our print_pay_stub
method is displaying too many decimal places. We need to figure out how to round the displayed pay to the nearest penny (two decimal places).
To deal with these sorts of formatting issues, Ruby provides the format
method.
Here’s a sample of what this method can do. It may look a little confusing, but we’ll explain it all on the next few pages!
So it looks like format
can help us limit our displayed employee pay to the correct number of places. The question is, how? To be able to use this method effectively, we’ll need to learn about two features of format
:
Format sequences (the little
%0.2f
above is a format sequence)Format sequence widths (that’s the
0.2
in the middle of the format sequence)
Relax
We’ll explain exactly what those arguments to format
mean on the next few pages.
We know, those method calls look a little confusing. We have a ton of examples that should clear that confusion up. We’re going to focus on formatting decimal numbers, because it’s likely that will be the main thing you use format
for in your Ruby career.
Format sequences
The first argument to format
is a string that will be used to format the output. Most of it is formatted exactly as it appears in the string. Any percent signs (%
), however, will be treated as the start of a format sequence, a section of the string that will be substituted with a value in a particular format. The remaining arguments are used as values for those format sequences.
Format sequence types
The letter following the percent sign indicates the type of value that’s expected. The most common types are:
| string |
| integer |
| floating-point decimal |
So %f
is for floating-point decimal numbers... We can use that sequence type to format the currency in our pay stubs.
By itself, though, the %f
sequence type won’t help us. The results still show too many decimal places.
Up next, we’ll look at a fix for that situation: the format sequence width.
Format sequence width
Here’s the useful part of format sequences: they let you specify the width of the resulting field.
Let’s say we want to format some data in a plain-text table. We need to ensure the formatted value fills a minimum number of spaces, so that the columns align properly.
You can specify the minimum width after the percent sign in a format sequence. If the argument for that format sequence is shorter than the minimum width, it will be padded with spaces until the minimum width is reached.
And now we come to the part that’s important for today’s task: you can use format sequence widths to specify the precision (the number of displayed digits) for floating-point numbers. Here’s the format:
The minimum width of the entire number includes decimal places. If it’s included, shorter numbers will be padded with spaces at the start until this width is reached. If it’s omitted, no spaces will ever be added.
The width after the decimal point is the maximum number of digits to show. If a more precise number is given, it will be rounded (up or down) to fit in the given number of decimal places.
Format sequence width with floating-point numbers
So when we’re working with floating-point numbers, format sequence widths let us specify the number of digits displayed before and after the decimal point. Could this be the key to fixing our pay stubs?
Here’s a quick demonstration of various width values in action:
That last format, "%.2f"
, will let us take floating-point numbers of any precision and round them to two decimal places. (It also won’t do any unnecessary padding.) This format is ideal for showing currency, and it’s just what we need for our print_pay_stub
method!
Previously, our calculated pay for our Employee
class’s print_pay_stub
method was displayed with excess decimal places:
But now we finally have a format sequence that will round a floating-point number to two decimal places:
Let’s try using format
in the print_pay_stub
method.
Using “format” to fix our pay stubs
We can test our revised print_pay_stub
using the same values as before:
We had to make a couple of detours, but we’ve finally got our Employee
class printing pay stubs as it should! Next, we’ll get back to the business of perfecting our class...
When we forget to set an object’s attributes...
Now that you have the employee pay printing in the correct format, you’re puttering along, happily using your new Employee
class to process payroll. That is, until you create a new Employee
instance and forget to set the name
and salary
attributes before calling print_pay_stub
:
What happened? It’s only natural that the name is empty; we forgot to set it. But what’s this “undefined method for nil” error? What the heck is this nil
thing?
This sort of error is pretty common in Ruby, so let’s take a few pages to understand it.
Let’s alter the print_pay_stub
method to print the values of @name
and @salary
, so we can figure out what’s going on.
“nil” stands for nothing
Now let’s create a new Employee
instance and call the revised method:
Well, that wasn’t very helpful. Maybe we’re missing something, though.
Back in Chapter 1, we learned that the inspect
and p
methods can reveal information that doesn’t show up in ordinary output. Let’s try again, using p
:
We create another new instance, make another call to the instance method, and...
Ruby has a special value, nil
, that represents nothing. That is, it represents the absence of a value.
Just because nil
represents nothing doesn’t mean it’s actually nothing, though. Like everything else in Ruby, it’s an object, and it has its own class:
But if there’s actually something there, how come we didn’t see anything in the output?
It’s because the to_s
instance method from NilClass
always returns an empty string.
The puts
and print
methods automatically call to_s
on an object to convert it to a string for printing. That’s why we got two blank lines when we tried to use puts
to print the values of @name
and @salary
; both were set to nil
, so we wound up printing two empty strings.
Unlike to_s
, the inspect
instance method from NilClass
always returns the string "nil"
.
You may recall that the p
method calls inspect
on each object before printing it. That’s why the nil
values in @name
and @salary
appeared in the output once we called p
on them.
“/” is a method
So, when you first create an instance of the Employee
class, its @name
and @salary
instance variables have a value of nil
. The @salary
variable, in particular, causes problems if you call the print_pay_stub
method without setting it first:
It’s obvious from the error that the problem is related to the nil
value. But it says undefined method '/'
... Is division really a method?
In Ruby, the answer is yes; most mathematical operators are implemented as methods. When Ruby sees something like this in your code:
6 + 2
...it converts it to a call to a method named +
on the Fixnum
object 6
, with the object on the right of the +
(that is, 2
) as an argument:
Both forms are perfectly valid Ruby, and you can try running them yourself:
The same is true for most of the other mathematical operators.
Even comparison operators are implemented as methods.
But while the Fixnum
and Float
classes define these operator methods, NilClass
does not.
In fact, nil
doesn’t define most of the instance methods you see on other Ruby objects.
And why should it? If you’re doing mathematical operations with nil
, it’s almost certainly because you forgot to assign a value to one of the operands. You want an error to be raised, to bring your attention to the problem.
It was a mistake when we forgot to set a salary
for an Employee
, for example. And now that we understand the source of this error, it’s time to prevent it from happening again.
The “initialize” method
We tried to call print_pay_stub
on an instance of our Employee
class, but we got nil
when we tried to access the @name
and @salary
instance variables.
employee = Employee.new employee.print_pay_stub
Chaos ensued.
Here’s the method where the nil
values caused so much trouble:
Here’s the key problem: at the time we create an Employee
instance, it’s in an invalid state; it’s not safe to call print_pay_stub
until you set its @name
and @salary
instance variables.
If we could set @name
and @salary
at the same time as we create an Employee
instance, it would reduce the potential for errors.
Ruby provides a mechanism to help with this situation: the initialize
method. The initialize
method is your chance to step in and make the object safe to use, before anyone else attempts to call methods on it.
class MyClass def initialize puts "Setting up new instance!" end end
When you call MyClass.new
, Ruby allocates some memory to hold a new MyClass
object, then calls the initialize
instance method on that new object.
Ruby calls the
initialize
method on new objects after they’re created.
Employee safety with “initialize”
Let’s add an initialize
method that will set up @name
and @salary
for new Employee
instances before any other instance methods are called.
Now that we’ve set up an initialize
method, @name
and @salary
will already be set for any new Employee
instance. It’ll be safe to call print_pay_stub
on them immediately!
Arguments to “initialize”
Our initialize
method now sets a default @name
of "Anonymous"
and a default @salary
of 0.0
. It would be better if we could supply a value other than these defaults.
It’s for situations like this that any arguments to the new
method are passed on to initialize
.
We can use this feature to let the caller of Employee.new
specify what the initial name and salary should be. All we have to do is add name
and salary
parameters to initialize
, and use them to set the @name
and @salary
instance variables.
And just like that, we can set @name
and @salary
via arguments to Employee.new
!
Of course, once you set it up this way, you’ll need to be careful. If you don’t pass any arguments to new
, there will be no arguments to forward on to initialize
. At that point, you’ll get the same result that happens any time you call a Ruby method with the wrong number of arguments: an error.
We’ll look at a solution for this in a moment.
Using optional parameters with “initialize”
We started with an initialize
method that set default values for our instance variables, but didn’t let you specify your own...
Then we added parameters to initialize
, which meant that you had to specify your own name and salary values, and couldn’t rely on the defaults...
Can we have the best of both worlds?
Yes! Since initialize
is an ordinary method, it can utilize all the features of ordinary methods. And that includes optional parameters. (Remember those from Chapter 2?)
We can specify default values when declaring the parameters. When we omit an argument, we’ll get the default value. Then, we just assign those parameters to the instance variables normally.
With this change in place, we can omit one or both arguments and get the appropriate defaults!
The
new
method is needed to actually create the object;initialize
just sets up the new object’s instance variables.
“initialize” does an end-run around our validation
You remember our name=
attribute writer method, which prevents the assignment of an empty string as an Employee
name:
There’s also our salary=
attribute writer method, which ensures that negative numbers aren’t assigned as a salary:
We have bad news for you: since your initialize
method assigns directly to the @name
and @salary
instance variables, bad data has a new way to sneak in!
“initialize” and validation
We could get our initialize
method to validate its parameters by adding the same validation code to the initialize
method...
But duplicating code like that is a problem. What if we changed the initialize
validation code later, but forgot to update the name=
method? There would be different rules for setting the name, depending on how you set it!
Rubyists try to follow the DRY principle, where DRY stands for Don’t Repeat Yourself. It means that you should avoid duplicating code wherever possible, as it’s likely to result in bugs.
What if we called the name=
and salary=
methods from within the initialize
method? That would let us set the @name
and @salary
instance variables. It would also let us run the validation code, without duplicating it!
Call other methods on the same instance with “self”
We need to call the name=
and salary=
attribute writer methods from within the initialize
method of the same object. That will let us run the writer methods’ validation code before we set the @name
and @salary
instance variables.
Unfortunately, code like this won’t work...
The code in the initialize
method treats name=
and salary=
not as calls to the attribute writer methods, but as resetting the name
and salary
local variables to the same values they already contain! (If that sounds like a useless and nonsensical thing to do, that’s because it is.)
What we need to do is make it clear to Ruby that we intend to call the name=
and salary=
instance methods. And to call an instance method, we usually use the dot operator.
But we’re inside the initialize
instance method...what would we put to the left of the dot operator?
We can’t use the amy
variable; it would be silly to refer to one instance of the class within the class itself. Besides, amy
is out of scope within the initialize
method.
We need something to put to the left of the dot operator, so that we can call our Employee
class’s name=
and salary=
attribute accessor methods within our initialize
method. The problem is, what do we put there? How do you refer to the current instance from inside an instance method?
Ruby has an answer: the self
keyword. Within instance methods, self
always refers to the current object.
We can demonstrate this with a simple class:
class MyClass def first_method puts "Current instance within first_method: #{self}" end end
Within instance methods, the keyword
self
refers to the current object.
If we create an instance and call first_method
on it, we’ll see that inside the instance method, self
refers to the object the method is being called on.
The string representations of my_object
and self
include a unique identifier for the object. (We’ll learn more about this in Chapter 8.) The identifiers are the same, so it’s the same object!
We can also use self
with the dot operator to call a second instance method from inside the first one.
Now that we have self
to use the dot operator on, we can make it clear to Ruby that we want to call the name=
and salary=
instance methods, not to set the name
and salary
variables...
Let’s try calling our new constructor and see if it worked!
Success! Thanks to self
and the dot operator, it’s now clear to Ruby (and everyone else) that we’re making calls to the attribute writer methods, not assigning to variables.
And since we’re going through the accessor methods, that means the validation works, without any duplicated code!
When “self” is optional
Right now, our print_pay_stub
method accesses the @name
and @salary
instance variables directly:
class Employee def print_pay_stub puts "Name: #{@name}" pay_for_period = (@salary / 365.0) * 14 formatted_pay = format("$%.2f", pay_for_period) puts "Pay This Period: #{formatted_pay}" end end
But we defined name
and salary
attribute reader methods in our Employee
class; we could use those instead of accessing the instance variables directly. (That way, if you ever change the name
method to display last name first, or change the salary
method to calculate salary according to an algorithm, the print_pay_stub
code won’t need to be updated.)
We can use the self
keyword and the dot operator when calling name
and salary
, and it will work just fine:
But Ruby has a rule that can save us a little typing when calling from one instance method to another... If you don’t specify a receiver using the dot operator, the receiver defaults to the current object, self
.
As we saw in the previous section, you have to include the self
keyword when calling attribute writer methods, or Ruby will mistake the =
for a variable assignment. But for any other kind of instance method call, you can leave self
off, if you want.
If you don’t specify a receiver using the dot operator, the receiver defaults to the current object,
self
.
Implementing hourly employees through inheritance
The Employee
class you’ve created for Chargemore is working great! It prints accurate pay stubs that are formatted properly, and thanks to the initialize
method you wrote, it’s really easy to create new Employee
instances.
But, at this point, it only handles salaried employees. It’s time to look at adding support for employees that are paid by the hour.
The requirements for hourly employees are basically the same as for salaried ones; we need to be able to print pay stubs that include their name and the amount paid. The only difference is the way that we calculate their pay. For hourly employees, we multiply their hourly wage by the number of hours they work per week, then double that amount to get two weeks’ worth.
(salary / 365.0) * 14
Salaried employee pay calculation formula
hourly_wage * hours_per_week * 2
Hourly employee pay calculation formula
Since salaried and hourly employees are so similar, it makes sense to put the shared functionality in a superclass. Then, we’ll make two subclasses that hold the different pay calculation logic.
Let’s start by ensuring the common logic between SalariedEmployee
and HourlyEmployee
stays in the Employee
superclass.
Since pay stubs for both salaried and hourly employees need to include their names, we’ll leave the name
attribute in the superclass, for the subclasses to share. We’ll move the code that prints the name into the print_name
method in the superclass.
We’ll move the logic to calculate pay for salaried employees to the SalariedEmployee
class, but we’ll call the inherited print_name
method to print the employee name.
With those changes in place, we can create a new SalariedEmployee
instance, set its name and salary, and print a pay stub as before:
Now we’ll build a new HourlyEmployee
class. It’s just like SalariedEmployee
, except that it holds an hourly wage and number of hours worked per week, and uses those to calculate pay for a two-week period. As with SalariedEmployee
, storing and printing the employee name is left up to the Employee
superclass.
class HourlyEmployee < Employee attr_reader :hourly_wage, :hours_per_week def hourly_wage=(hourly_wage) # Code to validate and set @hourly_wage end def hours_per_week=(hours_per_week) # Code to validate and set @hours_per_week end def print_pay_stub print_name pay_for_period = hourly_wage * hours_per_week * 2 formatted_pay = format("$%.2f", pay_for_period) puts "Pay This Period: #{formatted_pay}" end end
And now we can create an HourlyEmployee
instance. Instead of setting a salary, we set an hourly wage and number of hours per week. Those values are then used to calculate the pay stub amount.
That wasn’t bad at all! Through the use of inheritance, we’ve implemented pay stubs for hourly employees, kept pay stubs for salaried employees, and minimized code duplication between the two.
We’ve lost something in the shuffle, though—our initialize
method. We used to be able to set up an Employee
object’s data at the time we created it, and these new classes won’t let us do that. We’ll have to add initialize
methods back in.
Restoring “initialize” methods
To make SalariedEmployee
and HourlyEmployee
objects that are safe to work with as soon as they’re created, we’ll need to add initialize
methods to those two classes.
As we did with the Employee
class before, our initialize
methods will need to accept a parameter for each object attribute we want to set. The initialize
method for SalariedEmployee
will look just like it did for the old Employee
class (since the attributes are the same), but initialize
for HourlyEmployee
will accept a different set of parameters (and set different attributes).
With our initialize
methods added, we can once again pass arguments to the new
method for each class. Our objects will be ready to use as soon as they’re created.
Inheritance and “initialize”
There’s one small weakness in our new initialize
methods, though: the code to set the employee name is duplicated between our two subclasses.
In all other aspects of our subclasses, we delegate handling of the name
attribute to the Employee
superclass. We define the reader and writer methods there. We even print the name via the print_name
method, which the subclasses call from their respective print_pay_stub
methods.
But we don’t do this for initialize
. Could we?
Yes! We’ve said it before, and we’ll say it again: initialize
is just an ordinary instance method. That means that it gets inherited like any other, that it can be overridden like any other, and that overriding methods can call it via super
like any other. We’ll demonstrate on the next page.
“super” and “initialize”
To eliminate the repeated name
setup code in our Employee
subclasses, we can move the name handling to an initialize
method in the superclass, then have the subclass initialize
methods call it with super
. SalariedEmployee
will keep the logic to set up a salary, HourlyEmployee
will keep the logic to set up an hourly wage and hours per week, and the two classes can delegate the shared logic for name
to their shared superclass.
First, let’s try moving the name handling from the initialize
method in SalariedEmployee
to the Employee
class.
Trying to use this revised initialize
method reveals a problem, though...
Oops! We forgot a key detail about super
that we learned earlier—if you don’t specify a set of arguments, it calls the superclass method with the same set of arguments that the subclass method received. (This is true when you’re using super
in other instance methods, and it’s true when you’re using super
within initialize
.) The initialize
method in SalariedEmployee
received two parameters, and super
passed them both on to the initialize
method in Employee
. (Even though it only accepts one argument.)
The fix, then, is to specify which parameter we want to pass on: the name
parameter.
Let’s try to initialize a new SalariedEmployee
again...
It worked! Let’s make the same changes to the HourlyEmployee
class...
Previously, we used super
within our print_pay_stub
methods in SalariedEmployee
and HourlyEmployee
to delegate printing of the employee name to the Employee
superclass. Now we’ve just done the same thing with the initialize
method, allowing the superclass to handle setting of the name
attribute.
Why does it work? Because initialize
is an instance method just like any other. Any feature of Ruby that you can use with an ordinary instance method, you can use with initialize
.
Same class, same attribute values
With your HourlyEmployee
class complete, Chargemore is ready to begin a hiring blitz to staff their new stores. Here’s the set of employees they need to create for their first store downtown:
If you look at the above code, you’ll probably notice there are large groups of objects where similar arguments are passed to the new
method. There’s a good reason for this: the first group are cashiers for the new store, the second group are janitors, and the third group are security guards.
Chargemore starts all new cashiers off at the same base pay and number of hours per week. Janitors get a different rate and number of hours than cashiers, but it’s the same for all janitors. And the same is true for security guards. (Individuals may get raises later, depending on performance, but they all start out the same.)
The upshot is that there’s a lot of repetition of arguments in those calls to new
, and a lot of chances to make a typo. And this is just the first wave of hiring, for the first Chargemore store, so things can only get worse. Seems like we can make this easier.
An inefficient factory method
When you need to make many instances of a class that have similar data, you can often save some repetition by making a factory method to create objects prepopulated with the needed attribute values. (Factory methods are a programming pattern that can be used in any object-oriented language, not just Ruby.)
But if we use only the tools we have now, any factory method we make will be inefficient at best.
To demonstrate what we mean, let’s try making a method to set up new HourlyEmployee
objects with the default pay and hours per week for cashiers.
This works, yes. So what’s so inefficient about it? Let’s look at our initialize
method (which of course has to run when we create a new HourlyEmployee
) again...
We’re setting the hourly_wage
and hours_per_week
attributes within initialize
, then immediately turning around and setting them again within turn_into_cashier
!
This is inefficient for Ruby, but there’s potential for it to be inefficient for us, too. What if we didn’t have default parameters for hourly_wage
and hours_per_week
on initialize
? Then, we’d have to specify the arguments we’re throwing away!
That’s the problem with writing factory methods as instance methods: we’re trying to make a new instance of the class, but there has to already be an instance to run the methods on! There must be a better way...
Fortunately, there is! Up next, we’re going to learn about class methods.
Class methods
You don’t have an instance of a class, but you need one. And you need a method to set it up for you. Where do you put that method?
You could stick it off by itself in some little Ruby source file, but it would be better to keep it together with the class that it makes instances of. You can’t make it an instance method on that class, though. If you had an instance of the class, you wouldn’t need to make one, now would you?
It’s for situations like this that Ruby supports class methods: methods that you can invoke directly on a class, without the need for any instance of that class. You don’t have to use a class method as a factory method, but it’s perfect for the job.
A class method definition is very similar to any other method definition in Ruby. The difference: you specify that you’re defining it on the class itself.
Within a class definition (but outside any instance method definitions), Ruby sets self
to refer to the class that’s being defined. So many Rubyists prefer to replace the class name with self
:
In most ways, class method definitions behave just like you’re used to:
You can put as many Ruby statements as you like in the method body.
You can return a value with the
return
keyword. If you don’t, the value of the last expression in the method body is used as the return value.You can optionally define one or more parameters that the method accepts, and you can make the parameters optional by defining defaults.
We’ve defined a new class, MyClass
, with a single class method:
class defined a new class, def self.my_class_method(p1, p2) puts "Hello from MyClass!" puts "My parameters: #{p1}, #{p2}" end end
Once a class method is defined, you can call it directly on the class:
Perhaps that syntax for calling a class method looks familiar to you...
MyClass.new
That’s right, new
is a class method! If you think about it, that makes sense; new
can’t be an instance method, because you’re calling it to get an instance in the first place! Instead, you have to ask the class for a new instance of itself.
Now that we know how to create class methods, let’s see if we can write some factory methods that will create new HourlyEmployee
objects with the pay rate and hours per week already populated for us. We need methods to set up predefined pay and hours for three positions: cashier, janitor, and security guard.
We won’t know the name of the employee in advance, so we accept that as a parameter to each of the class methods. We do know the values for hourly_wage
and hours_per_week
for each employee position, though. We pass those three arguments to the new
method for the class, and get a new HourlyEmployee
object back. That new object is then returned from the class method.
Now we can call the factory methods directly on the class, providing only the employee name.
angela = HourlyEmployee.security_guard("Angela Matthews") edwin = HourlyEmployee.janitor("Edwin Burgess") ivan = HourlyEmployee.cashier("Ivan Stokes")
The HourlyEmployee
instances returned are fully configured with the name we provided, and the appropriate hourly_wage
and hours_per_week
for the position. We can begin printing pay stubs for them right away!
In this chapter, you’ve learned that there are some pitfalls to creating new objects. But you’ve also learned techniques to ensure your objects are safe to use as soon as you make them. With well-designed initialize
methods and factory methods, creating and configuring new objects is a snap!
With well-designed
initialize
methods and factory methods, creating and configuring new objects is a snap!
Your Ruby Toolbox
That’s it for Chapter 4! You’ve added the initialize
method and class methods to your toolbox.
Up Next...
So far, we’ve been working with objects one at a time. But it’s much more common to work with groups of objects. In the next chapter, we’ll show you how to create a group, with arrays. We’ll also show you how to process each of the items in those arrays, using blocks.
Get Head First Ruby now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.