What is a Ruby class?
As a beginner Rubyist, your capabilities will increase significantly once you understand how to build and manage classes. So, what is a class? Every object within ruby is part of a class. In IRB, if you were to run [1,2,3].class, the output would be Array. Likewise, if you run {:name => "Joe"}.class, the output will be Hash.
Every object exists within a class, and that class includes instance methods that can be run on objects of that class. For example, ".include?" is an instance method within the array class. Running [1,2,3].include?(3) yields 'True'. The code behind the ".include?" method is written within the scope of the Array class, and so the ".include?"" method can be run on any instance of an array.
Once you've built your own class, you can create instances of that class, just like [1,2,3] is an instance of Array. Within your class you can create methods that are unique to instances of that class. As a simple example, lets say we created a class called Cars. You might then define a method within that class called honk, that, when called on an instance of Cars, returns the string "Honk!".
In my current role as a finance analyst, one of the worst parts of my jobs is keeping track of headcount for the businesses that I support. I'm going to walk you through the creation of a class that will help manage employees and expenses in our fictional company Hello Mellon, Inc.
Creating a class and instances of that class
Creating a class is super simple. Lets call ours 'Headcount':
class Headcount
end
gary = Headcount.new
bob = Headcount.new
craig = Headcount.new
Initializing
Cool, now we've got three objects - gary, bob, and craig - that are each an instance of our Headcount class. Let's refer to these objects as 'employees' from now on. So, what can we really do with our employees at this point? Not a whole lot. Lets fix that! When managing headcount, it is important for each employee in the database to have a first name, last name, and start date. If we were building a real app, we'd want these attributes to be required inputs when creating a new employee in a system. In order to require certain attributes when a new instance of Headcount is created, we need to use the initialize method:
class Headcount
def initialize(firstname, lastname, startdate)
end
end
in `initialize': wrong number of arguments (0 for 3) (ArgumentError)
This message is basically saying "Hang on there partner! In order to create an instance of Headcount, you need to input some basic information! Three tidbits of information, in fact. You input zero!" And that's because we didn't initialize the 'gary' instance of Headcount, meaning, we didn't input the required attributes of 'gary' when creating 'gary'. Let's give 'gary' some attributes:gary = Headcount.new("Gary", "Hammell", "8/4/2014")
No error? Great. So, we now have a working instance of Headcount, called 'gary'. Ok, so now what can we do with this? Again, not much yet! In any decent headcount system, you'll want to be able to easily see certain attributes of a specific employee. So, if we wanted to see gary's last name, how could we do that?Instance Variables
Through the use of instance variables of course! An instance variable is similiar to any other variable, except that it is only available within the scope of the class that it is defined in. Let's create some, and then I'll walk you through it:
class Headcount
def initialize(firstname, lastname, startdate)
@firstname = firstname
@lastname = lastname
@startdate = startdate
end
end
Instance Methods
Ok, so CAN WE DO ANYTHING YET? Not quite. We need to write some instance methods that, when called on an instance of Headcount, will perform some actions for us. There is no tag for an instance method like there is for an instance variable (the '@'), rather instance methods are just methods defined within a class, that can be called on specific instances of that class. Let's add a method called firstname below our initialize method:
class Headcount
def initialize(firstname, lastname, startdate)
@firstname = firstname
@lastname = lastname
@startdate = startdate
end
def firstname
@firstname
end
end
gary = Headcount.new("Gary", "Hammell", "8/4/2014")
puts gary.firstname
Gary
Gnarrrr! By calling the firstname method on 'gary', we are accessing the corrseponding code within the Headcount class. All this method does is return the instance variable @firstname, which was set equal to the user input! Likewise, we can define methods to return @lastname and @startdate:
class Headcount
def initialize(firstname, lastname, startdate)
@firstname = firstname
@lastname = lastname
@startdate = startdate
end
def firstname
@firstname
end
def lastname
@lastname
end
def startdate
@startdate
end
end
gary = Headcount.new("Gary", "Hammell", "8/4/2014")
puts gary.firstname
puts gary.lastname
puts gary.startdate
Gary
Hammell
8/4/2014
attr_accessor
Let's say you messed up, and input the last name 'Hammell', even though my real last name is 'THUNDERCLAP'. You've already created 'gary', so how could we go about changing the last name? You might try:
gary.lastname = "THUNDERCLAP"
Except you'll get:
undefined method `lastname=' for #
def lastname=(lastname)
@lastname=lastname
end
gary = Headcount.new("Gary", "Hammell", "8/4/2014")
puts gary.lastname
gary.lastname = "THUNDERCLAP"
puts gary.lastname
Hammell
THUNDERCLAP
def lastname
@lastname
end
def lastname=(lastname)
@lastname=lastname
end
attr_accessor :lastname
Let's do that for each of our simple methods. Our total code now looks like this:
class Headcount
attr_accessor :firstname, :lastname, :startdate
def initialize(firstname, lastname, startdate)
@firstname = firstname
@lastname = lastname
@startdate = startdate
end
end
gary = Headcount.new("Gary", "Hammell", "8/4/2014")
puts gary.lastname
gary.lastname = "THUNDERCLAP"
puts gary.lastname
Hammell
THUNDERCLAP
Alright, lets step things up a notch.
More instance methods
We want this to be a well rounded headcount application, so lets require a couple extra inputs when creating a new employee: base salary, bonus, and grade level.
class Headcount
attr_accessor :firstname, :lastname, :base, :bonus, :grade, :startdate
def initialize(firstname, lastname, base, bonus, grade, startdate)
@firstname = firstname
@lastname = lastname
@base = base
@bonus = bonus
@grade = grade
@startdate = startdate
end
end
def monthly_exp
ben_tax_rate = 0.185
(@base * (1+@bonus) * (1+ben_tax_rate) / 12).to_i
end
Let's try it out:
gary = Headcount.new("Gary", "Hammell", 500000, 0.3, 8, "8/4/2014")
#that's right, $500000 / year baby! ..maybe someday...
puts gary.monthly_exp
64187
Damn, that's a lot of cash money for one month. We could change the method to output the value as a string, formatted like '$64,187', but that would make it more difficult to access from other methods within the Headcount class. That's a challenge for another day.Let's also create a method to calculate the daily expense of an employee, which uses the 'monthly_exp' method. Let's add this to our Headcount class:
def daily_exp
monthly_exp * 12 / 365
end
gary.daily_exp
2110
Nice.What if we wanted to see the amount of money that Hello Mellon will be paying for an employee for the rest of the year? Let's try out another method! This is going to get a little bit trickier. Before we write the method, lets switch a couple things up. First, lets add the code
require 'date'
to the top of our program. I won't get too into this, but basically this allows us to access additional methods that are already defined within a different class, Date. Now, lets also switch up our required parameters again. Lets break 'startdate' into three paramaters 'year', 'month', and 'day' in our initialize method. So, now our code looks like this:
require 'date'
class Headcount
attr_accessor :firstname, :lastname, :base, :bonus, :grade, :startdate
def initialize(firstname, lastname, base, bonus, grade, startyear, startmonth, startday)
@firstname = firstname
@lastname = lastname
@base = base
@bonus = bonus
@grade = grade
@startdate = startdate
end
def monthly_exp
ben_tax_rate = 0.185
(@base * (1+@bonus) * (1+ben_tax_rate) / 12).to_i
end
def daily_exp
monthly_exp * 12 / 365
end
end
@startdate = Date.civil(startyear,startmonth,startday)
Now, upon initialization, we're creating an instance variable @startdate, that is set to an actual date value created by the Date.civil method. The Date.civil method takes three inputs - a year, a month, and a day - and returns a date value that functions like a number, as opposed to a string.Ok, so our code looks like this:
require 'date'
class Headcount
attr_accessor :firstname, :lastname, :base, :bonus, :grade, :startdate
def initialize(firstname, lastname, base, bonus, grade, startyear, startmonth, startday)
@firstname = firstname
@lastname = lastname
@base = base
@bonus = bonus
@grade = grade
@startdate = Date.civil(startyear,startmonth,startday)
end
def monthly_exp
ben_tax_rate = 0.185
(@base * (1+@bonus) * (1+ben_tax_rate) / 12).to_i
end
def daily_exp
monthly_exp * 12 / 365
end
end
def remaining_cost
yearend = Date.civil(2014,12,31)
cost = if @startdate < Date.today
(yearend - Date.today) * daily_exp
else (yearend - @startdate) * daily_exp
end.to_i
end
Remember how I said that using 'Date.civil' creates a date value that functions like a number? That's why something like (yearend - Date.today) works. That equation returns the number of days between the two dates.
Let's try it:
p gary.remaining_cost
314390
(as of 7/8/2014.. also, disclaimer, these values might not be 100% because I am not accounting for cents)
Awesome. So now we've got three great methods to help us manage Hello Mellon's expenses. But each of these methods only provides us information on individual employees. What if we wanted a report of monthly expense by employee? Or what if we wanted to know our total employee count? For these functions, my friends, we need class methodsClass Methods
Class methods are methods that are called on classes themselves. Until now, we've been writing instance methods that are called on specific objects of a class, like gary.firstname. A class method would look something like Headcount.report. See how the method is called on the class itself? Lets jump right in to some applications.In order to write effective class methods, we need some class variables. A class variable is a variable that is the same between all instances of a class. It is a global variable within the context of a single class, and its syntax requires '@@' in front of the variable name. Let's add the following to our code, just above the attr_accessor method:
@@employee_count = 0
@@employees = {}
Let's also add the following code into our initialize method (don't worry, we'll cover everything here):
@id = (rand(1000) + 1).to_s.rjust(6,'0')
@@employee_count += 1
@@employees[@id] =
{firstname: firstname,
lastname: lastname,
base: base,
bonus: bonus,
grade: grade,
function: function,
startdate: startdate}
require 'date'
class Headcount
@@employee_count = 0
@@employees = {}
attr_accessor :firstname, :lastname, :base, :bonus, :grade, :startdate
def initialize(firstname, lastname, base, bonus, grade, startyear, startmonth, startday)
@firstname = firstname
@lastname = lastname
@base = base
@bonus = bonus
@grade = grade
@startdate = Date.civil(startyear,startmonth,startday)
@id = (rand(1000) + 1).to_s.rjust(6,'0')
@@employee_count += 1
@@employees[@id] =
{firstname: firstname,
lastname: lastname,
base: base,
bonus: bonus,
grade: grade,
function: function,
startdate: startdate}
end
def monthly_exp
ben_tax_rate = 0.185
(@base * (1+@bonus) * (1+ben_tax_rate) / 12).to_i
end
def daily_exp
monthly_exp * 12 / 365
end
def remaining_cost
yearend = Date.civil(2014,12,31)
cost = if @startdate < Date.today
(yearend - Date.today) * daily_exp
else (yearend - @startdate) * daily_exp
end.to_i
end
end
- we added an instance variable @id to our initialize method. This variable is set equal to a random number between 1 and 1000, and will act as a unique identifier for each of Hello Mellon's employees.
- we added @@employee_count +=1 to our initialize method. Now, everytime we create a new employee, this counter will increase by 1
- and with the @@employees section of our initialize method, every time we create a new employee, the @@employees hash will be updated with a key, set equal to the employees ID, and a value, set equal to another hash that includes all of the other employee information.
This may seem like a lot, so I highly recommend reviewing all of the above code to ensure that you understand it's purpose before continuing in this exercise.
Alright! Now lets make a method that spits out a report of all employees within Hello Mellon. Our first class method!! Notice how the method is defined using the syntax self.method_name:
def self.summary
puts "Hello Mellon has #{@@employee_count} employees:"
puts
@@employees.each do |id, info|
puts "ID: #{id}"
info.each do |data, value|
print "#{data}: #{value}; "
end
puts puts
end
end
gary = Headcount.new("Gary", "Hammell", 500000, 0.3, 8, 2014, 8, 4)
craig = Headcount.new("Craig", "Hammell", 300000, 0.25, 7, 2014, 10, 1)
bob = Headcount.new("Bob", "Hammell", 250000, 0.2, 7, 2014, 9, 2)
jack = Headcount.new("Jack", "Hammell", 400000, 0.3, 8, 2014, 11, 20)
angela = Headcount.new("Angela", "Hammell", 200000, 0.2, 6, 2014, 2, 28)
# It's a family company
Headcount.summary
We get:
Hello Mellon has 5 employees:
ID: 000764
firstname: Gary; lastname: Hammell; base: 500000; bonus: 0.3; grade: 8; startdate: 2014-08-04;
ID: 000362
firstname: Craig; lastname: Hammell; base: 300000; bonus: 0.25; grade: 7; startdate: 2014-10-01;
ID: 000222
firstname: Bob; lastname: Hammell; base: 250000; bonus: 0.2; grade: 7; startdate: 2014-09-02;
ID: 000964
firstname: Jack; lastname: Hammell; base: 400000; bonus: 0.3; grade: 8; startdate: 2014-11-20;
ID: 000679
firstname: Angela; lastname: Hammell; base: 200000; bonus: 0.2; grade: 6; startdate: 2014-02-28;
Object Space
We might write something like:
def self.total_costs
puts "Hello Mellon remaining headcount costs by employee for the current year:"
puts
total = ObjectSpace.each_object(Headcount).inject(0) do |result, employee|
puts "#{employee.firstname} #{employee.lastname}: #{employee.remaining_cost}"
result += employee.remaining_cost
end
puts
puts "The total remaining headcount costs for the year is #{total}"
end
Headcount.total_costs
We'd get:
Hello Mellon remaining headcount costs by employee for the current year:
Angela Hammell: 137104
Jack Hammell: 69208
Bob Hammell: 116760
Craig Hammell: 110747
Gary Hammell: 314390
The total remaining headcount costs for the year is 748209
- We did not include a value in the employee info hash that represented the remaining expense for that employee, so there would have been nothing to toggle if we iterated through that hash
- You might be thinking, 'well then why not just add the remaining expense value to each employee's information hash?', and the answer is that I wanted the output of the remaining_cost method to remain dynamic, meaning that it would change every day. If we stored a value in the hash, then it would be a static value that might not actually represent the remaining expense in the year. So, I needed to figure out a way to iterate through each employee and at the same time run employee.remaining_cost.
Thanks
I really hope you enjoyed this tutorial. There is a lot more that could be built into this class, and there is definitely room for improvement in my current code, but I think this is a good starting point. Hopefully someday I can build something like this into an awesome headcount app so that finance analysts like myself don't tear their hair out tying headcount out.Thanks!!
Copyright: Gary Hammell 2014