Sharing code with Ruby and Rails
To work on a large-scale application like Poll Everywhere, writing code that’s easy to read, understand, and maintain is a must. Our Rails application is over a decade old, worked on by a team of about 10, and has lots of moving parts and business requirements that are constantly evolving, so we value engineers who know how to share code flexibly and judiciously. Here are 8 ways we organize our code that you can use to level up your application.
Kernel#include, SimpleDelegator, def_delegate, PORO inheritance, STI, Module#append, class injection… which one?
In this article, we will discuss Ruby class inheritance, Rails’ single-table inheritance, several ways to use Ruby modules, and delegation in Ruby and Rails.
Let us look at some classic Ruby inheritance styles.
Plain old ruby inheritance
In Ruby, class inheritance is denoted by
< in the class definition:
Ruby’s class inheritance behaves like inheritance in many other programming languages: a subclass has all the methods and instance variables of its superclass plus any additional methods it defines in its body. It can also override superclass methods by defining a method of the same name, and call into the superclass method using the
One aspect of Ruby’s inheritance that may differ from other languages you’ve used is that a subclass also inherits its superclass’s namespace, allowing it to reference constants and nested classes or modules without qualification. It can even “override” these constants within its own namespace without affecting the superclass:
Inheritance isn’t evil, but sometimes we are.
Despite the fact that inheritance allows classes to reuse their superclass’s code, don’t. Class inheritance brings all the behavior of the superclass to its children, which can make future changes difficult to impossible. Instead, use inheritance for specialization; that is, when the subclass really IS-A subtype of a superclass in a behavioral sense, not just a conceptual one. Sandi Metz gives 3 conditions for when inheritance is appropriate:
- The inheritance hierarchy is shallow and narrow. If you find yourself with 5 or more subclasses of a single base class, you may want to find a different approach to the problem.
- The subclasses are at the leaf nodes of the system’s object graph. If a class references other classes by name, it probably shouldn’t be a subclass.
- The subclasses use all the behavior of the superclass. Any code that expects a
SQLDatabaseshould be able to work with a
MySQLDatabasewithout checking its class, or the return types of any of its methods. If a subclass can’t satisfy any of the contracts of its superclass, it shouldn’t be a subclass. Conversely, if all subclasses implement a method, the superclass probably should, too (at least to raise a
If you can’t fulfill all of these conditions, you should solve your problem with composition, not inheritance. For further information on the proper use of inheritance and composition, I recommend Metz’s talks All the Small Things and Nothing is Something. If you’re still feeling a craving, check out OO Inheritance – Not Always Evil – Refactoring to Open-Closed with Inheritance by Philip Schwarz.
Rails can use Ruby’s class inheritance system to represent a class hierarchy in a single table. This is known as Single-Table Inheritance (STI), and ActiveRecord reserves the column name
type for this purpose. Whenever an ActiveRecord model is connected to a table with a column named
type, it will interpret the values in that column as the names of subclasses. When it makes a query from the base class, it will instantiate each record as the subclass named by the
type, and queries from the subclasses will be conditioned on the value of the
However, single-table inheritance still relies on Ruby class inheritance. Subclasses still inherit all the code of their superclasses, so all guidelines from the previous section still apply. In addition, sharing a table means that any database field that is needed for one subclass is available to all subclasses. If there are many (any) columns that are not used by all subclasses, STI is probably the wrong choice.
In addition to class inheritance, which limits each class to only one superclass, Ruby supports mixins, known in Ruby as “modules”. Like classes, modules are collections of methods, but unlike classes, modules cannot be instantiated into objects. Instead, modules can be mixed into classes or other modules using the
include class method, giving objects of a class access to module methods as though they had been defined on the class itself.
While Ruby modules can be used in a few different ways, their primary purpose is to share code between classes that may behave similarly but are otherwise unrelated. The best example of this is Ruby’s
Enumerable module, which provides that class with access to a host of other useful methods, including
filter. Any class that implements an each method can gain access to them simply by mixing in
At Poll Everywhere, we use modules to share code between related classes, like Ruby’s Comparable module or a
Commentable module for ActiveRecord objects that can be commented upon, as well as to organize related code in a large class. In the latter case, Poll Everywhere usually namespaces the module under its including class, such as with
Mixing into a class or module with include
The simplest and most common way to mix a module into a class is with include. This gives instances of the class access to any method defined in the module as though it were their own. This is the primary method of mixing in functionality and should be used any time you want to share code between related classes without introducing an inheritance hierarchy, or when you want to organize some related code from a large class.
If you’re interested, Ruby implements this by placing the module just above the class in the object’s inheritance hierarchy. So if a module has a method with the same name as the class, the class will win. To override a method defined on the class from a module, use
prepend instead of
Mixing into an object with extend
include method is great for adding methods to instances of a class, sometimes you want to add methods to the class itself. In Ruby, this is done with
extend. After extending a class with a module, all of the methods defined in the module can be called from the class context. This is often used to create class-level domain-specific languages (DSLs), as with ActiveRecord’s association interface.
In fact, this behavior is not special to classes; it is defined for all objects, and works on classes simply because they are themselves objects. You can actually extend any Ruby object with a module at runtime without affecting other objects of its class.
In addition to
include, Ruby provides the included hook method to give programmers more control over what happens when a module is included. When class
Foo includes module
Bar, Ruby will call
Bar.included(Foo). Note that
included is a method of the Bar module itself, not an instance method defined inside it.
included when you need to define additional class level behavior, like model associations or validations or serializer attributes, that is relevant to the module. For example, Poll Everywhere has a
User::AccountRelationship mixin that uses
included to define validations on the account role attributes. It can also be used to add class methods at the same time as instance methods:
Despite the convenience of code sharing through class inheritance or mix-ins, it is often not a good way to build maintainable software. Maintainable, reusable software is better built through composition, in which an object holds on to one or more other objects and calls their methods as appropriate. Sometimes, though, a class wants to expose a method of these internal objects as its own method (often to avoid violating the Law of Demeter). While we could redefine each method needed on the composite class, it’s annoying to do and prone to rot. Ruby and Rails provide several helpers that automate this process, known as delegation, thereby allowing a composite class to reuse the code of its component parts
Ruby class-level delegation: forwardable
Ruby’s standard library includes the module
Forwardable, which provides a DSL for delegating method calls to other objects that an instance may be holding onto. This DSL consists primarily of the method
def_delegator, which takes three symbols that identify, respectively, the receiver (the instance variable or method that the call is delegated to), the method to delegate, and an optional alias name for the method on the delegating class. From Ruby’s documentation:
Forwardable also provides def_delegators for delegating several methods to the same receiver without any aliasing.
One common delegation pattern involves a wrapper, known as a decorator, around an existing object to extend its functionality for a particular context. For this purpose, Ruby provides
SimpleDelegator, a class which is instantiated with a single argument, and will delegate any method that it does not define to that argument.
Decorators built with
SimpleDelegator are powerful because they support arbitrary nesting. An object can be wrapped by as many decorators as desired, and the resulting Matryoshka doll will respond to any method that any of its layers implements.
However, be wary of nesting too deeply, as outer layers can shadow inner layers, leading to difficult issues to debug. If you find yourself nesting decorators, consider instead implementing a larger composite object to use one decorator at a time.
Rails delegation sugar
Forwardable is useful for delegation in vanilla Ruby, Rails adds its own delegation DSL to all modules and classes. The two most useful methods are
delegate_missing_to. Like def_delegators, delegate accepts a list of symbols identifying the methods to delegate, but rather than taking the first argument as the receiver, it takes an options hash and looks at the key
This options hash can also include
:prefix, which will alias the methods with a prefix, and
:allow_nil, which will cause the delegated methods to return
nil instead of failing if the receiver is
delegate method to replace violations of the Law of Demeter, such as
account.owner.payment_method, with direct or aliased methods, like
account.owner_payment_method. However, beware delegations of methods that are themselves delegated, since this will encode your application structure into one class, making it harder to change in the future.
Rails’ other delegation method,
delegate_missing_to, takes a single symbol as its argument, which identifies a catchall receiver for any method the object itself does not implement. It’s not likely that you’ll need this, but if you want an object that is almost a decorator, but needs a little extra special behavior,
delegate_missing_to is the perfect tool. Say you’re writing a Rails presenter about the relationship between an Item, but you also need to know a little bit about the User who wants to buy the item.
Ruby and Rails each provide powerful tools for sharing code between objects, each of which serves specific purposes. Inheritance is perfect for creating a few specialized variants of existing classes. Mix-ins make it easy to share functionality across similar but unrelated classes (duck types), and to organize related code in large classes. Delegation helps you build decorators and to avoid violating the Law of Demeter. Each is a sharp knife, but when you know how and when to use them, there is no better tool.