- Explain the relationship between
.new()
anddef initialize()
- Distinguish whether a piece of data is best-suited to being stored in a local, instance, or class variable
- Explain whether given data is best-suited to having its accessibility defined by
attr_accessor
,attr_reader
,attr_writer
, or none of the above - Describe the relationship of
attr_
and "getter" and "setter" methods - Properly define instance and class variables
- List two ways of defining class methods
Ruby is an object-oriented language. Everything in Ruby is an object. The langugage allows us to write code in an object-oriented way.
...I just defined Object Oriented by saying it's oriented like an object. Let's break it down!
What is an object? It is simply a model of a real world thing. Think of a desk, a student, a car...what methods (functionality) and properties (nouns) do these real world objects have? A chair has legs, can be broken, thrown, burned, introduced into a WWF ring, etc.
How do we describe an object oriented language? To be object-oriented, the language must have 4 major principles:
- Encapsulation
- Data Abstraction
- Polymorphism
- Inheritence
We will define these as we see them today.
Are there other ways to write code besides being object oriented? Yes!
You do not have to use object oriented programming - you could just write functions, variables, and tie them all together at the moment you notice you need some new functionality. Or, you could use another programming paradigm and write in a functional or procedural style. The reason programmers choose an object oriented approach is to reduce complexity and cut down on dependency between different parts of your program. Writing in an object-oriented way is a practice that you must pay attention to. If you do, you will find it makes your code less complex, less error prone, more readable and a joy to work with.
So, as you learned with OOJS, an object is a collection of related attributes (aka properties) and methods (aka behavior). You can think of an object as a machine: it has displays you can read, settings you can configure, and buttons you can press.
When you write an object-oriented application, the idea is that you write the blueprints for these machines (classes), then you create machines from these blueprints (instances of the class). You also write how the machines interact with each other.
Much of the code you see today will be very similar to what you encountered in our OOJS class. That's because classes are a concept that have been around for quite some time but only just introduced in Javascript. Use this pre-existing knowledge to your advantage in today's class!
One of the main benefits of writing in an object-oriented style is breaking down a complex problem into manageable components.
Let's try breaking down a few things found in the real world into bite-sized chunks that we programmers can use:
- Ingredients
- Size
- Shape
- Percentage eaten
"Tic Tac Toe is a game where players try to get three squares in a row."
- Game
- Players
- Squares
- Game Pieces (ex: chocolate chip cookes and 'x's)
"Facebook is an app where users can post statuses and add friends."
- Users
- Statuses
- Friends
Putting your idea in a nutshell gives you a starting place for what those objects may be.
Spend three minutes working with a partner to come up with at least three types of objects that you might define when creating the following examples.
- Amazon (The website, or...the jungle)
- A Homework Grading App
- An Attendance Taking App
- Uber
A helpful approach might be to take the "nouns" involved in the application and say they are objects.
Say that we have a car. Each of us has a mental model of what a car is: it has four wheels, runs on gas, has a steering wheel that allows us to drive it, etc. This is like a class. Now, when we see a car in front of us, this is like an instance, it's an actual object in front of us. Each object has its blueprint, and is an instance of that blueprint or class.
A class is a blueprint from which objects are made. In javascript we used classes, which operate very similarly to classes in Ruby. Each object made from a class is an instance of that class. Each instance of a class is an object.
Let's define a User
class. We'll be using binding.pry
to test our code.
$ touch app.rb
$ gem install pry # run this is you haven't installed pry yet
require "pry"
class User
def set_name_to(some_string)
@name = some_string
end
def get_name
return @name
end
def greet
puts "Hi! My name is #{@name}!"
end
end
binding.pry
puts "end of file"
What about this Ruby class looks similar to a Javascript class?
- The
class
keyword- The class contains methods
Now let's generate some instances of this class...
alice = User.new
alice.set_name_to("Alice")
puts alice.get_name
bob = User.new
bob.set_name_to("Bob")
puts bob.get_name
alice.greet
bob.greet
Is User
a(n)...
- class?
- instance?
Is alice
a(n)...
- class?
- instance?
User.greet
throws an error. alice.greet
works fine. So we can deduce that the greet
method can only be called on...
- instances of the
User
class - the
User
class itself
Thus, would it make sense to call greet
a(n)...
- "instance method"?
- "class method"?
User.new
works fine. alice.new
throws an error. So we can deduce that the new
method can only be called on...
- instances of the
User
class - the
User
class itself
Thus, it would be make sense to call new
a(n)...
- "instance method"
- "class method"
`class User` works fine. `class user` throws an error. What's a rule we can deduce about classes from this?
Class names must begin with a capital letter. This is not optional.
`class UserName` works fine. `class User Name` throws an error. What's a rule we can deduce about classes from this?
Class names must not contain spaces.
What was the purpose of a constructor function in Javascript classes?
To initialize any properties we want a class instance to have when it is created.
Ruby classes have an equivalent to Javascript constructors: the initialize
method!
require 'pry'
class User
def initialize
puts "I'm a new User"
end
def greet
puts "Nice to meet you!"
end
end
binding.pry
puts "end of file"
alice = User.new
alice.greet
bob = User.new
bob.greet
User.new
puts alice
puts bob
What can we conclude about the relationship of `def initialize` and `.new`? (Hint: it serves the same purpose as Javascript's constructor function)
The
initialize
method is run every time.new
is called.
How is this different from other User methods we've seen?
initialize
andnew
aren't the same word. Going by what else we've seen, we'd expect to seeUser.initialize
correspond todef initialize
. (Under the hood,.new
is a separate class method that calls theinitialize
instance method.)
initialize
is a special method in its relationship to .new
, but otherwise it behaves like any other method. This means you can pass arguments to it (again, just like Javascript's constructor
)...
require "pry"
class User
def initialize(firstname, lastname)
puts "I'm a new User named #{firstname} #{lastname}"
end
end
binding.pry
puts "end of file"
# pry
juan = User.new("Juan", "Juanson")
# I'm a new User named Juan Juanson
# => #<User:0x007f96f312b240>
Let's create a method that prints the full name of the user.
In Ruby, normal variables are available only inside the method in which they were created.
If you put an @
before the variable's name, it's available inside the entire instance
in which it was created.
This is an instance variable.
class User
def initialize(firstname, lastname)
@firstname = firstname
@lastname = lastname
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
end
# pry
juan = User.new("Juan", "Juanson")
# => #<User:0x007faf3903f670 @firstname="Juan", @lastname="Juanson">
juan.full_name
# => "Juan Juanson"
To get Juan's first name, we can't simply type juan.firstname
. To set Juan's first name, we can't simply type juan.firstname = "Jorge"
The only things available outside an instance are its methods. @firstname
is a property, not a method. We can't access data inside of an instance unless it contains methods that let us do so.
To make a property "gettable" and "settable", we need to create "getter" and "setter" methods for it.
class User
def initialize(firstname, lastname)
@firstname = firstname
@lastname = lastname
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
def get_firstname
return @firstname
end
def set_firstname(firstname)
@firstname = firstname
end
end
# pry
juan = User.new("Juan", "Juanson")
# => #<User:0x007faf3903f670 @firstname="Juan", @lastname="Juanson">
puts juan.get_firstname
# "Juan"
juan.set_firstname("Jorge")
puts juan.get_firstname
# "Jorge"
Recall how we couldn't simply type juan.firstname="some other name"
in a prior example.
class User
def get_name
return @name
end
def set_name(some_string)
@name = some_string
end
end
alice = User.new
alice.name = "Alice"
# This throws an error
alice.set_name("Alice")
puts alice.get_name
If only there were a way to define a class so that we don't have to define a getter and setter method for every single property...
class User
attr_accessor :name
end
alice = User.new
alice.name = "Alice"
puts alice.name
These have the same result, so we can deduce that `attr_accessor` is a shortcut that does what?
It creates getter and setter methods for the
name
instance variable.
attr_reader
makes an attribute readable, attr_writer
makes an attribute writeable. attr_accessor
makes an attribute both readable AND writeable.
To illustrate the difference between attr_reader
and attr_writer
, let's have a look at the code below.
class User
attr_reader :firstname
attr_writer :lastname
def initialize(firstname, lastname)
@firstname = firstname
@lastname = lastname
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
end
juan = User.new("Juan", "Juanson")
juan.firstname
# => "Juan"
juan.lastname
# => Error!
juan.firstname = "Jorge"
# => Error!
juan.lastname = "Anderson"
juan.full_name
# => "Juan Anderson"
attr_reader
creates a getter method only. Trying to do juan.firstname = "Jorge"
will fail.
attr_writer
creates a setter method only. Trying to do puts juan.lastname
will fail.
attr_accessor
creates getters and setters.
For the next exercise, clone down the repo linked below: https://github.com/ga-wdi-exercises/oop_monkey
- Spend 10 minutes, and as a pod:
- Investigate what other kinds of variables we can use in Ruby?
- How many variable types are there in Ruby?
- What are the pros and cons of each kind of variable?
Let's come up with a way of keeping track of how many users have been created total...
class User
attr_accessor :firstname, :lastname
@@all = 0
def count
return @@all
end
def initialize(firstname, lastname)
@firstname = firstname
@lastname = lastname
@@all += 1
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
end
juan = User.new("Juan", "Juanson")
juan.count
# => 1
jorge = User.new("Jorge", "Jorgeson")
juan.count
# => 2
jorge.count
# => 2
steve = User.new("Steve", "Steveson")
juan.count
# => 3
jorge.count
# => 3
steve.count
# => 3
But there's something weird going on here: note that we aren't counting the number of Steves, Jorges or Juans. Think about what .count
might be returning. More on this in a moment!
A variable name beginning with @@
is a class variable. Every instance of a class has the same value for this variable. It cannot be accessed with attr_accessor
. You have to actually create a method to access it.
A method name beginning with the class name is a class method. It is attached to the class itself, rather than to instances. There are also methods you call on User
itself. So far we've only seen .new
.It would make more sense if, in order to retrieve the total number of users, we ran User.count
instead of steve.count
...
class User
attr_accessor :firstname, :lastname
@@all = 0
def initialize(firstname, lastname)
@firstname = firstname
@lastname = lastname
@@all += 1
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
# You could also define this as `def self.count`, where self represents the class
def User.count
return @@all
end
end
juan = User.new("Juan", "Juanson")
juan.count
# => Error!
User.count
# => 1
self
is a special variable that contains the current instance of an object (like this
in Javascript). It's how the object refers to itself.
self
has another context as well: def self.all
Here, self
refers to class User
. What does this mean? It means that the method .all
is called on the class User
, much like .new
, and is therefore a class method.
class User
attr_accessor :firstname, :lastname
@@all = []
def initialize(firstname, lastname)
@firstname = firstname
@lastname = lastname
# here, `self` refers to the current instance
puts "Creating #{self.firstname}"
@@all.push(self)
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
# Can also be written as `def User.all`
# here, `self` refers to the class
def self.all
return @@all
end
end
juan = User.new("Juan", "Juanson")
# "Creating Juan"
jorge = User.new("Jorge", "Jorgeson")
# "Creating Jorge"
steve = User.new("Steve", "Steveson")
# "Creating Steve"
User.all
# => [#<User @firstname="Juan">, #<User @firstname="Jorge">, #<User @firstname="Steve">]
From Chris Pine's "Learn to Program": p 133, section 13.6
Make an OrangeTree class that has...
- a
height
method that returns its height in feet- it's initial value should be determined by some input
- hint: you don't necessarily have to define the method
- a
one_year_passes
method that, when called, ages the tree one year. Start the age at0
.
Test your code.
- Each year the tree grows taller by one foot
- After 50 years the tree should "die" (its height goes to 0)
Test your code.
- After the first 5 years, the tree bears 20 oranges
- You should be able to
count_the_oranges
, which returns the number of oranges on the tree
Test your code.
- You should be able to
pick_an_orange
, which reduces the number of oranges by 1 - Ensure that your tree cannot have negative oranges
- Ensure that after each year your tree has 20 total oranges again
Test your code.
- The number of oranges the tree bears each year is equal to 20 plus the age of the tree
Create an OrangeTreeOrchard
class that manages multiple OrangeTrees
. It can...
- Age all the trees by one year
- Pick and count all the fruit
- Calculate average height and fruit of all orange trees
- 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
- Class: a blueprint for objects
- Instance: an object that is created using a class
- Instance Variable: a property that is particular to an instance
- Class Variable: a property that is accessible by all instances of a class
- Instance Method: a method that can be called by an instance of a class (e.g.,
sample_user.reset_password
) - Class Method: a method that can be called by a class (e.g.,
User.list_user
) initialize
: a class method that, when triggered, creates an instance and assigns initial properties.new
: a class method that, when called, triggers itsinitialize
methodattr_accessor
: a setting that allows you to directly "get" or "set" an instance variable
- Draw a picture of a machine, real or imaginary, that has inputs (buttons, switches, keypads...) and displays (dials, lights, screens...). Label what they do.
- Most machines have internal gauges or memories that help it make decisions: temperature monitors, voltage monitors, hard disks, and so on. These are visible only inside the machine: whoever's using the machine can't see them. Draw two of these on your machine and label them.
By default all instance and class methods are public, except for def initialize
which is private. This means they're visible to other objects. An analogy: they're functions that have their own buttons on the outside of the machine, like a car's turn signal.
There may be methods that all other objects don't need to know about.
class User
attr_accessor :firstname, :lastname
@@all = []
def initialize(firstname, lastname, password)
@firstname = firstname
@lastname = lastname
@password = encrypt(password)
@@all.push(self)
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
def User.all
return @@all
end
private
def encrypt(input)
return input.reverse
end
end
juan = User.new("Juan", "Juanson", "wombat")
# #<User @firstname="Juan" @password="tabmow">
juan.encrypt("wombat")
# Error! Private method `encrypt`
Putting private
in front of methods means they can be used inside the object, but are not available outside it. An analogy: they're functions that do not have their own buttons on the outside of the machine, like a car's air filter.
private
is useful mostly for keeping things organized. Consider jQuery: It's already cluttered enough, with all these methods like .fadeOut
and .css
. It has lots of other methods hidden inside it that we don't really need to know about.
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.
- Variables cheat sheet
- Other exercises
- Screencasts