Ruby Object Oriented
Object-Oriented Ruby
Section titled “Object-Oriented Ruby”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.
Ruby Class Definition
Section titled “Ruby Class Definition”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 hereendDefining Ruby Objects (Instances)
Section titled “Defining Ruby Objects (Instances)”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.newbox2 = Box.newThe initialize Method (Constructor)
Section titled “The initialize Method (Constructor)”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 endend
# When you call Box.new(10, 20), the initialize method is invoked with w=10 and h=20.Instance Variables
Section titled “Instance Variables”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 endendAccessor 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}" endend
# Create an objectbox = Box.new(10, 20)box.display_dimensions
# Use accessor (getter) methodsputs "Initial width: #{box.width}"
# Use setter methodsbox.width = 30box.height = 50
puts "Updated width: #{box.width}"box.display_dimensionsWhen the above code is executed, it produces:
Box dimensions - Width: 10, Height: 20Initial width: 10Updated width: 30Box dimensions - Width: 30, Height: 50Manually 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 endend
box = Box.new(10, 20)box.width = 30 # Calls width=(30)puts box.height # Calls height()Instance Methods
Section titled “Instance Methods”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 endend
# Create an objectbox = Box.new(10, 20)
# Call instance methoda = box.areaputs "Area of the box is: #{a}"When executed, this produces:
Area of the box is: 200Class Methods and Class Variables
Section titled “Class Methods and Class Variables”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}" endend
# Create two objectsbox1 = Box.new(10, 20)box2 = Box.new(30, 100)
# Call class methodBox.print_countThis produces:
Box count is: 2The to_s Method
Section titled “The to_s Method”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 endend
# Create an objectbox = Box.new(10, 20)
# puts calls to_s automaticallyputs "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_methodwill fail, even from within the class).initializeis 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
selfor 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 endend
box = Box.new(10, 20)
# Call public methodarea = box.calculate_areaputs "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 thisWhen 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: 200Box 1 is larger than Box 2: trueClass Inheritance
Section titled “Class Inheritance”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 endend
# BigBox is a subclass of Boxclass 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 endend
# Create an object of the subclassbig_box = BigBox.new(100, 200)
# Call inherited method (via print_area)big_box.print_area
# Call inherited method directlyputs "Directly calling area: #{big_box.area}"This produces:
Big box area is: 20000Directly calling area: 20000Method Overriding
Section titled “Method Overriding”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}" endend
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}" endend
colored_box = ColoredBox.new(10, 20, "Red")puts colored_box.display_infoThis produces:
Box with width 10 and height 20, and color RedOperator Overloading
Section titled “Operator Overloading”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})" endend
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)Freezing Objects
Section titled “Freezing Objects”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 endend
app_config = Config.new("initial_value")puts "Initially frozen? #{app_config.frozen?}" # => false
app_config.setting = "new_value" # Modifiableputs "Setting after change: #{app_config.setting}"
app_config.freeze # Freeze the objectputs "Now frozen? #{app_config.frozen?}" # => true
begin app_config.setting = "another_value" # This will raise an errorrescue 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? falseSetting after change: new_valueNow frozen? trueError: can't modify frozen ConfigClass Constants
Section titled “Class Constants”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}." endend
box = Box.newbox.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: CardboardClass constants are inherited by subclasses and can be overridden (though this also generates a warning and is generally discouraged).
Creating Objects with allocate
Section titled “Creating Objects with allocate”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 endend
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}" # => nilputs "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}"endThis produces output similar to:
Creating box1 with new:Initialize called!Area of box1: 200
Creating box2 with allocate:box2 width: nilbox2 height: nilError calculating area for box2: undefined method `*' for nil:NilClassClass Information (Introspection)
Section titled “Class Information (Introspection)”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 classendWhen this file is loaded, it produces:
Inside class definition:Type of self: ClassName of self: MyCoolClassSelf inspect: MyCoolClassThis 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.