Pretty much everything in a Ruby program is some type of 'Object'.
If you haven't already, you'll soon here the phrase 'Object Oriented Programming' thrown around. You'll also hear the abbreviations OO or OOP.
With OOP we create programs and try to model them after the real world. 'Objects' allow us to encapsulate real-world ideas and then manipulate them and provide the user of our program with some output. So the typical flow of any object-oriented program is as follows:
Input - some type of data or value
Program - takes the input and manipulates it
Output - what the program returns, what it gives us after performing the manipulations
So, OOP is really about turning that 'input' into 'objects' and then manipulating those objects and giving us the result.
I don't have a Computer Science degree, I actually studied Finance in school. The first time I was exposed to this idea of object-oriented programming, it made sense in the abstract. What really confused me though, was the way in which we create and manipulate objects from the real-world. How is this modeling of real-world ideas actually done...
For the next 5 minutes or so, we're going to create a simple Ruby program that models a bank account.
What we want to do is think about this idea of a bank account and then try and break it down into its primary moving pieces. These primary moving pieces are what we'll turn into objects and then manipulate in our program. There are a number of directions you can go with this, but to keep our example simple, the primary components of our bank account are going to be (1) the account itself, and (2) transactions within the account.
Ruby groups together similar objects and the code we write to interact with those objects in a structure called a Class. A class is actually an object itself, but we're not going to get into that now. We'll have two classes in our program then, an "Account" Class to model the bank account itself, and a "Transaction" class to model monetary transactions - a purchase of some type or a money transfer.
Classes primarily allow us to do two things, (1) create objects and (2) interact with or manipulate those objects.
Let's begin with the Transaction class. We start by defining the class:
class Transaction
end
Ruby gives us a method called new which we use to create new Transaction objects. A method is just a chunk of code that gives us some functionality. Often times we'll define our own methods, but in this case the standard Ruby library gives us new for free.
20:14:36 davemox@Daves-MacBook-Pro $ irb
2.0.0p353 :001 > load 'lib/transaction.rb'
=> true
2.0.0p353 :002 > Transaction.new
=> #<Transaction:0x007fea3b005ac8>
2.0.0p353 :003 > t = Transaction.new
=> #<Transaction:0x007fea3b006388>
2.0.0p353 :004 > t.class
=> Transaction
An object of a specific class is known as an instance of that class. Here we have a Transaction object, also known as an instance of class Transaction. Unfortunately, our new instance isn't very helpful. We don't know anything about the it. Ideally, we'd like to be able to see each Transaction's name, description, amount and purchase date. These externally facing facets of our object are known as its attributes.
We can give our Transaction object these attributes by declaring instance variables when we create the object. We do this by passing arguments to Transaction.new:
2.0.0p353 :005 > Transaction.new("Trampoline", "Jump around, jump around...", -435, "11/25/13")
ArgumentError: wrong number of arguments (4 for 0)
from (irb):5:in `initialize'
from (irb):5:in `new'
from (irb):5
from /Users/davemox/.rvm/rubies/ruby-2.0.0-p353/bin/irb:12:in `<main>'
Uh oh, that doesn't look good. Looking at the Error message, we see that our program did not expect 4 arguments to be passed. We also see a reference to the initialize method in the error. But we haven't declared an initialize method?? It appears nowhere in our file.
The initialize method is pretty special. When we call Transaction.new(argument1, argment2) with arguments, Ruby actually looks for an initialize method in our code and tries to pass this method the arguments. The job of the initialize method is to take those arguments that Transaction.new passes and turn them into instance variables which are then part of the object.
Let's define initialize in our program:
class Transaction
def initialize(name, description, amount, purchase_date)
@name = name
@description = description
@amount = amount
@purchase_date = purchase_date
end
end
Note we take each parameter and assign an instance variable, the words starting the '@' symbol. And, try to create our transaction object again:
2.0.0p353 :008 > Transaction.new("Trampoline", "Jump around, jump around...", -435, "11/25/13")
=> #<Transaction:0x007fea3a3c91b0 @name="Trampoline", @description="Jump around, jump around...", @amount=-435, @purchase_date="11/25/13">
2.0.0p353 :009 >
Success!!
Our happiness is short-lived though. Alas, we have no way of accessing these instance variables. Recall that I said classes allow us to do two things, create objects and also manipulate them? Well, in Ruby,we use instance methods to manipulate our objects. We can use instance methods to access the instance variables of our objects.
Let's define an instance method for each variable we want access to.
class Transaction
def initialize(name, description, amount, purchase_date)
@name = name
@description = description
@amount = amount
@purchase_date = purchase_date
end
def name
@name
end
def description
@description
end
def amount
@amount
end
def purchase_date
@purchase_date
end
end
It is very important to note that our instance variables are available to our instance methods. Instance variables are available to any instance method of our Instance class. And, it is also important to note that instance methods, can be called on any instance of the same class. Let's see how these methods work. Heading back over to IRB:
2.0.0p353 :018 > t = Transaction.new("Trampoline", "Jump around, jump around...", -435, "11/25/13")
=> #<Transaction:0x007fea3a3532f8 @name="Trampoline", @description="Jump around, jump around...", @amount=-435, @purchase_date="11/25/13">
2.0.0p353 :019 > t.amount
=> -435
2.0.0p353 :020 > t.name
=> "Trampoline"
2.0.0p353 :021 > t.description
=> "Jump around, jump around..."
2.0.0p353 :022 > t.purchase_date
=> "11/25/13"
2.0.0p353 :023 >
Yea buddy!! Now, we're cookin.
One more thing to note before we move on. Because declaring methods to access instance variables is so common, Ruby actually gives us a method to do just that.
We can replace the above with:
class Transaction
attr_reader :name, :description, :amount, :purchase_date
def initialize(name, description, amount, purchase_date)
@name = name
@description = description
@amount = amount
@purchase_date = purchase_date
end
end
Cool, so we've successfully modeled a Transaction, now let's model our Bank Account.
We'll start out with a Bank Account class.
class BankAccount
end
And now let's decide on what instance variables we want to declare and make available to our instance methods. Well, our bank account should have a name and an account number, so let's add those in.
class BankAccount
def initialize(owner)
@owner = owner
@account_number = rand(10**10)
end
end
It should also have some way of holding a bunch of transaction objects, after all, a bank account with no ability to track purchases is pretty useless.
class BankAccount
attr_accessor :transactions
attr_reader :owner, :account_number
def initialize(owner)
@owner = owner
@account_number = rand(10**10)
@transactions = []
end
end
Woah!! An instance of our BankAccount class can actually hold instance of our Transaction class?? You bet
Note that instance variables do not have to originate solely from arguments passed to initialize from new constructor. In this case, we assign the @transactions instance variable to point to an empty array. As we create transactions, we'll add those to the transactions array.
Remember our class definition from earlier? Classes primarily allow us to do two things, (1) create objects and (2) interact with or manipulate those objects. Let's declare some instance variables, which remember can be called on BankAccount objects, to add Transaction objects to our BankAccount object and also perform some basic calculations on those transactions.
First, let's declare a method to add Transaction objects to the @transactions array. An array by the way (rhyming!!) is simply an ordered collection of objects, any object you like! In this case it will be filled with Transaction objects.
def add_transaction(some_transaction)
@transactions << some_transaction
end
And now let's add instance methods to total number of transactions in our BankAccount as well as the current balance of the BankAccount.
NOTE: I'm kinda trailing off here. Will finish up the discussion of these last two methods tomorrow.