AKA OOP: There it is
- Define Object-Oriented Programming, and its benefits
- Define and differentiate between classes and objects
- Create a Ruby class with an initialize method
- Instantiate an object from a class and interact with it
- Use
binding.pry
to play with code live - Explain the difference between local and instance variables
- Write setter and getter methods
- Describe the use of
attr_reader
/attr_writer
/attr_accessor
- Write methods to define an interface to the class' behaviors
- Describe how OOP supports encapsulation and abstraction
- Object
- Class
- Property
- Attribute
- Method
def initialize
.new
attr_reader
,attr_writer
,attr_accessor
def method_name=
@variable_name
- Instance method
- Class method
binding.pry
Object-oriented programming is the idea that our programs consist mainly of objects, which have attributes (aka properties), and methods (aka behavior).
It may be easier to figure out what this means by looking at something that's not OOP. Check out this old Javascript that Robin wrote many moons ago.
It's not DRY at all. There are 4 functions with
address
in the name. There are 9 functions withamount
in the name, and each one begins with the exact same 2 lines of code.
What I was going for with those amount
functions is some kind of shipping calculator. But a calculator comes in a tidy little package: everything's inside a nice plastic container.
Having all these near-identical functions just hanging out in one big script file is like, instead of having a calculator, having a microchip for multiplication, a microchip for division, a microchip for addition, and a microchip for whatever else tossed together in a cardboard box, along with all the shipping labels and the postage scale.
Plus, you need to actually be able to get data into and out of the microchips, so you'd need to give each one its own display screen and keypad.
This works, but it's ugly and inefficient. It would be much easier if the microchips were all in that nice plastic container sharing the same screen and keypad.
With this old code, it would be much easier if all the amount
functions were in their own "plastic case", and we only had to write those two lines of code they share once.
An "object" in real life is a thing that has attributes and can do stuff. An object in programming is the same thing. The "stuff" that it can "do" are called methods.
Here are some real-world examples:
- Bank Account
- attributes: balance, transactions, owner
- methods: deposit, withdraw, close_account
- Person
- attributes: first_name, last_name, hunger_level
- methods: speak, run, eat
- Squad
- attributes: name, instructor, students, size
- methods: add_student, remove_student, meet, high_five, dance
Students should model a car, listing attributes and methods it may have.
Example Model: (don't peek!)
- Car
- attributes: make, model, year, color, speed, milage, num_doors, driver
- methods: turn_on, turn_off, change_gear, accelerate, brake
In JS, objects are somewhat like ruby hashes, in that they are key-value pairs, but JS objects are more powerful than a hash, in that JS objects can have methods, while ruby hashes can't.
Ruby objects are like JS Objects, only we can't access their properties/ attributes directly (we have to call methods on the object instead.)
Ruby also has Classes, which help us build objects, while JS Objects have constructor functions (which we'll cover in unit 3).
It's very common (in the world and in our programs), that we have many objects that share the same list of properties, and the same methods. E.g. there are lots of people who each have all the attributes and methods of a person.
Classes are like blueprints that define the generic list of of properties and methods, and can be used to build individual objects of that class. The objects each have their own unique values for properties. In Ruby, it might look something like this:
# assuming we have a `Person` class
class Person
# define properties and methods, more on this later
end
bob = Person.new("Bob", "Ross", 5)
kanye = Person.new("Kanye", "West",10)
bob.first_name # returns "Bob"
bob.hunger_level # returns 5
bob.eat("Banana") # hunger_level drops to 1
bob.hunger_level # returns 1
kanye.first_name # returns "Kanye"
kanye.hunger_level # returns 10
kanye.eat("Chipotle Burrito") # hunger_level drops to 0
kanye.hunger_level # returns 0
If we require 'pry'
in our program, we can add binding.pry
to any line to
pause and get a pry REPL at that point in our code. We can then play with our
code, see what the value of in-scope variables are, etc.
Ex:
scratch.rb
:
require 'pry'
a = 5
binding.pry
b = 10
in the terminal
:
$ ruby scratch.rb
1: require 'pry'
2: a = 5
3: binding.pry
=> 4: b = 10
[1] pry(main)> a
=> 5
[2] pry(main)> b
=> nil
[3] pry(main)>
There must be some functioning code after binding.pry
. If all that comes after it is white space and comments, it won't work. Even putting foo = "bar"
after it will cause it to function.
Classes are define with the class
keyword:
# person1.rb
class Person
end
Pure ruby classes often have an initialize
method, which gets run when we
create new
instances of that class.
# person2.rb
class Person
def initialize()
puts("new person created")
end
end
bob = Person.new # "new person created"
Often, the initialize method is used to set some/all instance variables of an
instance. Instance variables are somewhat different, each instance of a class
can have unique values for that variable. Instance variables start with an
at-sign, like so @first_name
.
# person3.rb
class Person
def initialize(initial_name)
@name = initial_name
end
def introduce
puts "Hello, I'm #{@name}"
end
end
me = Person.new("Adam Bray")
jesse = Person.new("Jesse")
me.introduce # prints "Hello, I'm Adam Bray"
jesse.introduce # prints "Hello, I'm Jesse Shawl"
We can't directly access instance variables of an object. We can only call methods. If we want to access or modify (get or set) properties of an object, we need to create methods to do so, often called getters and setters.
# person4.rb
class Person
def initialize(initial_name, initial_hunger_level)
@name = initial_name
@hunger_level = initial_hunger_level
end
def introduce
puts "Hello, I'm #{@name}"
end
# GETTER
def first_name
return @first_name
end
def hunger_level
return @hunger_level
end
# SETTERS
def name=(new_name)
@name = new_name
end
def hunger_level=(new_hunger_level)
if new_hunger_level < 0
@hunger_level = 0
else
@hunger_level = new_hunger_level
end
end
end
me = Person.new("Adam Bray", 10)
# Using Getters
me.name # returns "Adam Bray"
me.hunger_level # returns 10
# Using / Testing Setters
me.name = "Adam Bray, Esq." # changes name
me.name # returns "Adam Bray, Esq."
me.hunger_level = 5 # changes hunger level
me.hunger_level # returns 5
me.hunger_level = -8 # changes hunger level, according to rules
me.hunger_level # returns 0
Clone this exercise and follow the instructions in the readme.
Since these getters and setters are so common, ruby gives us shortcuts to create them for us:
attr_reader :hunger_level
- creates a getterattr_writer :name
- creates a setterattr_accessor :name
- creates a getter & a setter
These are used inside of a class (customarily at the very top).
# person5.rb
# functionally identical to the previous example, much less code
class Person
attr_accessor :name
attr_reader :hunger_level
def initialize(initial_name, initial_hunger_level)
@name = initial_name
@hunger_level = initial_hunger_
end
def introduce
puts "Hello, I'm #{@name}"
end
# Custom setter for hunger_level
def hunger_level=(new_hunger_level)
if new_hunger_level < 0
@hunger_level = 0
else
@hunger_level = new_hunger_level
end
end
end
Objects help us build programs that model how we tend to think about the world. Instead of a bunch of variables and functions (procedural style), we can group relevant data and functions into objects, and think about them as individual, self-contained units. This grouping of properties (data) and methods is called encapsulation.
This is especially important as our programs get more and more complex. We can't keep all the code (and what it does) in our head at once. Instead, we often want to think just a portion of the code.
Objects help us organize and think about our programs. If I'm looking at code for a Squad object, and I see it has associated people, and those people can dance when the squad dances, I don't need to think about or see all the code related to a person dancing. I can just think at a high level "ok, when a squad dances, all it's associated people dance". This is a form of abstraction... I don't need to think about the details, just what's happening at a high-level.
One side effect of encapsulation (grouping data and methods into objects) is that these objects can be in control of their data. This usually means ensuring consistency of their data.
Consider the bank account example... I might define a bank account object
such that you can't directly change it's balance. Instead, you have to use the
withdrawl
and deposit
methods. Those methods are the interface to the
account, and they can enforce rules for consistency, such as "balance can't be
less than zero".
If our objects are well-designed, then they interact with each other in well-defined ways. This allows us to refactor (rewrite) any object, and it should not impact (cause bugs) in other areas of our programs.
Clone this exercise and follow the instructions in the readme.
- Create a Ruby class for a student, initialized with a name and an age.
- Write a getter for name and age, and a setter for name only
- Create a new student and demonstrate using all the methods
- Explain the difference between local and instance variables