Classes

Blueprints for creating objects. They define attributes and methods that objects instantiated from the class will have. Instance variables are prefixed with @. Class variables are prefixed with @@

class Person
 @@species = "homo sapien" # class var
  attr_accessor :name, :age # provides getter and setter methods
 
  def initialize(name, age)
    @name = name # instance var
    @age = age
  end
 
  class << self
    def species # exposes class variable
      @@species
    end
  end
end
 
person = Person.new("Alice", 25)
puts "I am a #{Person.species}"

Methods

Different types of methods include: instance methods, class methods, and module methods.

class Person
  attr_accessor :name
 
  def initialize(name)
    @name = name
  end
 
  # instance methods operate on the instance
  def greet
    "Hi, I'm #{@name}."
  end
 
  class << self # class methods operate on the class itself
    def info # self refers to the class
      "This is a Person class."
    end
  end
end
 
person = Person.new("Alice")
puts person.greet # Hi, I'm Alice.
puts Person.info # This is a Person class.

Modules (mixins)

A way to organize and namespace our classes

Modules can be mixed into classes using include (as instance method), extend (as class method), or prepend (as instance methods that override local instance methods)

prepend methods technically don’t override local methods, they just appear earlier in the method lookup

module Flyable
  def fly # module method
    "I can fly!"
  end
end
 
class Bird
  include Flyable # module method will operate on instance
end
 
class Plane
  extend Flyable # module method will operate on class
end
 
class Kite
  prepend Flyable
 
  def fly
    "I can fly if there is wind"
  end
 
  class << self
    def fly
      "Kites can fly!"
    end
  end
end
 
bird = Bird.new
kite = Kite.new
 
puts bird.fly       # I can fly!
puts Plane.fly      # I can fly!
puts kite.fly       # I can fly!
puts Kite.fly       # Kites can fly!

Singleton Methods

A method on a specific instance

kite = Kite.new
 
def kite.crash
  "We are stuck in a tree!"
end
 
puts kite.crash # we are stuck in a tree
puts Kite.crash # NoMethodError

Self

class MyClass
  puts self  # MyClass
 
  def instance_method
    puts self  # instance of MyClass
  end
 
  def self.class_method
    puts self  # MyClass
  end
end

Inheritance

A class can inherit from another using <. super calls the parent method with the same name

class Animal # parent class
  attr_reader :species
 
  def initialize(species)
    @species = species
  end
end
 
class Dog < Animal # child class
  attr_reader :name
 
  def initialize(name)
    super("Dog") # calls Animal's initialize with provided args
    # super() calls with no args
    # super calls with current args (name)
    @name = name
  end
end
 
dog = Dog.new("Buddy")
puts "#{dog.species} #{dog.name}" # Dog Buddy

Polymorphism/Overriding

A subclass can redefine methods from its parent class

class Animal
  def speak
    "Some generic sound"
  end
end
 
class Cat < Animal
  def speak
    "Meow!"
  end
end
 
animals = [Animal.new, Cat.new]
animals.each do |animal|
  puts animal.speak # Some generic sound, Meow!
end

Encapsulation

Methods may be:

  • Public: default, accessible anywhere
  • Private: can only be called within the class
  • Protected: can be accessed by instances of the same class or its subclasses
class Person
  def initialize(name, secret)
    @name = name
    @secret = secret
  end
 
  def introduce
    "Hi, I'm #{@name}."
  end
 
  private # methods below this are private
 
  def reveal_secret
    @secret
  end
 
end
 
person = Person.new("Alice", "I love Ruby.")
puts person.introduce # Hi, I'm Alice.
puts person.reveal_secret  # errors

Reusing Code

Blocks

Chunks of code that can be passed to methods

# implicit block
def greet_with_block(name)
  yield(name) if block_given? # yields to the block, if block is given
end
 
# explicit block
def greet_with_block(name, &block) # &block represents the proc object
  block.call(name) if block
end
 
greet_with_block("Alice") { |name| puts "Hello, #{name}!" } # Hello, Alice!

Procs and Lambdas

Procs are objects that hold blocks of code

add = proc { |a, b| a + b }
puts add.call(2, 3) # 5
puts add.call(2) # TypeError 2 + nil

Proc shorthand (:&)

arr.map(&:to_s) # same as arr.map { |item| item.to_s }

Lambdas are Procs with strict argument checking

add = ->(a, b) { a + b }
puts add.call(2, 3) # 5
puts add.call(2) # errors, wrong number of arguments

Manipulating Built ins

Built in methods can be overridden to customize behavior

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end
 
  def to_s # string representation
    "Name: #{@name}, Age: #{@age}"
  end
 
  def inspect # used for debugging
    "#<Person: #{@name}, #{@age}>"
  end
end
 
person = Person.new("Alice", 30)
puts person.to_s     # Name: Alice, Age: 30
puts person.inspect  # #<Person: Alice, 30>

Metaprogramming

Metaprogramming refers to generating or modifying code at runtime

Runtime: when the program is executing, as opposed to when it is written or statically analyzed

instance_eval

Opens instance context. Then we can access, add, and modify instance variables

class Person
  def initialize(name)
    @name = name
  end
end
 
person = Person.new("Alice")
 
person.instance_eval do
  puts @name  # Alice
  @age = 25   # sets instance variable
end

class_eval

Opens class context. Then we can access, add, and modify class variables and methods

Person.class_eval do
  def greet
    "Hi, I'm #{@name}"
  end
end
 
puts person.greet  # Hi, I'm Alice

define_method

Create dynamic methods

class User
  [:name, :email, :age].each do |attr|
    define_method(attr) do # create getter method
      instance_variable_get("@#{attr}")
    end
 
    define_method("#{attr}=") do |value| # create setter
      instance_variable_set("@#{attr}", value)
    end
  end
end
 
user = User.new
user.name = "Bob"
puts user.name  # Bob

send and public_send

Call methods dynamically

class Calculator
  def add(a, b)
    a + b
  end
 
  private
 
  def secret
    "secret!"
  end
end
 
calc = Calculator.new
 
# we can dynamically
# decide which method to call
method_name = :add
puts calc.send(method_name, 2, 3)  # 5
 
puts calc.send(:secret)            # secret!, ignores private
puts calc.public_send(:secret)     # NoMethodError, respects private