6. Modules and Mixins

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.

Introduction

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.

Including 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:

Figure 6.1: Output after including modules

Figure 6.1: Output after including modules

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.

Exercise 6.01: Controlling the Operations of Services in an Application

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:

  1. Open up a new file, Exercise6.01.rb.
  2. Define the Logger module:

    module Logger

      def log_message(level, message)

        File.open("ServiceLogs.txt", "a") do |f|

            f.write "#{level}: #{message} "

        end

      end

    end

  3. Now we can define the Service class:

    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

  4. Now we instantiate the class and call the functions on the service called Windows Update:

    TestService = Service.new

    TestService.stop_service("Windows Update")

    TestService.start_service("Windows Update")

  5. Invoke the code with ruby Exercise6.01.rb.

    The contents of the ServiceLogs.txt file should be as follows:

    Figure 6.2: Service logs output

Figure 6.2: Service logs output

Thus, we have successfully used the Logger module to generate a service update on an application.

Inheritance with Module Methods

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:

Figure 6.3: Output for subclass overriding a method

Figure 6.3: Output for subclass overriding a method

Our subclass has overridden a method defined in the module and is able to call super to that method if necessary.

Inclusion Ordering

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:

Figure 6.4: Output error for inclusion ordering

Figure 6.4: Output error for inclusion ordering

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:

Figure 6.5: Output after changing the order

Figure 6.5: Output after changing the order

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.

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.

Exercise 6.02: Extending Your Modules with Class and Instance Methods

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:

  1. Define a HelperMethods module that outputs a hash map of all the instance variables using the inject method:

    module HelperMethods

      def to_hash

        self.instance_variables.inject({}) do |map, iv|

            map[iv] = self.instance_variable_get(iv) 

            map

      end

     end

    end

  2. Define a User class with attr_accessors:

    class User

      include HelperMethods

      attr_accessor :id, :name, :email

    end

  3. Test the to_hash method on variables:

    u = User.new

    u.id = 1

    u.name = "Bob"

    u.email = "[email protected]"

    u.to_hash

    The output will be as follows:

    Figure 6.6: Output for the hash method on variables

    Figure 6.6: Output for the hash method on variables

  4. Next, we create a submodule within the HelperMethods module to contain the class methods:

    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.

  5. Amend the to_hash method to only use the attributes allowed by the class 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.

  6. Amend the User class to include the instance methods, and extend the ClassMethods submodule:

    class User

      include HelperMethods

      extend HelperMethods::ClassMethods

      attr_accessor :id, :name, :email

    end

  7. Let's now test our User module:

    u = User.new

    u.id = 1

    u.name = "Bob"

    u.email = "[email protected]"

    u.to_hash

    The output should be as follows:

    Figure 6.7: Output for extending classes

Figure 6.7: Output for extending classes

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.

Module Callbacks

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.

Exercise 6.03: Using Module Functions to Extend Classes

In this exercise, we will extend the Facebook class with two functions from the ApiWrapper module, which are called send_message and new_post:

  1. Create a new file, Exercise6.03.rb.
  2. Define the ApiWrapper module with the send_message and new_post functions:

    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

  3. Define the Facebook class. We will extend ApiWrapper within this class:

    class Facebook

      extend ApiWrapper

    end

    Note

    Read more about Ruby life cycle callbacks here: https://packt.live/35ts4D4.

  4. Add testing code to the file:

    Facebook.send_message("Packt","Students","thank you!")

    Facebook.new_post("Author","Extending your classes","Extend imports functions from modules as class methods!")

  5. Invoke the test code with ruby Exercise6.03.rb. The output will be as follows:
Figure 6.8: Output for the Facebook class extension

Figure 6.8: Output for the Facebook class extension

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:

  • The included method callback is defined with self. in front of it. This is known as a module method, and we will cover it in the next chapter.
  • The base_class argument can be called anything. However, it will always represent the class that did the including of the module.
  • The included method is called as soon as the class definition is loaded. Since class definitions are usually loaded earlier on in program execution, it makes it for an ideal time to further modify class definitions within the module.

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.

Importing Modules as Class Methods using Extended

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.

Combining include and extend

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:

Figure 6.9: Output for the User class

Figure 6.9: Output for the User class

As you can see, the included callback is called, and we then extend ClassMethods into the User class. We can infer the following points:

  • This is a simple example, but this paradigm provides the basis to leverage some powerful features of Ruby to write useful modules that add diverse functionality to lots of different classes.
  • We are calling the ClassMethods submodule due to common convention, but this is arbitrary.
  • In the preceding example, we are using include as the entry point for the class to interact with the module, and thus we use the included callback and call extend to add class methods. However, module authors can do the converse by telling developers to use extend instead, which then calls the self.extended callback, which then calls include on the class that did the extending. Again, the reason for using one approach over the other is merely convention and possibly for semantics, as it makes slightly more sense linguistically to include a module than to extend it.

Enumerated Types

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:

  • Enumerated types allow you to explicitly list the set of possible values that
  • can occur.
  • Enumerated types allow you to change the name at any time as long as the value remains the same.
  • Enumerated types are commonly referred to as enums.

    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.

Exercise 6.04: Writing a Module to Define Payment Methods for a Product Application

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:

  1. Create a class that includes the Enum module and defines the enum types in the DATA array:

    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.

  2. Build out the module with a basic included callback:

    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.

  3. Add a constructor to the module:

    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.

  4. Test out the instantiation:

    pt = PaymentType.new(1, :wire, "Wire")

    pt.id

    pt.name

    pt.label

    The output would be as follows:

    Figure 6.10: Output for payment details

    Figure 6.10: Output for payment details

    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:

    Figure 6.11: Payment type details

    Figure 6.11: Payment type details

    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.

  5. Add a ClassMethod module to get all the types:

    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:

    Figure 6.12: Output for all payment types

    Figure 6.12: Output for all payment types

    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.

  6. Add some instance methods to PaymentTypes:

    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.

  7. Respond to interrogation methods using method_missing:

    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:

    Figure 6.13: Output for checking the payment type

Figure 6.13: Output for checking the payment type

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.

Module 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:

  • The URI module:

    irb(main):061:0> URI.parse("https://google.com")

    => #<URI::HTTPS https://google.com> 

  • The FileTest module:

    irb(main):003:0> FileTest.directory?("specs")

    => false

  • The Math module:

    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.

Namespaces

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:

Figure 6.14: Output after using the scoping operator

Figure 6.14: Output after using the scoping operator

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.

Exercise 6.05: How to Reuse Constants with a Namespace

In this exercise, we will be reusing the global User constants. We will be using the global scoping operator for this purpose:

  1. Write a global class constant that is also a constant inside a module:

    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.

  2. Call the Report module of the test method:

    Report.test_namespace

    The output will be returned as follows:

    Figure 6.15: Output after applying the namespace method

    Figure 6.15: Output after applying the namespace method

    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?

  3. Add a method to access the global constant from inside the module namespace:

    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:

    Figure 6.16: Output for the test_global module

Figure 6.16: Output for the test_global module

We have thus reused the User global constant as a common constant for all methods.

prepend

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:

Figure 6.17: Output after prepend

Figure 6.17: Output after prepend

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:

Figure 6.18: The ancestor output for the User class

Figure 6.18: The ancestor output for the User class

Similarly, the ancestors for the Company class will be:

Company.ancestors

Figure 6.19: The ancestor methods for the Company class

Figure 6.19: The ancestor methods for the Company class

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.

prepend with Subclasses

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:

Figure 6.20: The ancestor method output for ParentClass

Figure 6.20: The ancestor method output for ParentClass

Similarly, the output for ChildClass will be as follows:

ChildClass.ancestors

Figure 6.21: The ancestor method output for ChildClass

Figure 6.21: The ancestor method output for ChildClass

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:

Figure 6.22: ChildClass

Figure 6.22: ChildClass

ChildClass.ancestors

The ancestors method on ChildClass will respond as follows

Figure 6.23: The ancestor method output for prepended ChildClass

Figure 6.23: The ancestor method output for prepended ChildClass

Similarly, for ParentClass, the ancestors method will respond as follows:

ParentClass.ancestors

The output would be as follows:

Figure 6.24: The ancestor method output for prepended ParentClass

Figure 6.24: The ancestor method output for prepended ParentClass

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:

Figure 6.25: Output for the prepended module

Figure 6.25: Output for the prepended module

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:

Figure 6.26: Output for the inherited callback

Figure 6.26: Output for the inherited callback

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:

Figure 6.27: Using the prepended module

Figure 6.27: Using the prepended module

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.

Exercise 6.06: Prepending Modules to Classes

In this exercise, we will prepend ApplicationDebugger to the Application class and define the debug function, which takes application_name and debugs the application:

  1. Create a new Ruby file, Exercise6.06.rb.
  2. Define the ApplicationDebugger module with the debug function:

    module ApplicationDebugger

      def debug(args)

        puts "Application debug start: #{args.inspect}"

        result = super

        puts "Application debug finished: #{result}"

      end

    end

  3. Define the Application class:

    class Application

      prepend ApplicationDebugger

      def debug(args)

        {result: "ok"}

      end

    end

  4. Add the invocation code:

    DBugger = Application.new

    DBugger.debug("NotePad")

  5. Invoke the script with ruby Exercise6.06.rb. The output would be as follows:
Figure 6.28: Output for prepending ApplicationDebugger

Figure 6.28: Output for prepending ApplicationDebugger

Activity 6.01: Implementing Categories on the Voting Application Program

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:

  1. Open the Terminal and create a new file.
  2. Write a new test for VotingMachine to add a category.
  3. Implement the add_category method on the voting machine. Run tests when complete.
  4. Write a test for record_vote that adds the category argument.
  5. Amend the record_vote implementation to include category.
  6. Amend the test_run_vote_controller test to include category.
  7. Implement category choosing in the VoteController.
  8. Implement the ControllerLogging module.

Here is the expected output:

Figure 6.29: Voting application with categories

Figure 6.29: Voting application with categories

Note

The solution to the activity can be found on page 472.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.170.134