.new()
and def initialize()
attr_accessor
, attr_reader
, attr_writer
, or none of the aboveRuby is an object-oriented language. That means it's based on the idea that you'll build your application with objects in mind.
As you learned with OOJS, an object is a collection of related attributes (properties) and methods (behavior). You can think of an object as a little machine: it has displays you can read and buttons you can push.
When you write an object-oriented application, the idea is that you write the blueprints for these machines, and then you write a sequence of events your users can initiate in which these machines interact with each other.
Much of the code you see today will be very similar to what you encountered in last week's OOJS class. That's because classes are a concept that have been around for quite some time but only just introduced in Javascript (ECMAScript 2016/ES6). Use this pre-existing knowledge to your advantage in today's class!
We've talked quite a bit about object oriented programming as a paradigm, but we haven't talked much about how to break a problem down into object components.
"Monopoly is a game where players try to accumulate wealth through property ownership and money"
"Facebook is an application where users can post statuses and add friends."
Breaking your ideas down gives you a starting place for what those objects may be.
Spend 10 minutes working with a partner to come up with at least three types of objects that you might define when creating the following examples. Add your responses as a comment on this issue. We'll go over your answers as a class.
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 blueprint 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.
Aside: pry is a ruby gem that allows us to work with ruby code in an IRB (interactive ruby shell). It's similar to working in our developer console in a web browser with javascript.
$ touch app.rb
$ gem install pry # run this if 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"
Now let's generate some instances of this class...
alice = User.new
alice.set_name_to("alice")
puts alice.get_name
madhatter = User.new
madhatter.set_name_to("Mad Hatter")
puts madhatter.get_name
alice.greet
madhatter.greet
Is User
a(n)...
Is alice
a(n)...
User.greet
throws an error. alice.greet
works fine. So we can deduce that the greet
method can only be called on...
User
class?User
class itself?Thus, would it make sense to call greet
a(n)...
User.new
works fine. alice.new
throws an error. So we can deduce that the new
method can only be called on...
User
class?User
class itself?Thus, it would be make sense to call new
a(n)...
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 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"
alice = User.new
alice.greet
madhatter = User.new
madhatter.greet
puts alice
puts madhatter
initialize
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
harry = User.new("Harry", "Potter")
# I'm a new User named Harry Potter
# => #<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 becomes an instance variable and therefore available inside the entire instance
in which it was created.
class User
def initialize(firstname, lastname)
@firstname = firstname
@lastname = lastname
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
end
# pry
harry = User.new("Harry", "Potter")
# => #<User:0x007faf3903f670 @firstname="Harry", @lastname="Potter">
harry.full_name
# => "Harry Potter"
To get Harry's first name, we can't simply type harry.firstname
. To set Harry's first name, we can't simply type harry.firstname = "Harry"
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
harry = User.new("Harry", "Potter")
# => #<User:0x007faf3903f670 @firstname="Harry", @lastname="Potter">
puts harry.get_firstname
# "Harry"
harry.set_firstname("Ginny")
puts harry.get_firstname
# "Ginny"
Recall how we couldn't simply type Harry.firstname = "some other name"
the previous example.
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
harry = User.new("Harry", "Potter")
harry.firstname = "Ginny"
# This throws an error
harry.set_firstname("Ginny")
puts harry.get_firstname
# =>
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 :firstname, :lastname
def initialize(firstname, lastname)
@firstname = firstname
@lastname = lastname
end
def full_name
return "#{@firstname.capitalize} #{@lastname.capitalize}"
end
end
nayana = User.new("Nayana", "Davis")
nayana.firstname = "Nayana"
puts nayana.firstname
attr_accessor
is actually a shortcut that combines two other shortcutsattr_accessor
is attr_reader
combined with attr_writer
.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
hermione = User.new("Hermione", "Granger")
hermione.firstname
# => "Hermione"
hermione.lastname
# => Error!
hermione.firstname = "Ginny"
# => Error!
hermione.lastname = "Weasley"
hermione.full_name
# => "Hermione Weasley"
attr_reader
creates a getter method only. Trying to do hermione.firstname = "Ginny"
will fail.
attr_writer
creates a setter method only. Trying to do puts hermione.lastname
will fail.
attr_accessor
creates getters and setters.
You will most commonly use
attr_accessor
For the next exercise, clone down the repo linked below: https://github.com/ga-wdi-exercises/oop_monkey
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
harry = User.new("Harry", "Potter")
harry.count
# => 1
ron = User.new("Ron", "Weasley")
harry.count
# => 2
ron.count
# => 2
draco = User.new("Draco", "Malfoy")
harry.count
# => 3
ron.count
# => 3
draco.count
# => 3
But there's something weird going on here: note that we aren't counting the number of Rons, Harrys or Dracos. 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 harry.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
ginny = User.new("Ginny", "Weasley")
ginny.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
draco = User.new("Draco", "Malfoy")
# "Creating Draco"
luna = User.new("Luna", "Lovegood")
# "Creating Luna"
bellatrix = User.new("Bellatrix", "LeStrange")
# "Creating Bellatrix"
User.all
# => [#<User @firstname="Draco">, #<User @firstname="Luna">, #<User @firstname="Bellatrix">]
From Chris Pine's "Learn to Program - Second Edition": p 112, section 13.6
Make an OrangeTree class that has...
height
method that returns its height in feet
one_year_passes
method that, when called, ages the tree one year. Start the age at 0
.Test your code.
Test your code.
count_the_oranges
, which returns the number of oranges on the treeTest your code.
pick_an_orange
, which reduces the number of oranges by 1Test your code.
Create an OrangeTreeOrchard
class that manages multiple OrangeTrees
. It can...
sample_user.reset_password
)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 its initialize
methodattr_accessor
: a setting that allows you to directly "get" or "set" an instance variableBy 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
harry = User.new("Harry", "Potter", "Expecto Patronum")
# #<User @firstname="Harry" @password="munortaP otcepxE">
harry.encrypt("Expecto Patronum")
# 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.