Lesson Wednesday

So far we’ve been building small projects. In the real world, however, it’s very likely that we’ll work with much larger code bases. While some development agencies might focus on building smaller projects for clients, even these projects will be much larger and more complex than the simple Sinatra applications that we’re currently building.

Imagine we have a project where users can both do mathematical operations with specific shapes and also draw shapes. We’re part of a development team writing the code that involves mathematical operations with various shapes. Meanwhile, another team is working on code that allows users to actually draw shapes.

Now imagine both teams add a Rectangle class. What will happen to our code? Let’s take a look using a simple example. Go ahead and open pry or irb in the terminal and input the following code:

class Rectangle
  def initialize(length, width)
    @length = length
    @width = width
  end
end

class Rectangle
  def initialize(length, width, color, line_width, line_color)
    @length = length
    @width = width
    @color = color
    @line_width = line_width
    @line_color = line_color
  end
end

Now try to create a Rectangle with just length and width properties:

> rectangle = Rectangle.new(5, 7)
=> ArgumentError: wrong number of arguments (given 2, expected 5)

Ouch! What happened? We defined the Rectangle class as having a length and a width and then we closed the class.

When we call the Rectangle class again, we reopen the Rectangle class. This is not a new class. This is the class we’ve already defined. However, our new initialize() method overwrites the original initialize() method. That’s not good at all.

We could always rename one of our classes but that’s not always a good solution. Imagine we're working on a gem or library that many other developers use in their projects. Should we try to anticipate what other developers will name their classes? Or should those developers have to check every gem they’re working with to make sure that none of the classes in their applications clash with classes in the gems? That would lead to a lot of frustration and broken code.

Fortunately, we can use namespacing to solve this problem. Namespacing, which we briefly discussed in the last course section, is a computer science term for encapsulating code inside a container. In our case, this means storing our classes inside a Ruby module. Here’s how we can namespace the classes above:

module Shape
  class Rectangle
    def initialize(length, width)
      @length = length
      @width = width
    end
  end
end

module Draw
  class Rectangle
    def initialize(length, width, color, line_width, line_color)
      @length = length
      @width = width
      @color = color
      @line_width = line_width
      @line_color = line_color
    end
  end
end

Now we have two separate modules: Shape and Draw. Each has its own Rectangle class; these two Rectangle classes are not related.

Let’s try this out in IRB. Add the code above (after first exiting and reopening the REPL to clear any previous code). Then try out the following:

rectangle = Rectangle.new()
NameError: uninitialized constant Rectangle
rectangle = Shape::Rectangle.new(5, 4)
=> #<Shape::Rectangle:0x007fe781277638 @length=5, @width=4>
rectangle2 = Draw::Rectangle.new(4, 4, "red", 3, "black")
=> #<Draw::Rectangle:0x007fe7816cbf40 @color="red", @length=4, @line_color="black", @line_width=3, @width=4>

We aren’t able to create a rectangle with just Rectangle.new(). If we do, we’ll get an uninitialized constant error. Instead, we have to reference the module and then the class inside it. We do this by connecting the module and class with double colons ::. Shape::Rectangle refers to the Rectangle class inside the Shape module while Draw::Rectangle refers to the Rectangle class inside the Draw module.

This illustrates an important part of Ruby. Classes and modules are really just constants with methods attached to them. We can actually nest as many modules, classes, and constants as we like. Here’s an example:

module House
 module Bedroom
   module Bed
     class Sleep
       HOURS_OF_SLEEP = 8
     end  
   end  
 end  
end

We’d access the Sleep class using :: to connect each nested module until we reach Sleep. In fact, we can take this one step further and access the constant that we’ve created inside the Sleep class: HOURS_OF_SLEEP.

> House::Bedroom::Bed::Sleep::HOURS_OF_SLEEP
=> 8

As long as an object in Ruby is a constant, Ruby can look it up using ::. It doesn’t matter if it’s a module, class, or an actual constant in our application.

Normally we wouldn’t want to nest modules to the extent that we do in the example above, but Ruby can handle deeply nested modules if necessary.

Practice nesting your classes inside of modules during this course section. You won't be expected to nest your code inside a module for this section's independent project but you are encouraged to explore modules in your classwork. While modules may not seem necessary for smaller projects, learning how to use them will prepare you for reading code in larger projects — and also for working on those larger projects as well.

Namespacing: Encapsulating code inside a container. We use modules to namespace in Ruby. Here's an example module:

module Shape
  class Rectangle
    ...
  end
end

To access this Rectangle class, we'd do the following: Shape::Rectangle.

Lesson 31 of 37
Last updated August 7, 2022