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
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
> 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:
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
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
> 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:
Lesson 31 of 37
Last updated August 7, 2022