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