Overview
By the end of the chapter, you will be able to implement modules within the Ruby object model; add instance methods by including a module into a class; add class methods by extending a class with a module; create a namespace with a module; distinguish between prepending modules into classes and including and extending them and use modules to address multiple inheritance in Ruby.
In the previous chapter, we learned about the basics of object-oriented programming using Ruby. We learned that classes serve as templates for objects. We also learned that classes can also serve as templates for other classes by using the mechanism of inheritance. However, there may be situations where we might have to share code among different classes that don't really fit into an inheritance architecture. For example, we could be designing a reality simulator. In the previous chapter, we talked about how cars have four wheels, bicycles have two wheels, and boats have no wheels, but they still fall under the "Vehicles" class. Imagine that we had previously been tasked with modeling houses or places to live, which we can easily do using classes. Now we are tasked with modeling a mobile home or RV, which serves as both a vehicle and a home.
In other object-oriented languages, this problem is solved with a concept known as "multiple inheritance". For instance, in C++, a class could inherit from more than one base class. Ruby does not support multiple inheritance. Instead, Ruby solves this code reusability problem using the concept of modules.
Modules provide a way to conveniently wrap code in a way that can be shared among many other pieces of code. Modules can be included, extended, and prepended into other code. Modules can also serve as a way to namespace code. The nuances of each of these approaches require a bit more discussion of the Ruby object model. We'll be talking about the Ruby object model. This will give us a foundation for understanding how modules work so we can then learn about extend and prepend and how and when to use them. We will also study the mixin characteristic of modules. The idea of mixins essentially refers to the property of multiple modules being used by a class to improve code functionality and provide multiple inheritance in Ruby.
Let's begin with the include functionality of modules.
Modules, in their simplest definition, are a way to wrap code into a bundle so the code can be reused within other code without needing to be duplicated.
This definition sounds very similar to that of a Ruby class. Specifically, what makes a Ruby module distinct is that it cannot be instantiated into an object like a class. It can, however, be included in a class so the methods and variables defined in the module are accessible by the class. This refers to the mixin property of modules. Also, the methods and variables in a class can also be accessible to the code in the module. Essentially, when a module is included in a class, Ruby treats that code as if it were written right into that class. All the previous concepts we learned about classes and inheritance still apply to the module code when called.
For instance, if you call a module method from inside a class, that module method calls super and it will call the super class method of the class that the module was included in. This will be made clearer through the following example.
Let's assume we have a User class in which every user has a postal address. It can be realized as shown in the following code block:
class User
attr_accessor :address_line1, :address_line2, :city, :state, :postal_code, :country
def mailing_label
label = []
label << address_line1
label << address_line2
label << "#{city}, #{state} #{postal_code}"
label << country
label.join(" ")
end
end
Now, say we need to add the concept of buildings to our application. Buildings should also have addresses associated with them. We don't want to repeat ourselves and yet we still want to make sure that our User class and Building class both have this same functionality. We could theoretically do this with inheritance, but it doesn't really make sense to subclass the User class and Building class from an Address class as they aren't "types" of addresses. Instead, we will wrap this functionality in a module and include it in both classes.
Look at the following code:
module Address
attr_accessor :address_line1, :address_line2, :city, :state, :postal_code, :country
def mailing_label
label = []
label << @address_line1
label << @address_line2
label << "#{@city}, #{@state} #{@postal_code}"
label << @country
label.join(" ")
end
end
class User
include Address
end
class Building
include Address
end
As we can see, modules are declared using the module keyword, and they are included in classes using the include keyword. Defining methods is done similarly as they are inside a class definition.
The preceding code is using instance variables in the module to demonstrate a point but could also use the accessor methods created by attr_accessor.
This is a great way of sharing code. It is a clean way to reuse code. If any new models are necessary in the future and they need an address, we basically get it for free by simply including the module. This is the power of Ruby and object-oriented programming in action.
Let's dive deeper into how this works by working with some objects.
Consider the following code example:
u = User.new
b = Building.new
u.address_line1 = "123 Main Street"
b.address_line1 = "987 Broadway"
puts u.address_line1
puts b.address_line1
puts u.instance_variable_get("@address_line1")
puts u.instance_variable_get("@address_line1").object_id
puts b.instance_variable_get("@address_line1")
puts b.instance_variable_get("@address_line1").object_id
The output would show up as follows:
There is an important point in the preceding output. While the module is the same code and, in essence, referencing an instance variable with the same name, it is, indeed, a completely different object instance. This is demonstrated by grabbing the instance variable and outputting the object_id attribute of that object. The object_id attribute on any object is a unique ID maintained by Ruby. You can think of this as Ruby copying and pasting the module code into the class that we included the module in. So, any variables and code that are included are actually separate across different classes that include them.
In this exercise, we will extend the Service class with a module called Logger and take a look at how the inclusion of the module modifies the inner workings of our application.
Note
The Logger module is a tool used for debugging in Ruby. We will be looking at the Logger module in more detail in Chapter 8, Debugging with Ruby.
The following steps will help you complete the exercise:
module Logger
def log_message(level, message)
File.open("ServiceLogs.txt", "a") do |f|
f.write "#{level}: #{message} "
end
end
end
class Service
include Logger
def stop_service(service_name)
log_message :info, "Stopping service: #{service_name}"
sleep 3
log_message :info, "The service: #{service_name} was stopped!"
end
def start_service(service_name)
log_message :inf, "Starting the service: #{service_name}"
sleep 2
log_message :info, "The service: #{service_name} was started!"
end
end
TestService = Service.new
TestService.stop_service("Windows Update")
TestService.start_service("Windows Update")
The contents of the ServiceLogs.txt file should be as follows:
Thus, we have successfully used the Logger module to generate a service update on an application.
When we include a module in a class, we are basically copying the instance methods into a class. This is a very important point in understanding modules. include brings in the methods of a module into a class as instance methods.Because they are brought in as instance methods, all of the concepts that we learned about in the previous chapter about object-oriented programming and inheritance will apply to these methods.
Consider the following code block:
inheritancewithmodulemethods.rb
1 module Address
2 attr_accessor :address_line1, :address_line2, :city, :state,:postal_code, :country
3
4 def region
5 return nil if country.nil? || country == ""
6 case country
7 when "United States", "Canada", "Mexico"
8 "North America"
9 else
10 "Global"
11 end
12 end
13 end
14
Here, we've amended our Address module to include a region method. We've also created a Department class and subclassed User to create an Employee class that has a Department attribute. The Employee class has implemented its own region method and is delegating region to the department. If the department does not have a region, it calls super, which will, in effect, call the original Address class's method, which was included in the User class. Let's see this on the console:
e = Employee.new
e.region
e.country = "Mexico"
e.region
e.department = Department.new
e.region
e.department.country = "England"
e.region
The output would be as follows:
Our subclass has overridden a method defined in the module and is able to call super to that method if necessary.
The "copying and pasting" that's happening occurs at runtime, so the order of the inclusion of modules is important. Let's examine a case in which two modules have a method called email. One module returns a formatted email address, and the other sends an email:
module EmailFormatter
def email
"#{first_name}.#{last_name}@#{domain}"
end
end
module EmailSender
def email(msg, sender, recipient)
# contrived implementation for now
puts "Delivering email to #{recipient} from #{sender} with message: #{msg}"
end
end
class User
attr_accessor :first_name, :last_name, :domain
include EmailFormatter
include EmailSender
end
u = User.new
u.first_name = "John"
u.last_name = "Smith"
u.domain = "example.com"
puts u.email
The output will be as follows:
Here, we can see that the implementation from the EmailSender module is being called. Let's reverse it. Please restart IRB for this to work properly:
class User
attr_accessor :first_name, :last_name, :domain
include EmailSender
include EmailFormatter
end
u = User.new
u.first_name = "John"
u.last_name = "Smith"
u.domain = "example.com"
puts u.email
The output would be as follows:
And now the implementation we expected is being called. The second module would have done better to call its send_email method instead of just the generic email name.
Note
Avoid using methods names that are generic and may be used in other modules or classes. This will help to avoid name conflicts, especially if the module you are writing is intended to be used for a large application or will be publicly released.
Inclusion ordering is also important if the module contains class methods that can be called at the class level. You won't be able to call a class-level method that the module provides if the module has not yet been included. We'll take a look at this in the next section, which discusses adding class methods with modules using extend.
In the previous section, we learned about including methods from a module for instances of a class using the include keyword. Modules can also be used to add class methods to a class. This can be accomplished by using the extend keyword.
Consider the following example:
module HelperMethods
def attributes_for_json
[:id, :name, :created_at]
end
end
class User
extend HelperMethods
end
class Company
extend HelperMethods
end
irb(main):014:0> User.attributes_for_json
=> [:id, :name, :created_at]
irb(main):015:0> Company.attributes_for_json
=> [:id, :name, :created_at]
Here, we've defined a module called HelperMethods, which defines a single method called attributes_for_json. The intention is that these are a common set of attributes that will be used to convert objects into JSON. Because these attributes are global, in the sense that they apply to all objects, this method should be defined as a class method.
You can see, though, that in the module, the method is just defined as a straightforward method. There is no self. that precedes it. When the module is extended inside of a class, all of the methods that are defined as basic methods get extended into the class, meaning that they are added as class methods.
When working with modules, it's important to consider how the methods are defined inside the module. This information, along with how you want the methods to be defined, will inform whether you include the module or extend it.
There is nothing stopping you from including the preceding module as follows (restart IRB for this chapter):
class User
include HelperMethods
end
class Company
include HelperMethods
end
However, now these methods are defined as instance methods:
irb(main):013:0> User.attributes_for_json
Traceback (most recent call last):
2: from /Users/peter/.rbenv/versions/2.5.1/bin/irb:11:in `<main>'
1: from (irb):13
NoMethodError (undefined method `attributes_for_json' for User:Class)
irb(main):014:0> User.new.attributes_for_json
=> [:id, :name, :created_at]
Module authors will usually include in their README file or documentation as to how their module should be used by other developers.
Often, sophisticated modules add both class methods and instance methods. In the following exercise, we'll take a look at a couple of approaches.
In this exercise, we will be creating a user module that implements the User class and instance methods to display the name and email address of an individual. We will be using the HelperMethods module extensively to map the hashes for all the variables. The following steps will help you to complete the exercise:
module HelperMethods
def to_hash
self.instance_variables.inject({}) do |map, iv|
map[iv] = self.instance_variable_get(iv)
map
end
end
end
class User
include HelperMethods
attr_accessor :id, :name, :email
end
u = User.new
u.id = 1
u.name = "Bob"
u.email = "[email protected]"
u.to_hash
The output will be as follows:
module HelperMethods
def to_hash
self.instance_variables.inject({}) do |map, iv|
map[iv] = self.instance_variable_get(iv)
map
end
end
module ClassMethods
def attributes_for_json
[:name, :email]
end
end
end
The intention here is to only allow some attributes to be output in the to_hash method.
module HelperMethods
def to_hash
formatted_class_attributes = self.class.attributes_for_json.map{|attr| "@#{attr}".to_sym}
filtered_ivars = self.instance_variables & formatted_class_attributes
filtered_ivars.inject({}) do |map, iv|
map[iv] = self.instance_variable_get(iv)
map
end
end
module ClassMethods
def attributes_for_json
[:name, :email]
end
end
end
We use & to get the set intersection of two arrays. In other words, we're filtering for elements that exist in both arrays.
class User
include HelperMethods
extend HelperMethods::ClassMethods
attr_accessor :id, :name, :email
end
u = User.new
u.id = 1
u.name = "Bob"
u.email = "[email protected]"
u.to_hash
The output should be as follows:
And we're in business. We wrote a module that has both instance methods and class methods available. Our to_hash instance method brought in by the module calls the class method. However, in the current form, our client code, the User class, has to both include and extend the submodule. This is a bit verbose and since our instance methods require the class method to be there, it would be nice if, as module authors, we didn't leave it up to other developers to write both lines of include and extend.
Luckily, Ruby provides a way for us to detect when a module has been included, and so we can write code to automatically extend the class methods for us. Let's learn about this in the next section.
Callbacks, in general, are an architectural paradigm where a method or function is "called back" to act upon some life cycle event. Web frameworks have callbacks to allow code to be called before and/or after a web request is processed. Database frameworks usually have callbacks to call code before a record is created, updated, or deleted. You can even think of the standard initialize method as a callback because Ruby is "calling back" to this method when an object is created. Callbacks allow developers to "hook into" those life cycle events and execute any code they wish.
In Ruby, there are a number of different types of callbacks to hook into Ruby object's life cycle events, but here we're going to focus on module callbacks. Ruby triggers a callback when a module has been either included or extended. This ability gives rise
to a massively useful paradigm, as we will see later. But first, let's see each callback in action.
In this exercise, we will extend the Facebook class with two functions from the ApiWrapper module, which are called send_message and new_post:
module ApiWrapper
def send_message(from, to, message)
puts "Hi #{to}, I wanna say #{message}"
end
def new_post(from, title, description)
puts "This is a post from #{from}, with title: #{title} and #{description}"
end
end
class Facebook
extend ApiWrapper
end
Note
Read more about Ruby life cycle callbacks here: https://packt.live/35ts4D4.
Facebook.send_message("Packt","Students","thank you!")
Facebook.new_post("Author","Extending your classes","Extend imports functions from modules as class methods!")
We have successfully extended the class functionality using module functions.
The include keyword extends the namespace of a specific class or module with extra functionality. Consider the following example:
module SpyModule
def self.included(base_class)
puts "I've been included into: #{base_class}"
end
end
class User
include SpyModule
end
Here, we've defined our SpyModule module, which has a self.included method, which takes an argument of the base class. There are a few things to point out here:
Understanding class loading is a complex topic, especially when using a framework such as Ruby on Rails. For the purposes of this book, we can consider that all class loading will be done ahead of all program execution; however, it should be noted that, in reality, class loading is much more dynamic and can occur anywhere in program execution, even near the end.
The extended callback works identically to the included callback. Look at the
following example:
module SpyModule
def self.extended(base_class)
puts "I've been extended into: #{base_class}"
end
end
class User
extend SpyModule
end
In the preceding code, we have defined the SpyModule module, which defines the self.extended method. This method, in turn, takes the argument of base_class.
As we saw in the previous exercise, module authors usually want to add both instance and class methods. Module authors also want to make it convenient and less error-prone for client code to leverage their module. We can refactor the code from the previous exercise, so that client code can simply include our module, and then our module takes care of everything else. The magic is actually pretty simple. We will just add the following code to our module:
def self.included(base_class)
base_class.extend(ClassMethods)
end
Altogether, this code would look like the following:
module HelperMethods
def self.included(base_class)
puts "#{base_class} has included HelperMethods. We're also going to extend ClassMethods into it as well"
base_class.extend(ClassMethods)
end
def to_hash
formatted_class_attributes = self.class.attributes_for_json.map{|attr| "@#{attr}".to_sym}
filtered_ivars = self.instance_variables & formatted_class_attributes
filtered_ivars.inject({}) do |map, iv|
map[iv] = self.instance_variable_get(iv)
map
end
end
module ClassMethods
def attributes_for_json
[:name, :email]
end
end
end
Now, when our User class includes the HelperMethods module and completes its class loading, the included callback will be called and the calling class (User) will be passed as an argument. The User class constant will then be sent extend with the ClassMethods submodule passed to it:
class User
include HelperMethods
end
The output will be as follows:
As you can see, the included callback is called, and we then extend ClassMethods into the User class. We can infer the following points:
Enumerated types are not specific to modules, but will come in handy for the next exercise, so let's take a brief moment to talk about them in Ruby. An enumerated type is a data type consisting of a set of named values. In other words, enumerated types, or, as they are commonly called, enums, are custom data types that consist of a set of predefined, or enumerated values.
A common example of an enumerated type is a status. For instance, if you have an e-commerce application, you might have an order status that is comprised of [:draft, :ordered, :delivered, :canceled]. It is useful to fix those values to an integer, although a symbol value can work just as well. Integers are commonly used because they are performant for storing in a database, so comparisons and queries are much faster. In Ruby, symbols are more performant than strings, but if you had to save a status in a database, that symbol would get converted to a string. Therefore, it's good practice for enums to have integer values.
To summarize:
Note
Enumerated types should not be confused with the Ruby Enumerable module.
In practice, enumerated types rarely change, but when they do, it is usually to add an additional type into the set of values.
Here is an example of a PaymentTypes enum:
class PaymentTypes
CREDIT_CARD = 1
CHECK = 2
WIRE = 3
TYPES = [CREDIT_CARD, CHECK, WIRE]
end
Here, we've defined the possible payment types as named constants and the named constants are assigned an integer value. We've also created a TYPES constant so we can easily iterate over the possible types that are defined.
Designing enums in this way is a bit cumbersome and is only mildly useful. Let's design a module that makes working with enums far more useful and makes it quick and easy to define classes as enums.
In this exercise, we are going to write a module that defines what payment types will be accepted at a product application store. We are going to write a utility module, called Enum, that allows for any class to be turned into an enumerated data type. The requirements for our module will be such that the enum classes will be defined with a DATA constant, which is an array of all the values of our enum. Each element of the DATA array will itself be an array with the integer value, symbol, and label. This gives us the flexibility to store the value in a database, work with it in Ruby as a symbol, or output a human-readable label:
class PaymentTypes
include Enum
DATA = [
[ WIRE = 1, :wire, "Wire"],
[ CHECK = 2, :check, "Check"],
[ CREDIT = 3, :credit, "Credit card"],
]
end
Here, we have a basic Ruby class that includes the Enum module (to be defined). Then, we declare a DATA constant that has our actual Enum types of :wire, :check, and :credit with associated values and labels.
module Enum
def self.included(base_class)
base_class.extend ClassMethods
base_class.class_eval do
attr_reader :id, :name, :label
end
end
module ClassMethods
end
end
Here, we've added some new magic. As discussed in the previous chapter, using the included callback is a powerful paradigm for modules. As we've seen, we first extend ClassMethods; we're going to leave the ClassMethods submodule empty for now. The next line runs class_eval and passes a block to it.
class_eval is a special Ruby method that will run the code contained in the preceding block in the context of the class definition. Running attr_reader in the class_eval block is the same as if we called it normally in a basic class definition. So, essentially, we are just adding reader methods on whatever class includes Enum for :id, :name, and :label.
module Enum
# ... omitted for brevity
def initialize(id, name, label=nil)
@id = id
@name = name
@label = label
end
end
This is a basic constructor and uses the basic include module principles we learned at the beginning of the chapter. The initialize method will be added to all instances of the class that include Enum.
pt = PaymentType.new(1, :wire, "Wire")
pt.id
pt.name
pt.label
The output would be as follows:
Okay, so we can see our module is basically working. We properly extended the class with the attr_reader method and our constructor was added as well. However, we can also do something weird, such as this:
pt = PaymentTypes.new(nil, :foo, "Huh?")
On the console, it would look as follows:
We just created a weird payment type that isn't really an enumerable that we expect. We could add a validation to the constructor, but we'll leave that as an exercise for you, dear reader.
module ClassMethods
def all
@all ||= begin
self::DATA.map { |args| new(*args) }
end
end
end
Here, we've defined an all class method that loops over the DATA constants, instantiates each one and assigns it to a class instance variable. Let's test it out:
PaymentTypes.all
The output would be as follows:
Isn't this amazing? We wrote a module that allows us to easily define enums as an array in any class. Our module adds an all method that returns all of the types for us as instances of objects. When we started with enums, they were just integers stored in constants. But now with our module, they are instances of objects and we have all the power of object-oriented programming behind us to work with these types.
class PaymentTypes
include Enum
DATA = [
[ WIRE = 1, :wire, "Wire"],
[ CHECK = 2, :check, "Check"],
[ CREDIT = 3, :credit, "Credit card"],
]
def wire?
id == WIRE
end
def check?
id == CHECK
end
def credit?
id == CREDIT
end
end
We've added some methods here. These methods are called interrogation methods because they ask the instances what they are. Is it a wire? Is it a credit card? Because our enum is a plain old Ruby class, we have full flexibility for the behavior of this type.
If we were to create more and more enums, the chances are high that we would also create interrogation methods on each of those types. Can we write code in our module that automatically creates those interrogation methods for us? If you've been paying attention, then you'll know the answer is yes. There are a few approaches here, but we'll use our old friend method_missing from the previous chapter.
module Enum
def is_type?(type)
name.to_sym == type.to_sym
end
def method_missing(method, *args, &block)
interrogation_methods = self.class.all.map{|type| "#{type.name}?".to_sym}
if interrogation_methods.include?(method)
type = method.to_s.gsub("?", '').to_sym
is_type?(type)
else
super
end
end
end
Great, so we added a is_type? method, which is our comparison method. We overrode method_missing to check whether the missing method that was called was an interrogation method to a valid type, and if so, formatted it and passed it to the is_type? method:
PaymentTypes.all[0].wire?
PaymentTypes.all[0].credit?
PaymentTypes.all[2].wire?
PaymentTypes.all[2].credit?
The output would be as follows:
Now we get these interrogator methods for free with any enum class that includes our module. This module is quite often used in production applications, although it contains quite a few more utility methods.
As we've learned so far, modules have primarily been used to add instance or class methods to other classes. You can add instance or class methods depending on whether you include or extend that module into your class. In both cases, though, the methods to be added are always defined as basic methods. This is in contrast to class methods in a class definition, which have the self. prefix for their declaration.
However, we also saw that the module callbacks were declared differently using the self. prefix. Let's see what happens if we define other methods using the self. prefix on a module like so:
module BabelFish
def self.the_answer
return 42
end
end
What we're doing here is defining static module methods. These are very similar to class methods but on a module. They don't contain any state. They are called straight onto the module constant itself:
irb(main):058:0> BabelFish.the_answer
=> 42
So, module methods are pretty straightforward. Here are a few modules that are defined in the Ruby Core and Standard libraries that have module methods:
irb(main):061:0> URI.parse("https://google.com")
=> #<URI::HTTPS https://google.com>
irb(main):003:0> FileTest.directory?("specs")
=> false
irb(main):005:0> Math.atan(45)
=> 1.5485777614681775
Note
You can explore more modules by going to the Ruby documentation here: https://packt.live/2M6n8MM. Modules have an "M" next to them.
In addition to adding methods to classes and providing out-of-the-box functionality as part of the module, another major purpose of modules is to provide a namespace. A namespace is just what it sounds like: it provides a scope or space for naming. In particular, it provides a space for constants. With the exception of raw global methods, the entry point for most code will be through a constant, whether it be a class constant or a module constant.
We've learned how to create classes and modules. Really, what we are doing is creating constants that point to those objects in memory. When we create constants (classes, modules, or otherwise) in IRB, we are creating a constant in the global namespace. This can quickly get crowded, especially if you are creating a class or module constant that may have a common name.
For instance, in the previous topic, we created an Enum module. Enum is a very common word in the Ruby world, and do we really think our Enum module is the best and that we should own that word? It is possible there is a more official Enum library, or that Ruby may use it as a global constant in the future. Therefore, it's a good practice to name your global constants with a name that is more unique. By doing this, you are also declaring a unique namespace that you can then put other constants inside of to make them safe from name collision.
As such, let's rename our Enum module to be a bit more specific to what the module
is doing:
module ActsAsEnum
end
class PaymentTypes
include ActsAsEnum
end
The name ActsAsEnum is not very inspired, but it is descriptive and as such makes it easy to read and understand what might be happening. While ActsAsEnum is more unique than Enum, in the Ruby world, lots of people use the ActsAs convention, so this may still have issues. We'll go with it for now.
Now that we've defined our ActsAsEnum module, assuming it's unique, we are free to add constants inside the module namespace and we can be sure to avoid name conflicts. In fact, we can use modules for no other purpose than to define namespaces:
module Zippy
SKIPPY = "skippy"
class Zappy
end
module Dappy
def self.say_something
puts "doo"
end
end
end
We defined our arbitrary namespace, Zippy, and created the Skippy, Zappy, and Dappy classes. If we need to access the classes within the namespace, we use the scoping operator, ::, as follows:
Zippy::SKIPPY
Zippy::Zappy.new
Zippy::Dappy.say_something
The output would be something as follows:
We can see that it doesn't matter whether we're accessing a constant with all caps, a class constant, or a module constant – we still use the :: scoping operator to access it.
Ruby is pretty smart about its constant lookup, but sometimes it can get confused, or sometimes you have to override its lookup behavior. The following exercise will show you the problem and provide you with a solution.
In this exercise, we will be reusing the global User constants. We will be using the global scoping operator for this purpose:
class User
def self.output
return "Global User"
end
end
module Report
def self.test_namespace
User.output
end
class User
def self.output
return "Report::User"
end
end
end
Here, we've got two classes with the commonly labeled User constant. However, the second User class is present within the Report module namespace.
Report.test_namespace
The output will be returned as follows:
This is as expected. You would expect to call User, which is inside the module, to be called by the module itself. What if we want the global User constant, though?
class User
def self.output
return "Global User"
end
end
module Report
def self.test_namespace
User.output
end
def self.test_global
::User.output
end
class User
def self.output
return "Report::User"
end
end
end
We've added a test_global module method that references the global User constant by using the scoping operator, ::, but without anything before it. This is the global scoping operator. The output would now be as follows:
We have thus reused the User global constant as a common constant for all methods.
So far, we've discussed the include, extend, and module methods and namespaces. There is one more aspect to modules that came with the Ruby 2.0 release several years ago: prepend. prepend is not often used, perhaps because it is not well understood. Let's change that.
First, let's consider the following example:
module ClassLogger
def log(msg)
"[#{self.class}] #{msg}"
end
end
class User
include ClassLogger
def log(msg)
"[#{Time.now.to_f.to_s}] #{super(msg)}"
end
end
class Company
prepend ClassLogger
def log(msg)
"[#{Time.now.to_f.to_s}] #{super(msg)}"
end
end
We've created a module called ClassLogger, which implements a log method. This method wraps a string and outputs the current class. We've also created two classes, Company and User, which implement an identical log method that first calls super with the msg argument, then adds a prefix of the current time to the log message.
The difference is that User calls include to this module, whereas Company calls prepend:
User.new.log("hi")
Company.new.log("hi")
The output would be as follows:
The User implementation (include) got both the time and class prefixes, whereas Company only has the time prefix. What's going on here? The answer lies in how Ruby dispatches methods, as shown in the following code:
User.ancestors
The ancestors method output for the User class will be displayed as follows:
Similarly, the ancestors for the Company class will be:
Company.ancestors
We call the ancestors method, which is an introspection method Ruby provides us with on class objects. We can see a significant difference here. The User class ancestors have User first and then ClassLogger second. The Company class has ClassLogger first and then Company second. As such, Ruby calls methods from each of these classes in this hierarchy in that order. The ClassLogger implementation doesn't call super, so the chain stops there, which is why we only see that particular output. When User calls log, it first calls the User implementation (the time prefix) and then calls super, which then calls the ClassLogger implementation. This is why we see the string output with time first and then class second.
When we started this chapter, we said that calling include was essentially like copying and pasting the code into the class. This isn't entirely true. As we can see, what's actually happening is that it's being added into the class hierarchy in an ordered fashion. That is, it adds the methods to the class hierarchy after the class itself. prepend, on the other hand, is adding to the class hierarchy before the class itself.
Prepending methods to the class hierarchy gives rise to some very important behaviors. Primarily, it allows modules to have their methods called first. By being called first, modules then have complete control of the original implementation. They can preprocess or postprocess data and behavior. Module authors should dutifully call super to make sure that the original implementation is called or have a good reason to not do it.
We've said that prepend adds module methods to the top of the class hierarchy. However, what happens when we subclass a class that has prepended a module? First, let's look at the subclass hierarchy in the following code:
class ParentClass
end
class ChildClass < ParentClass
end
ParentClass.ancestors
The ancestors for ParentClass will be as follows:
Similarly, the output for ChildClass will be as follows:
ChildClass.ancestors
This makes sense. The child class is higher up in the class hierarchy than ParentClass, which is what explains basic inheritance behavior in Ruby. Now add a module to prepend, as shown in the following code:
module PrependedModule
def output
puts "Outputting from the PrependedModule"
super
end
end
class ParentClass
prepend PrependedModule
def output
puts "Outputting from the parent class"
end
end
class ChildClass < ParentClass
def output
puts "Outputting from the child class"
end
end
ChildClass.new.output
ChildClass will now look as follows:
ChildClass.ancestors
The ancestors method on ChildClass will respond as follows
Similarly, for ParentClass, the ancestors method will respond as follows:
ParentClass.ancestors
The output would be as follows:
As we can see, ChildClass appears higher up in the hierarchy than the prepended module. This means the module's function will not be called unless ChildClass calls super, as shown in the following code:
class ChildClass < ParentClass
def output
super
puts "Outputting from the child class"
end
end
puts ChildClass.new.output
The output would now be as follows:
If we want our module to work for subclasses too, we have another Ruby callback: inherited. Consider the following code:
inheritedcallback.rb
1 module PrependedModule
2
3 def output
4 puts "Outputting from the PrependedModule"
5 super
6 end
7 end
8
9 class ParentClass
10 prepend PrependedModule
11
12 def self.inherited(klass)
13 klass.send(:prepend, PrependedModule)
14 end
15
The output would be as follows:
This type of coding is very advanced, and care should be taken to make sure that the method chain is understood well. In other words, take care to understand how super is placed in the method chain so that the module method is not called multiple times if that is not desired.
The preceding code still requires ParentClass to define the inherited callback. We can go even further with our module with the following code:
inheritedcallback_withparentclass.rb
1 module PrependedModule
2 def output
3 puts "Outputting from the PrependedModule"
4 super
5 end
6 def self.prepended(base_class)
7 puts "Included: #{base_class}"
8 base_class.instance_eval do
9 def self.inherited(klass)
10 puts "Inherited: #{klass}"
11 klass.send(:prepend, PrependedModule)
12 end
13 end
14 end
15 end
The output would be as follows:
Here, we have done some really advanced Ruby coding by using the prepended callback to know when our module was prepended. We also use instance_eval to evaluate a block of code in the context of the class constant instance. This allows us to dynamically define the inherited callback method, which then allows us to prepend our module on the subclass.
In this exercise, we will prepend ApplicationDebugger to the Application class and define the debug function, which takes application_name and debugs the application:
module ApplicationDebugger
def debug(args)
puts "Application debug start: #{args.inspect}"
result = super
puts "Application debug finished: #{result}"
end
end
class Application
prepend ApplicationDebugger
def debug(args)
{result: "ok"}
end
end
DBugger = Application.new
DBugger.debug("NotePad")
In this activity, we're going to expand the voting program we wrote in Chapter 5, Object-Oriented Programming with Ruby. We will enable the voting program to allow multiple categories and make it so that users can create categories via the menu. Once a category is created, votes can start being recorded for that category. A category could be something such as "Employee of the Month," "Innovation Leader of Q1," or "Best Collaborator." Make sure that there are no duplicate categories. Add a module to the controller base class that handles the logging of each controller run.
The following steps will help you to complete the activity:
Here is the expected output:
Note
The solution to the activity can be found on page 472.
This chapter aimed to provide you with the knowledge necessary to create modules with the Ruby object model. We have added instance methods by including modules into classes. We have added class methods to extend class functionality. We have created namespaces for our modules. As a very crucial part, we have made a distinction between prepending modules into classes and how you can extend and include functionality. Modules in Ruby accomplish multiple purposes such as creating namespaces, creating reusable class and instance methods, and modifying a class's code at runtime. Modules are Ruby's answer to multiple inheritance, in that they allow classes to incorporate code from multiple sources. As we saw in the last section, when a module is included or prepended, it affects the class hierarchy, which is what Ruby uses to do method lookup and dispatch. The ordering of this hierarchy is important and something to keep in mind as you start using third-party modules in your application code.
Now that we've covered all the major fundamentals in Ruby, in the next chapter, we'll use what we have learned to focus on importing data from external sources, processing it using the models we'll create, and outputting that data in a common format such as CSV.
18.189.170.134