Skip to content

Instantly share code, notes, and snippets.

@stacietaylorcima
Last active January 11, 2018 05:50
Show Gist options
  • Save stacietaylorcima/e5aa177aa33a5df3ba0a1537d320bc4a to your computer and use it in GitHub Desktop.
Save stacietaylorcima/e5aa177aa33a5df3ba0a1537d320bc4a to your computer and use it in GitHub Desktop.

Ruby: Classes 2

Concept Description
Class methods Behaviors that the class needs to implement that should not be direct responsibilities of the instances it creates. In a BankAccount class, this could be a method that calculates the average balance across all BankAccount objects
Constants Variables that contain data that will not change and is shared amongst the instances of a class.
self self refers to that which the method was called on. If used in a method signature/header before the method name, it makes the method a class method. If used inside an instance method, it refers to the instance the method was called on
Inheritance Refers to the concept of extending a class by defining a subclass. The subclass inherits the attributes and behaviors of the parent class while keeping the ability to either override parent methods or create their own
super super Can be used inside of a subclass method overriding a parent method. It will use the implementation for that method in the parent class
Module Can be used to share behaviors amongst classes where classical inheritance wouldn't be appropriate. If inheritance is based on what the object is, a module allows us to inherit based on what the object does

Objectives

  • Understand how to use constants.
  • Understand the use of class methods.
  • Demonstrate the use of modules.
  • Understand the concept of self.
  • Understand the concept of inheritance.
  • Demonstrate the use of super.

Class Constant

  • There are times when we want to share an attribute with every member of the class.
  • For example, let's think of a RedKoopaTroopa class:
class RedKoopaTroopa
  attr_accessor :name, :shell_color

  def initialize(name)
    @name, @shell_color = name, "Red"
  end
end

koopa_bob = RedKoopaTroopa.new("Bob")

p "koopa's name: #{koopa_bob.name}, shell color: #{koopa_bob.shell_color}"
#=> "koopa's name: Bob, shell color: Red"
  • In the code above, each object of the RedKoopaTroopa class has an instance variable storing the value "Red" for shell_color.
    • Even though we don't have to pass it to our constructor, it's still an attribute for every object.
    • We can improve this by using a class constant.
  • Class Constant:
    • A class constant allows us to store a variable at the class level so ever object of that class can use it.
    • We can then add a method to the interface that returns the value of the constant.
class RedKoopaTroopa
  attr_accessor :name

  # Class constant
  SHELL_COLOR = "Red"

  def initialize(name)
    @name = name
  end

  # method to expose the constant
  def shell_color
    SHELL_COLOR
  end
end

koopa_bob = RedKoopaTroopa.new("Bob")

p "koopa's name: #{koopa_bob.name}, shell color: #{koopa_bob.shell_color}"
#=> "koopa's name: Bob, shell color: Red"
  • The code above results in the same thing, but now there is only one copy of the variable shared amongst all members of the class instead of each object having their own copy.
  • Remember that constants are called that way because they do not change.

Class Methods & Class Variables

  • Class Methods:
    • So far, we've seen a lot of instance methods. These are methods that can be called on an object of the class.
    • There are also class methods which can only be called on the class.
  • Class Variables:
    • The @@ is how we begin our class variables.
    • These are special variables that belong to the class.
    • Unlike class constants, their value can be changed.
  • Let's keep a tally of the number of koopas still walking around:
class RedKoopaTroopa
  attr_accessor :name

  # Class constant
  SHELL_COLOR = "Red"

  # Class variable
  @@number_of_koopas = 0

  def initialize(name)
    @name = name
    @@number_of_koopas += 1
  end

  # method to expose the constant
  def shell_color
    SHELL_COLOR
  end

  # class method
  def self.count
    @@number_of_koopas
  end
end

self

  • self in Ruby is a special thing concerned with scope.

    • Depending on where you use it, the value of self changes.
    • self refers to the thing that the method was called on.
    • That thing can be either an instance or a class.
  • Define a Class Method:

    • To define a class method, we simply add self. to the method name in the method signature and it makes it a class method that can only be called on the class.
  • Get Number of Koopa's Created:

    • Everytime we call RedKoopaTroopa.new to create a new koopa, we use the increment operator to increase the count of koopas by
    • To get the number of koopas created, call RedKoopaTroopa.count and it will return the value of the class variable keeping track of koopas.
    • NOTE: the class methods cannot be called on instances of a class.
  def self.count
    @@number_of_koopas
  end
  • Make a Koopa Apologize to Another:
    • As we said, self cares about scope.
    • Let's create an instance method that we can call to make a koopa apologize to another koopa when it bumps into it.
    • Then let's create 2 koopas and bump them into each other.
# complete class for reference
class RedKoopaTroopa
  attr_accessor :name

  # Class constant
  SHELL_COLOR = "Red"

  # Class variable
  @@number_of_koopas = 0

  # Constructor
  def initialize(name)
    @name = name
    @@number_of_koopas += 1
  end

  # class methods
  def self.count
    @@number_of_koopas
  end

  # instance methods
  def shell_color
    SHELL_COLOR
  end

  def bump_into(another_koopa)
    "Sorry about that, #{another_koopa.name}! I'm #{self.name}, by the way!"
  end
end

#create koopas
koopa_jane = RedKoopaTroopa.new("Jane")
koopa_bob = RedKoopaTroopa.new("Bob")

RedKoopaTroopa.count #=> 2

koopa_bob.bump_into(koopa_jane) #=> "Sorry about that, Jane! I'm Bob, by the way!"
  • When using self inside an instance method, it still refers to the thing the method was called on.
  • In this case, ‘self’ can be used implicitly by omitting it in the definition.
  • The code below results in the same output, but here we are using the implicit receiver. Some people prefer to use the explicit receiver to improve readability.
# code above
def bump_into(another_koopa)
  "sorry about that, #{another_koopa.name}! I'm #{name}, by the way!"
end
# code below

Inheritance

  • Our code must be written in a way that allows us to reuse it as often as possible. One of the tools that will help us accomplish this is Inheritance.
  • Inheritance can be classical (based on what the class is) or prototypal (based on what the class does). We'll focus on classical inheritance.
  • Inheritance allows us to establish a relationship between classes. Let's look at how to implement inheritance in Ruby:
lass KoopaTroopa
  attr_accessor :name, :in_shell

  @@number_of_koopas = 0

  def initialize(name, in_shell = false)
    @name, @in_shell = name, in_shell
    @@number_of_koopas += 1
  end

  def self.count
    @@number_of_koopas
  end

  def bump_into(another_koopa)
    "Sorry about that, #{another_koopa.name}! I'm #{name}, by the way!"
  end
end
  • We're defining a new base class for our koopas.
  • We're moving the class and instance methods that we need out of the RedKoopaTroopa into the base class as well.
class RedKoopaTroopa < KoopaTroopa
  SHELL_COLOR = "Red"

  def shell_color
    SHELL_COLOR
  end
end

class GreenKoopaTroopa < KoopaTroopa
  SHELL_COLOR = "Green"

  def shell_color
    SHELL_COLOR
  end
end
  • Our RedKoopaTroopa and GreenKoopaTroopa classes are now inheriting from the KoopaTroopa class.
  • We establish that relationship by using the following recipe:
class Childclass < Parentclass
end
  • Calling Childclass.superclass returns the name of the parent class
  • The class to the left of < will inherit all the attributes and methods of the class to the right.
  • This means that even though we aren't explicitly defining attributes on the GreenKoopaTroopa and RedKoopaTroopa classes, they have them thanks to their parent class.
    • Here's how it works:
greenie = GreenKoopaTroopa.new("Greenie")
reddie = RedKoopaTroopa.new("Reddie")

greenie.name #=> "Greenie"
GreenKoopaTroopa.count #=> 2
greenie.shell_color #=> "Green"
reddie.shell_color #=> "Red"

greenie.bump_into(reddie) #=> "Sorry about that, Reddie! I'm Greenie, by the way!"

Prototype Chain

  • Prototype Chain on Koopas:

    • Explanation: If we call greenie.name, your class (GreenKoopaTroopa) receives the message, "Do you have a method called name?"
      • If the answer is no, it moves over to the parent class and asks the class if it has a method with that name. If it doesn't, it goes all the way up the chain (Object, BasicObject, nil) to look for it. If it's not found, a NoMethodError is raised.
      • If the answer is yes, the method is executed.
    • Explanation: In our case, name is referring to the getter method that returns the value of name.
      • Since it's not defined in GreenKoopaTroopa, KoopaTroopa (the parent) is asked.
      • KoopaTroopa has the method so it is called.
      • The same thing happens with our initialize method.
      • When we call .new on any of the child classes (also called subclasses), using the parent's initialize method to create an instance of the subclass.
      • This is the reason why calling GreenKoopaTroopa.count returned 2 even though we only created one green koopa.
  • Prototype Chain on BankAccount:

    • Let's look at an example with a base class called BankAccount with methods for depositing/withdrawing amounts and a SavingsAccount with a little extra:
class BankAccount
  attr_reader :balance

  def initialize(balance)
    @balance = balance
  end

  def deposit(amount)
    @balance += amount if amount >= 0
  end

  def withdraw(amount)
    @balance -= amount if @balance >= amount
  end
end

class SavingsAccount < BankAccount
  attr_reader :number_of_withdrawals
  APY = 0.0017

  def initialize(balance)
    super(balance) # calls the parent method
    @number_of_withdrawals = 0 # then continues here
  end

  def end_of_month_closeout
    if @balance > 0
      interest_gained = (@balance * APY) / 12
      @balance += interest_gained
    end
    @number_of_withdrawals = 0
  end
end
  • We've defined our SavingsAccount to inherit from BankAccount.

  • Our BankAccount class has a balance attribute, and basic implementations of deposit and withdraw methods.

  • Our SavingsAccount class has an attribute to track the number of withdrawals the account has made.

  • It also has a method that is called end_of_month_closeout that is:

    • currently calculating the interest generated by this saving account
    • resetting the number of withdrawals made
    • adding the interest to the balance
    • We can also use this method to do things like generate statements, etc.
  • super:

    • We are reusing the parent's version of the method we called super in, but then we are adding some additional behavior needed by the SavingsAccount class. This demonstrates reusability.
      • The super keyword is telling our method to call the parent class version of the method it was called in. In this case, the initialize method of the BankAccount class is called and passes in the balance.
      • The result is an object of the SavingsAccount class created; but it then continues executing the initialize method of the SavingsAccount class, which then sets the number_of_withdrawals attribute to 0.
  • is_a?

    • All child class instances are also members of the parent class, but the opposite is not true!
      • The is_a? method returns true if the object called on is an instance of the class passed in as an argument.
      • The is_a? method is defined in the Object class, and almost every class is a direct descendant of the Object class. Even the classes that that we can define.
my_account = SavingsAccount.new(100) # creates a SavingsAccount object with $100
my_account.is_a?(SavingsAccount) # returns true
my_account.is_a?(BankAccount) # also returns true

account = BankAccount.new(100) # creates a BankAccount object with $100
account.is_a?(BankAccount) # returns true
account.is_a?(SavingsAccount) # returns false
  • Polymorphism:
    • We can use inheritance to implement polymorphism.
    • This is when we can call a method of the parent class without concerning ourselves with what child class we are dealing with.
    • Since all objects are objects of the super class, they will all respond to the method.
    • Let's say that we have a method that takes a collection of the accounts and logs all the balances:
def report_balances(accounts)
  accounts.each do |account|
    Analytics.report(account.balance)
  end
end
  • This method won't need to check the class that each account belongs to. Since all the accounts are BankAccount objects, they'll all respond to the balance getter method for the balance attribute.

Modules

  • Modules allow us to encapsulate behavior that our classes can extend.
    • Sometimes our classes need to implement the same behavior but they are not related at all.
    • It’s important to maximize the reusability of a chunk of code in our applications.
    • This allows us to define behavior in a module that our classes can use without tying that behavior to a class.
    • Let's think about a game with a character who has an inventory:
class Player
  attr_accessor :name, :inventory

  def initialize(name)
    @name, @inventory = name, []
  end

  def add_to_inventory(item)
    inventory << item
  end

  def remove_from_inventory(item)
    inventory.delete(item)
  end
end
  • We've defined our Player class to have a name (String) and an inventory (Array) attribute.
  • We've defined basic methods to add/remove items from the inventory.
  • There are chests in the game that contain loot. Let's write that:
class Chest
  attr_accessor :inventory

  def initialize(inventory = [])
    @inventory = inventory
  end

  def add_to_inventory(item)
    inventory << item
  end

  def remove_from_inventory(item)
    inventory.delete(item)
  end
end
  • Both classes need to implement an inventory system, and because the inventory related methods are part of the interface of each class, we're repeating ourselves.
  • Also, if we want to establish an inventory limit or item weight restriction, we'd have to write that in more than one place.
  • Even though using inheritance here would work, it is not correct because a chest isn't a player.
  • Using a module in here would be a good way to abstract that inventory behavior into it's own thing.
  • We would then say that our Player and Chest classes implement inventory.
  • Look at how we write that:
# our module encapsulating inventory behavior
module Inventory
  def add_to_inventory(item)
    @inventory << item
  end

  def remove_from_inventory(item)
    @inventory.delete(item)
  end
end

class Player
  include Inventory # makes module available to the class
  attr_accessor :name, :inventory

  def initialize(name)
    @name, @inventory = name, []
  end

end

class Chest
  include Inventory

  attr_accessor :inventory

  def initialize(inventory)
    @inventory = inventory
  end
end
  • Where to Write Modules:

    • If we are defining our module in the same file as our class, the module definition should be placed above the classes that are implementing it.
    • Otherwise, the Ruby interpreter will raise an error because we are requiring a module that has not yet being defined.
  • include:

    • The include keyword tells Ruby to reference the module called Inventory (in this case).
    • The methods defined in our module are now "mixed into" our class, and can be called as if they were instance methods on the objects of the class loading the module.
    • This is why the module methods have access to the instance variables defined in the class.
hero = Player.new("Chrono")
hero.add_to_inventory("Masamune")
hero.inventory # returns  ["Masamune"]
@stacietaylorcima
Copy link
Author

Exercises - Modules

  • Define a module called Flying that we'll use to implement flying on our Koopa class as well as any other class that needs flying instances. It will have a method called fly that returns the string "[NAME] is flying", where [NAME] refers to the name of the flying object.

Starting Code:

class KoopaTroopa
  attr_accessor :name, :in_shell

  @@number_of_koopas = 0

  def initialize(name, in_shell = false)
    @name, @in_shell = name, in_shell
    @@number_of_koopas += 1
  end

  def self.count
    @@number_of_koopas
  end

  def bump_into(another_koopa)
    "Sorry about that, #{another_koopa.name}! I'm #{name}, by the way!"
  end
end

Add the following module to the top of the code: (it must preceed any classes that include it)

module Flying
  def fly
    "#{@name} is flying!"
  end
end

@stacietaylorcima
Copy link
Author

stacietaylorcima commented Jan 11, 2018

Inheritance

  • Define a class called CheckingAccount that inherits from BankAccount.

  • Write it so that if more than 3 withdrawals are made, it charges a $5 fee for each transaction afterwards.

  • Use a constant called MAX_FREE_WITHDRAWALS to set the maximum number of withdrawals before the fee kicks in.

  • Define a method called get_free_withdrawal_limit to expose the value of the constant.

  • If there aren't enough funds to cover the fee and the limit has been reached, it should not make a successful withdrawal.

  • The class should also have a transfer method that uses an account to move money from, and an amount to transfer as arguments and allows you to withdraw amount from account and deposit amount into the account the method was called on.

  • Don't implement a method to reset the number of transactions.

Starting Code:

class BankAccount
  attr_reader :balance

  def initialize(balance)
    @balance = balance
  end

  def display_balance
    @balance
  end

  def deposit(amount)
    @balance += amount if amount > 0
  end

  def withdraw(amount)
    @balance -= amount if amount <= @balance
  end
end

Solution:

class CheckingAccount < BankAccount #Inheritance!
  attr_reader :number_of_withdrawals
  MAX_FREE_WITHDRAWALS = 3

  def initialize(balance)
    super(balance)
    @number_of_withdrawals = 0
  end

  def get_free_withdrawal_limit
    MAX_FREE_WITHDRAWALS
  end

  def withdraw(amount)
    @number_of_withdrawals += 1
    if number_of_withdrawals > get_free_withdrawal_limit
      super(amount + 5)
    else
      super(amount)
    end 
  end

  def transfer(account, amount)
    account.withdraw(amount) && self.deposit(amount)
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment