Classes

Classes are one of the building blocks of object-oriented programming. Think of them as blueprints for creating objects. When we just throw variables and methods inside a Ruby file, it’s very loosely associated. However, when you encapsulate them into a class, you combine the state (instance variables) and behavior (methods) into a cohesive unit to which you could create objects. A few things to keep in mind about Ruby classes:

  • Classes in Ruby are constants.
  • The instance variable lives for the life of the object.
  • You could define your own “to_s” method to customize what you want to display when calling “puts.”
  • Objects are responsible for managing its own internal state. That’s why whether you use attr_accessor or attr_reader would be a design decision. You determine what state you want to expose.
  • Generally you want to keep your classes small so that it’s easier to read, reuse, and unit test. If you have too much code, that usually means you could break it up into another class, module, etc.

The benefit of objects is to encapsulate behavior and responsibilities. This way, objects could interact with each other and tell each other what to do. It’s important to keep in mind, objects are passed by reference in Ruby. So if you had the same object in two different arrays, they are actually the same object.

class Headphone
  attr_accessor :name, :brand, :version

  def initialize(name, brand, version)
    @name = name
    @brand = brand
    @version = version
  end
end

a = Headphone.new("Audeze", "LCD-4", "1.0")

array1 = [a]
array2 = [a]

array1[0].name = "Sony"

p array2[0]
# #<Headphone:0x007fd074844cb0 @name="Sony", @brand="LCD-4", @version="1.0">

Modules

Modules are used when your methods carry too much responsibility and you want offload some of it. You don’t want to use a class because these are methods/tasks that don’t have an initial state or require multiple instances. One good use of a Module is a database connector where you only need a single instance. They’re basically just a collection of related methods which could be used across classes and within other methods. One example of this is the Enumerable module. In many ways, modules are equivalent to having multiple inheritance.

Mixins

Modules provide the ability to mixin your methods across classes. The best way to think about it is in the context of “able” behaviors. For example if you have a few classes that are “Playable” you could include methods like pause, play, stop, next track, shuffle, etc. This could be shared with objects like Music or Videos. If you find yourself duplicated methods like play in both Music and Video classes, then it’s a sign you should mix it in as a module.

All you have to do to mixin is to place an “include [module name]” at the top of your class. From here all methods inside the module are instance methods of that class. When Ruby looks for methods, it first starts in the class, the the module, then the super class, etc. You could run [class].ancestors to see the chain.

One good practice is to use self.[attribute] inside the module instead of instance variables. That way you don’t require the class you’re mixing into to have the same instance variables. It’s better to depend on methods (which attr_accessor/reader/writer sets up real nice for us).

Namespaces

Modules also help prevent naming collisions, especially when you’re going to bundle your library into a gem where it’ll be shared with the world. It basically does this but nesting classes within the module declaration. This way, it forces you to call the class via the module name. For example: HotelsApp::Rooms, EscapeRoom::Rooms, and Hospital::Rooms. There’s no confusion as to which class you’re calling. You could have classes with the same name in different modules and everyone’s happy.

Inheritance

This concept is applied when a class “is a kind of” another class.

  • Dog is a kind of Animal
  • Van is a kind of Automobile
  • Automatic is a kind of Watch
  • Pinot Noir is a kind of Wine

So the parent the child is inheriting from will have qualities and characteristics that are shared. In essence, they share common code that could be reused. They also have unique traits and behaviors and could override their parent’s methods (polymorphism). The super method allows children to call parent methods or even initialize the child object.

class Automobile
  attr_reader :wheels, :cylinders, :seats, :tank_size

  def initialize(wheels=4, cylinders=4, seats=5, tank_size=16)
    @wheels = wheels
    @cylinders = cylinders
    @seats = seats
    @tank_size = tank_size
  end

  def rev
    puts "REV"
  end
end

class Tesla < Automobile
  def initialize(wheels, cylinders, seats, tank_size, top_speed)
    # use parent class to initialize instance variables
    super(wheels, cylinders, seats, tank_size)
    @top_speed = top_speed
  end

  # calls the rev method in the parent
  # the logic might change in the parent so you don't want to duplicate here
  def rev
    3.times { super }
  end
end

j = Automobile.new
p j

a = Tesla.new(5,6,7,8,160)
p a
a.rev

# #<Automobile:0x007fa4ca0340c8 @wheels=4, @cylinders=4, @seats=5, @tank_size=16>
# #<Tesla:0x007fa4ca02f758 @wheels=5, @cylinders=6, @seats=7, @tank_size=8, @top_speed=160>
# REV
# REV
# REV