Chapter 9. Mixins: Mix It Up

image with no caption

Inheritance has its limitations. You can only inherit methods from one class. But what if you need to share several sets of behavior across several classes? Like methods for starting a battery charge cycle and reporting its charge level—you might need those methods on phones, power drills, and electric cars. Are you going to create a single superclass for all of those? (It won’t end well if you try.) Or methods for starting and stopping a motor. Sure, the drill and the car might need those, but the phone won’t!

In this chapter, we’ll learn about modules and mixins, a powerful way to group methods together and then share them only with particular classes that need them.

The media-sharing app

This week’s client project is an app for sharing videos, music, and other media. Music and videos both need some of the same functionality: users need to be able to play songs and videos back, as well as leave comments on them. (To keep this example simple, we’ll omit functionality like pausing, rewinding, etc.)

image with no caption

There are some aspects that differ, however. We need the ability to track the number of beats per minute on songs, to separate the fast music from the slow music. Videos don’t need a number of beats per minute, but they do need to keep track of their resolution (how wide and how tall they are in pixels).

Since we’re mixing data and behavior (we need data like resolution, beats per minute, etc., as well as behavior like playback), it makes sense to put everything in Video and Song classes. Some of that behavior (playback and commenting) is shared, however. The best tool we have so far for sharing behavior between classes is inheritance. So we’ll make Video and Song subclasses of a superclass called Clip. Clip will have an attribute reader method named comments, as well as play and add_comment instance methods.

image with no caption

The media-sharing app...using inheritance

Here’s some code to define the Clip, Video, and Song classes and test them out. The resolution and beats_per_minute attributes are very straightforward, so we’ll focus on trying the add_comment and comments methods instead.

image with no caption

All this seems to be working pretty well! But then your client throws you a curveball...

One of these classes is not (quite) like the others

Your client wants to add photos to the media-sharing site. Like videos and music, photos should allow users to add comments. Unlike videos and music, of course, photos should not have a play method; they should have a show method instead.

So, if you make a Photo class, its instances will need a comments attribute accessor method and an add_comment method, just like the ones that Video and Song inherit from the Clip superclass.

Using only the things we’ve learned about Ruby classes so far, we have a couple of possible solutions, but each has its own problems...

Option one: Make Photo a subclass of Clip

We could just make Photo a subclass of Clip, and let it inherit comments and add_comment just like the other subclasses.

But there’s a weakness with this approach: it (wrongly) implies that a Photo is a kind of Clip. If you make Photo a subclass of Clip, it will inherit the comments and add_comment methods, yes. But it will also inherit the play method.

image with no caption

You can’t play a photo. Even if you overrode the play method in the Photo subclass so that it didn’t raise errors, every developer who looks at your code in the future will scratch their head wondering why Photo has a play method on it.

So it doesn’t seem like a good option to subclass Clip just to get the comments and add_comment methods.

Option two: Copy the methods you want into the Photo class

The second option isn’t much better: we could skip setting up a superclass for Photo, and implement the comments and add_comment methods again in the Photo class. (In other words, copy and paste the code.)

image with no caption

We learned the downsides to this approach at the start of Chapter 3, though. If we make a change to comments or add_comment in the Photo class, we have to make sure to change the code in the Clip class as well. Otherwise, the methods will behave differently, which could be a nasty surprise for future developers working with these classes.

So this isn’t a good option, either.

Not an option: Multiple inheritance

We need a solution that will let us share the implementation of the comments and add_comment methods between the Video, Song, and Photo classes, without Photo inheriting the play method.

It would sure be nice if we could move the comments and add_comment methods to a separate AcceptsComments superclass, while leaving the play method in the Clip superclass. That way, Video and Song could inherit the play method from Clip and inherit the comments and add_comment methods from AcceptsComments. Photo would only need AcceptsComments as a superclass, so it would inherit comments and add_comment without inheriting play.

image with no caption

But here’s a spoiler: we can’t. This concept of inheriting from more than one class is called multiple inheritance, and Ruby doesn’t allow it. (It’s not just Ruby; Java, C#, Objective-C, and many other object-oriented languages intentionally leave out support for multiple inheritance.)

The reason? It’s messy. Multiple inheritance gives rise to ambiguities that would require complex rules to resolve. Supporting multiple inheritance would make the Ruby interpreter much larger, and it would make Ruby code much uglier.

So Ruby has another solution...

Using modules as mixins

We need to add a group of methods to multiple classes without using inheritance. What can we do?

Ruby offers us modules as a way to group related methods. A module starts with the keyword module and the module name (which must be capitalized) and ends with the keyword end. In between, in the module body, you can declare one or more methods.

image with no caption

Looks similar to a class, right? That’s because a class is actually a kind of module. There’s a key difference, though. You can create instances of a class, but you can’t create instances of a module:

image with no caption

Instead, you can declare a class and then “mix in” a module. When you do so, the class gains all the module’s methods as instance methods.

image with no caption
image with no caption

Modules that are designed to be mixed into classes are often referred to as mixins. Just as a superclass can have more than one subclass, a mixin can be mixed into any number of classes:

image with no caption

Now, here’s the cool part: any number of modules can be mixed into a single class. The class will gain the functionality of all the modules!

image with no caption

You can mix modules into a class regardless of whether it has a superclass.

image with no caption

Mixins, behind the scenes

When we learned about method overriding back in Chapter 3, we saw that Ruby looks for instance methods on a class first, then looks on the superclass...

image with no caption

Conventional Wisdom

Like class names, module names must begin with a capital letter. It’s conventional to use “camel case” for the remaining letters.

module Secured
  ...
end
module PasswordProtected
  ...
end

When a module is used as a mixin, it generally describes an aspect of an object’s behavior. So there’s also a convention to use a description as the mixin’s name. Use a single adjective if you can.

module Traceable
  ...
end

If an adjective is too awkward, a multiword description is okay. (For example, Commentable doesn’t read very well, so AcceptsComments is probably preferable.)

module AcceptsComments
  ...
end

When you mix in a module, it works in a very similar fashion. Ruby adds the module to the list of places it will look for methods, between the class you’re mixing it into and its superclass.

image with no caption

From then on, if a method isn’t found on the class, Ruby will look for it in the module. And that’s how mixins add methods to a class!

Creating a mixin for comments

We need our Video, Song, and Photo classes to share a single implementation of the comments and add_comment methods, but we’ve learned that we can’t use multiple inheritance.

image with no caption

But as we just saw, we can share methods between multiple classes by moving them into a module, and using that module as a mixin. Let’s try moving the comments and add_comment methods to a module. Since our module is going to contain methods that allow an object to accept comments, we’ll name it AcceptsComments.

image with no caption

In addition to moving some methods to the module, we’ve added some logic to comments and gotten rid of the initialize method. We’ll explain why, but first let’s try out our changes.

Using our comments mixin

Let’s mix the new AcceptsComments module into the Video and Song classes[1] and see if everything still works the same way...

image with no caption

The mixin worked! Video and Song still have their comments and add_comment methods, except that now they get them from mixing in AcceptsComments instead of inheriting them from Clip. We can also confirm that Video and Song successfully inherited a play method from Clip:

image with no caption

Now let’s create a Photo class and see if we can also mix the AcceptsComments module into that...

image with no caption

The new Photo class has a show method, as it should, but it does not have the unwanted play method. (Only the subclasses of Clip have that.) Everything is working as we intended!

image with no caption

A closer look at the revised “comments” method

Now that we know it works, let’s take a closer look at that comments method. Our new version of the method looks for a @comments instance variable on the current object. If @comments has never been assigned to, it will have a value of nil. In that event, we assign an empty array to @comments, and return that empty array from the comments method.

After that, @comments is no longer nil. So subsequent calls to the comments method will return the array we assigned to @comments.

image with no caption

The add_comment method is unchanged; it still relies on the comments method to get the array it should add a comment to. So the first time you call it, your comment will get added to a new, empty array. Subsequent calls will keep adding comments to the same array, because that’s what the comments method will return.

Because the comments method ensures the @comments instance method is initialized for us, we no longer need the initialize method on the Clip superclass. So we’re free to delete it!

Why you shouldn’t add “initialize” to a mixin

The revised comments method on our AcceptsComments module works every bit as well as the comments attribute reader method worked on our old Clip superclass. We no longer need the initialize method.

image with no caption

But why did we go to the trouble of updating the comments method? Couldn’t we just move the initialize method from Clip to AcceptsComments?

image with no caption

Well, if we had done it that way, the initialize method would work just fine...at first.

image with no caption

The initialize method gets mixed into the Photo class, and it gets called when we call Photo.new. It sets the @comments instance variable to an empty array, before the comments reader method is ever called.

Using an initialize method in our module works...until we add an initialize method to one of the classes we’re mixing it into.

Suppose we needed to add an initialize method to our Photo class, to set a default format:

class Photo
  include AcceptsComments
  def initialize
    @format = 'JPEG'
  end
  ...
end

After we add the initialize method, if we create a new Photo instance and call add_comment on it, we’ll get an error!

image with no caption

If we add some puts statements to the two initialize methods to help debug, the issue becomes clear...

image with no caption

The initialize method defined in the Photo class gets called, but the initialize method in AcceptsComments doesn’t! And so, in the code above, the @comments instance variable is still set to nil when we call add_comment, and we get an error.

The problem is that the initialize method from the class is overriding the initialize method from the mixin.

Mixins and method overriding

As we saw, when you mix a module into a class, Ruby inserts the module into the chain of places it will look for methods, between the class and its superclass.

class MySuperclass
end

module MyModule
  def my_method
    puts "hello from MyModule"
  end
end

class MyClass  <  MySuperclass
  include MyModule
end
image with no caption

For a given class, you can get a list of all the places that Ruby will look for methods (both mixins and superclasses) by using the ancestors class method. It will return an array with all of the class’s mixins and superclasses, in the order that they’ll be searched.

image with no caption

Back in Chapter 3, we saw that when Ruby finds the method it’s looking for on a class, it invokes the method and then stops looking. That’s what allows subclasses to override methods from superclasses.

image with no caption

The same is true for mixins. Ruby searches for instance methods in the modules and classes shown in a class’s ancestors array, in order. If the method it’s looking for is found in a class, it just invokes that method. Any method by the same name in a mixin is ignored; that is, it gets overridden by the class’s method.

Avoid using “initialize” methods in modules

So that’s why, if we were to include an initialize method in the AcceptsComments mixin, it would cause problems later. If we added an initialize method to the Photo class, it would override initialize in the mixin, and the @comments instance variable wouldn’t get initialized.

image with no caption

And that’s why, instead of relying on an initialize method in AcceptsComments, we initialize the @comments instance variable within the comments method.

image with no caption

It doesn’t matter that there’s an initialize method in the Photo class, because there’s no initialize method in AcceptsComments for it to override. The @comments instance variable gets set to an empty array when comments is first called, and everything works great!

image with no caption

So here’s the lesson: avoid using initialize methods in your modules. If you need to set up an instance variable before it’s used, do so in an accessor method within the module.

Using the Boolean “or” operator for assignment

It’s a bit of a pain, though: the comments method is several lines long. It would be nice if we could make our attribute accessor methods more concise, especially if we’ll be adding more of them.

Well, what if we told you that we could reduce those five lines of code in the comments method down to just one, while keeping the same functionality? Here’s one way:

image with no caption

As we learned back in Chapter 1, the || operator (read it aloud as “or”) tests whether either of two expressions is true (or not nil). If the lefthand expression is true, the value of the lefthand expression will be the value of the || expression. Otherwise, the value of the righthand expression will be the value of the || expression. That’s a bit of a mouthful, so it’s probably easier to just observe it in action:

image with no caption

So if @comments is nil, the value of the || expression in the comments method will be an empty array. If @comments is not nil, the value of the || expression will be the value in @comments.

image with no caption

The conditional assignment operator

The value of the || expression is what gets assigned back to @comments. So @comments gets reassigned its current value, or, if its current value is nil, it gets assigned an empty array.

image with no caption

This shortcut is so useful that Ruby offers the ||= operator, also known as the conditional assignment operator, to do the same thing. If a variable’s value is nil (or false), the conditional assignment operator will assign the variable a new value. But if the variable’s value is anything else, ||= will leave that value intact.

image with no caption

The conditional assignment operator is perfect for use in our

image with no caption

The comments method works the same as before, but now it’s one line instead of five!

Our complete code

Here’s our complete updated code for the media-sharing app. We don’t need that pesky initialize method in the AcceptsComments mixin, now that the comments method initializes the @comments array itself. Everything is working great!

image with no caption

Your Ruby Toolbox

That’s it for Chapter 9! You’ve added modules and mixins to your toolbox.

image with no caption

Up Next...

Now you know how to create and use a mixin. But what do you do with this amazing new tool? Well, Ruby has some ideas! It includes some powerful modules that are ready to mix into your classes. We’ll learn about them in the next chapter!



[1] We could also have included AcceptsComments in the Clip class and let both Video and Song inherit the methods, but we felt this was a bit clearer.

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

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