Role Sharing using Modules in Ruby

Nitish Sharma
4 min readOct 10, 2019

Understanding Roles

Some problems require sharing behaviour among unrelated objects. This common behaviour is basically a role an object plays. Many of the roles needed by an application are obvious during the design phase, but it is also common to discover new roles during development.

Discovering Roles

We often end up discovering roles which require not only specific message signatures, but also specific behaviour. When a role needs shared behaviour we’re faced with the problem of organising the shared code.

Ideally this code would be defined in a single place but be usable by any object that wished to act as the duck type and play the role. The Ruby way to do this is through Modules.

Methods can be defined in a module and then the module can be added to any object. Modules thus provide a perfect way to share behaviour by allowing objects of different classes to play a common role using a single set of code. When an object includes a module, its methods become available via automatic delegation.

Basically the object receives messages, it doesn’t understand them, they get automatically routed somewhere else, the correct method implementation is magically found, it is executed, and the response is returned. Once we start putting code into modules and adding modules to objects, we expand this shared behaviour the object can acquire.

An object that directly implements few methods might still have a very large response set. The total set of messages to which an object can respond includes
• Those it implements
• Those implemented in all objects above it in the hierarchy
• Those implemented in any module that has been added to it
• Those implemented in all modules added to any object above it in the hierarchy

Organising Responsibilities and Removing Unnecessary Dependencies

Before we can decide whether to create a duck type and put shared behaviour into a module, we have to know how to do it correctly.

class ExpiryChecker
def expired?(object)
case object.class
when AuthenticationToken
expiration_time = 6.months
when Coupon
expiration_time = 30.days
when AdCampaign
expiration_time = 2.weeks
else
return false
end
return DateTime.current < object.created_at + expiration_time
end
end

This is an anti-pattern of checking class to know what message to send. Here the ExpiryChecker checks class to know what value to use and clearly ExpiryChecker knows too much. This knowledge doesn’t belong in ExpiryChecker, it belongs in the classes whose names ExpiryChecker is checking.

The fact that the ExpiryChecker checks many class names to determine what value to place in one variable (expiration_time) suggests that the variable name should be turned into a message, which in turn should be sent to each incoming object.

Discovering the Duck Type and Writing Shareable Code

class AuthenticationToken
attr_accessor :created_at
def expiration_time
6.months
end
end

class Coupon
attr_accessor :created_at
def expiration_time
30.days
end
end

class AdCampaign
attr_accessor :created_at
def expiration_time
2.weeks
end
end

class ExpiryChecker
def expired?(object)
return DateTime.current < object.created_at + object.expiration_time
end
end

This change replaces an if statement that checks the class of an object with a message sent to that same object. It simplifies the code and moves the responsibility for knowing the correct expiration days to the last object that could possibly know the correct answer, which is exactly where this responsibility belongs.

The ExpiryChecker clearly does not care about object’s class, instead it only expects it to respond to a specific message. This message-based expectation transcends class and exposes a role, one played by all objects. The ExpiryChecker expects the passed object to behave like something that understands expiration_time, that is, like something that is “expirable” — a duck type. Expirables must implement expiration_time but currently have no other code in common. Discovering and using this duck type improves the code by removing the ExpiryChecker’s dependency on specific class names, which makes the application more flexible and easier to maintain.

Requiring that other objects know about a third party, to get behaviour from a string complicates the code by adding an unnecessary dependency. This specific example illustrates the general idea that objects should manage themselves, that they should contain their own behaviour. If our interest is in object B, we should not be forced to know about object A if our only use of it is to find things out about B.

The above code snippet breaks this rule. The instigator is trying to know if the object object is ExpiryChecker. It doesn’t ask this question of object itself, it instead asks a third party, ExpiryChecker. The object should be able to respond to expired? on it own, which means expired? should be added to the interface of Expirable role

Extracting the Abstraction

class AuthenticationToken
include Expirable
attr_accessor :created_at def expiration_time
6.months
end
end
class Coupon
include Expirable
attr_accessor :created_at def expiration_time
30.days
end
end
class AdCampaign
include Expirable
attr_accessor :created_at def expiration_time
2.weeks
end
end
module Expirable
def expired?
return DateTime.current < created_at + expiration_time
end
def expiration_time
raise NotImplementedError
end
end
authentication_token = AuthenticationToken.new(created_at: DateTime.current - 10.days)
authentication_token.expired? => false
coupon = Coupon.new(created_at: DateTime.current - 2.weeks)
coupon.expired? => true
ad_campaign = AdCampaign.new(created_at: DateTime.current - 1.month)
ad_campaign.expired? => false

The rules for modules are the same as for classical inheritance. If a module sends a message it must provide an implementation, even if that implementation just raises an error indicating that users of the module must implement the method.

We moved the methods to the Expirable module, including the module and overriding expiration_time. Now that we have created this module other objects can make use of it to become Expirable themselves. They can play this role without duplicating the code.

--

--