Skip to content

Ruby Object Oriented

Ruby is a pure object-oriented programming (OOP) language. In Ruby, everything is an object, including primitive data types like numbers, strings, and even true, false, and nil. Classes themselves are objects (instances of the Class class). This chapter explores the core OOP concepts in Ruby.

A class is a blueprint for creating objects. It encapsulates data (attributes or instance variables) and methods (behaviors) that operate on that data.

Defining a class creates a new data type. It specifies what an object of that class will consist of (its state) and what operations can be performed on it (its behavior).

A class definition starts with the class keyword, followed by the class name (which must begin with a capital letter), and ends with the end keyword. By convention, multi-word class names use CamelCase (e.g., CustomerAccount).

class Box
# code for the class goes here
end

Objects are instances of classes. You create an object (instantiate a class) using the new method, which is a class method available to all classes.

box1 = Box.new
box2 = Box.new

The initialize method is a special instance method in Ruby. It acts as a constructor and is automatically called when you create a new object using ClassName.new. It’s used to set up the initial state of an object, typically by assigning values to instance variables. It can accept arguments.

class Box
def initialize(width, height)
@width = width # @width is an instance variable
@height = height # @height is an instance variable
end
end
# When you call Box.new(10, 20), the initialize method is invoked with w=10 and h=20.

Instance variables start with an @ symbol (e.g., @width). They hold data specific to each instance (object) of a class. Each object maintains its own copy of instance variables. Within the class, they are accessed directly using @variable_name. To access or modify them from outside the class, accessor and setter methods are typically used.

class Box
def initialize(w, h)
# Assign instance variables
@width = w
@height = h
end
end

Accessor and Setter Methods (Getters and Setters)

Section titled “Accessor and Setter Methods (Getters and Setters)”

To allow controlled access to an object’s instance variables from outside the class, you define accessor (getter) methods to read their values and setter (mutator) methods to change them. Ruby provides convenient shortcuts for creating these common methods:

  • attr_reader :variable_name_symbol: Creates a getter method.
  • attr_writer :variable_name_symbol: Creates a setter method.
  • attr_accessor :variable_name_symbol: Creates both a getter and a setter method.

Example using attr_accessor:

#!/usr/bin/env ruby
class Box
# Creates getter (width, height) and setter (width=, height=) methods
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
end
def display_dimensions
puts "Box dimensions - Width: #{@width}, Height: #{@height}"
end
end
# Create an object
box = Box.new(10, 20)
box.display_dimensions
# Use accessor (getter) methods
puts "Initial width: #{box.width}"
# Use setter methods
box.width = 30
box.height = 50
puts "Updated width: #{box.width}"
box.display_dimensions

When the above code is executed, it produces:

Box dimensions - Width: 10, Height: 20
Initial width: 10
Updated width: 30
Box dimensions - Width: 30, Height: 50

Manually defining getters and setters (less common now due to attr_* helpers):

#!/usr/bin/env ruby
class Box
def initialize(w, h)
@width = w
@height = h
end
# Getter for width
def width
@width
end
# Setter for width
def width=(value)
@width = value
end
# Getter for height
def height
@height
end
# Setter for height
def height=(value)
@height = value
end
end
box = Box.new(10, 20)
box.width = 30 # Calls width=(30)
puts box.height # Calls height()

Instance methods define the behavior of objects. They are defined using the def keyword within a class and operate on the data (instance variables) of the object they are called on. They are invoked using an instance of the class.

#!/usr/bin/env ruby
class Box
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
end
# Instance method to calculate area
def area
@width * @height
end
end
# Create an object
box = Box.new(10, 20)
# Call instance method
a = box.area
puts "Area of the box is: #{a}"

When executed, this produces:

Area of the box is: 200

Class variables (e.g., @@count) are shared among all instances of a class, the class itself, and its subclasses. They are prefixed with @@ and must be initialized within the class definition.

Class methods are defined by prefixing the method name with self. (e.g., def self.method_name) or ClassName. within the class definition. They are called on the class itself, not on instances, and typically operate on class variables or serve as factories for instances.

#!/usr/bin/env ruby
class Box
# Class variable
@@count = 0
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
@@count += 1
end
# Class method
def self.print_count
puts "Box count is: #{@@count}"
end
end
# Create two objects
box1 = Box.new(10, 20)
box2 = Box.new(30, 100)
# Call class method
Box.print_count

This produces:

Box count is: 2

The to_s instance method is used to return a string representation of an object. It’s automatically called by methods like puts or when an object is used in string interpolation. It’s good practice to define a custom to_s method for your classes to provide a meaningful string representation.

#!/usr/bin/env ruby
class Box
attr_reader :width, :height # only getters needed for this example
def initialize(w, h)
@width = w
@height = h
end
# Custom to_s method
def to_s
"(w:#{@width}, h:#{@height})" # String formatting of the object
end
end
# Create an object
box = Box.new(10, 20)
# puts calls to_s automatically
puts "String representation of box is: #{box}"

This produces:

String representation of box is: (w:10, h:20)

Access Control (public, private, protected)

Section titled “Access Control (public, private, protected)”

Ruby provides three levels of access control for instance methods. Access control does not apply to instance or class variables directly; access to them is typically managed via methods.

  • Public: Public methods can be called by anyone, from anywhere. Methods are public by default (except for initialize, which is implicitly private).
  • Private: Private methods can only be called by other instance methods within the same class definition, and only with an implicit receiver (self). They cannot be called with an explicit receiver (e.g., object.private_method will fail, even from within the class). initialize is always private.
  • Protected: Protected methods can be called by instance methods of the defining class and its subclasses. Unlike private methods, they can be called with an explicit receiver, provided that receiver is self or an instance of the same class or a subclass.

Access control keywords (public, private, protected) apply to methods defined after them in the class definition until another access control keyword or the end of the class is encountered. They can also take method names (as symbols) as arguments to set specific methods’ visibility.

#!/usr/bin/env ruby
class Box
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
end
# Public by default
def calculate_area
internal_width * internal_height # Calls private methods
end
# Instance method to demonstrate protected
def compare_area(other_box)
# Can call protected method on other_box if other_box is a Box or its subclass
other_box.display_protected_area # This would work for protected
# For this example, let's just use its public calculate_area
self.calculate_area > other_box.calculate_area
end
protected
def display_protected_area
puts "Protected: Area is #{internal_width * internal_height}"
end
private
# private getter methods (example, usually attr_reader would be public or protected)
def internal_width
@width
end
def internal_height
@height
end
end
box = Box.new(10, 20)
# Call public method
area = box.calculate_area
puts "Area of the box is: #{area}"
# Attempt to call private or protected methods (will cause errors)
begin
# box.internal_width # This would raise NoMethodError (private method called)
# box.display_protected_area # This would raise NoMethodError (protected method called)
rescue NoMethodError => e
puts "Error: #{e.message}"
end
box2 = Box.new(5,10)
puts "Box 1 is larger than Box 2: #{box.compare_area(box2)}"
# box.display_protected_area # Still can't call this directly from outside
# box2.display_protected_area # Nor this

When the above code is executed, the attempts to call private/protected methods directly from outside will result in NoMethodError. The public calculate_area works.

Area of the box is: 200
Box 1 is larger than Box 2: true

Inheritance is a fundamental OOP concept where a class (subclass or derived class) can inherit attributes and methods from another class (superclass or base class). This promotes code reuse and establishes an “is-a” relationship (e.g., a BigBox is a type of Box).

Ruby supports single inheritance (a class can inherit from only one superclass). However, Ruby achieves features similar to multiple inheritance through mixins using modules. A mixin allows a class to include methods from one or more modules.

To define a subclass, use the < symbol followed by the superclass name.

#!/usr/bin/env ruby
class Box # Superclass
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
end
def area
@width * @height
end
end
# BigBox is a subclass of Box
class BigBox < Box
# BigBox inherits initialize, width, height, and area from Box
# Add a new instance method specific to BigBox
def print_area
# @width and @height are accessible because they are instance variables
# and BigBox is a Box. If they were private methods in Box, direct access might differ.
# 'area' method is inherited and public.
puts "Big box area is: #{area}" # using inherited area method
end
end
# Create an object of the subclass
big_box = BigBox.new(100, 200)
# Call inherited method (via print_area)
big_box.print_area
# Call inherited method directly
puts "Directly calling area: #{big_box.area}"

This produces:

Big box area is: 20000
Directly calling area: 20000

A subclass can provide its own specific implementation of an instance method that is already defined in its superclass. This is called method overriding. The overridden method in the subclass must have the same name as the method in the superclass.

You can call the superclass’s version of the method from within the overriding method using the super keyword.

#!/usr/bin/env ruby
class Box
def initialize(w, h)
@width = w
@height = h
end
def display_info
"Box with width #{@width} and height #{@height}"
end
end
class ColoredBox < Box
attr_accessor :color
def initialize(w, h, color)
super(w, h) # Calls Box's initialize method
@color = color
end
# Override display_info
def display_info
# Call superclass's display_info and add more
"#{super}, and color #{@color}"
end
end
colored_box = ColoredBox.new(10, 20, "Red")
puts colored_box.display_info

This produces:

Box with width 10 and height 20, and color Red

Ruby allows you to redefine how standard operators (like +, -, *, []) behave for objects of your class. This is done by defining methods with names corresponding to the operators. For example, to overload the + operator, you define a method named +.

class Vector2D
attr_accessor :x, :y
def initialize(x, y)
@x, @y = x, y
end
# Overload + operator for vector addition
def +(other_vector)
# Ensure other_vector is also a Vector2D for type safety (optional, but good practice)
raise ArgumentError, "Can only add Vector2D objects" unless other_vector.is_a?(Vector2D)
Vector2D.new(@x + other_vector.x, @y + other_vector.y)
end
# Overload unary - operator to negate vector components
def -@
Vector2D.new(-@x, -@y)
end
# Overload * operator for scalar multiplication
def *(scalar)
raise ArgumentError, "Can only multiply by a Numeric" unless scalar.is_a?(Numeric)
Vector2D.new(@x * scalar, @y * scalar)
end
def to_s
"(#{x}, #{y})"
end
end
v1 = Vector2D.new(2, 3)
v2 = Vector2D.new(5, 1)
sum_vector = v1 + v2 # Calls v1.+(v2)
puts "v1 + v2 = #{sum_vector}" # Output: (7, 4)
negated_v1 = -v1 # Calls v1.-@()
puts "-v1 = #{negated_v1}" # Output: (-2, -3)
scaled_v1 = v1 * 3 # Calls v1.*(3)
puts "v1 * 3 = #{scaled_v1}" # Output: (6, 9)

The freeze method (available on all objects as it’s from Object) prevents an object from being modified. Once an object is frozen, attempting to change its instance variables or internal state will raise a FrozenError (or RuntimeError in older Rubies). You can check if an object is frozen using the frozen? method.

#!/usr/bin/env ruby
class Config
attr_accessor :setting
def initialize(val)
@setting = val
end
end
app_config = Config.new("initial_value")
puts "Initially frozen? #{app_config.frozen?}" # => false
app_config.setting = "new_value" # Modifiable
puts "Setting after change: #{app_config.setting}"
app_config.freeze # Freeze the object
puts "Now frozen? #{app_config.frozen?}" # => true
begin
app_config.setting = "another_value" # This will raise an error
rescue FrozenError => e
puts "Error: #{e.message}"
end
# Note: Freezing an object typically performs a shallow freeze.
# If an instance variable refers to another mutable object (e.g., an array),
# that other object itself is not frozen unless you explicitly freeze it too.
# For example:
# mutable_array = [1, 2]
# app_config.some_array = mutable_array
# app_config.freeze
# app_config.some_array << 3 # This would still modify mutable_array if it wasn't frozen.

This code demonstrates freezing an object:

Initially frozen? false
Setting after change: new_value
Now frozen? true
Error: can't modify frozen Config

Constants within a class are defined by assigning a value to a name starting with an uppercase letter. They are typically used for values that should not change. By convention, constant names are in ALL_CAPS_WITH_UNDERSCORES.

Constants are accessible from within the class using their name. From outside the class, they are accessed using the class name followed by the scope resolution operator :: (e.g., ClassName::CONSTANT_NAME). While Ruby issues a warning if you try to reassign a constant, it technically allows it (which is often a source of confusion).

#!/usr/bin/env ruby
class Box
BOX_COMPANY = "ACME Inc."
DEFAULT_MATERIAL = "Cardboard"
attr_reader :material
def initialize(material = DEFAULT_MATERIAL)
@material = material
end
def display_company
puts "This box is from #{BOX_COMPANY} made of #{@material}."
end
end
box = Box.new
box.display_company
custom_box = Box.new("Plastic")
custom_box.display_company
puts "Default material for all boxes: #{Box::DEFAULT_MATERIAL}"

This produces:

This box is from ACME Inc. made of Cardboard.
This box is from ACME Inc. made of Plastic.
Default material for all boxes: Cardboard

Class constants are inherited by subclasses and can be overridden (though this also generates a warning and is generally discouraged).

The allocate class method creates an instance of a class without calling its initialize method. The resulting object is uninitialized; its instance variables will be nil. This is rarely needed in typical application code and is generally used for more advanced scenarios, such as object deserialization or certain metaprogramming tasks.

#!/usr/bin/env ruby
class Box
attr_accessor :width, :height
def initialize(w, h)
puts "Initialize called!"
@width, @height = w, h
end
def area
# This will cause an error if width or height is nil
@width * @height
end
end
puts "Creating box1 with new:"
box1 = Box.new(10, 20)
puts "Area of box1: #{box1.area}"
puts "\nCreating box2 with allocate:"
box2 = Box.allocate # initialize is NOT called
puts "box2 width: #{box2.width.inspect}" # => nil
puts "box2 height: #{box2.height.inspect}" # => nil
begin
puts "Area of box2: #{box2.area}"
rescue NoMethodError => e
# Trying to call * on nil (e.g. nil * nil)
puts "Error calculating area for box2: #{e.message}"
end

This produces output similar to:

Creating box1 with new:
Initialize called!
Area of box1: 200
Creating box2 with allocate:
box2 width: nil
box2 height: nil
Error calculating area for box2: undefined method `*' for nil:NilClass

Inside a class definition, self refers to the class itself. You can use standard object methods on self to get information about the class.

#!/usr/bin/env ruby
class MyCoolClass
puts "Inside class definition:"
puts "Type of self: #{self.class}" # self is the class MyCoolClass, its class is Class
puts "Name of self: #{self.name}" # self.name gives the class name as a string
puts "Self inspect: #{self.inspect}" # Same as self.name for a class
end

When this file is loaded, it produces:

Inside class definition:
Type of self: Class
Name of self: MyCoolClass
Self inspect: MyCoolClass

This demonstrates that a class definition is executable code, and during its execution, the current object (self) is the class being defined. This allows for powerful metaprogramming capabilities in Ruby.