DEV Community

Ivan Bondarenko
Ivan Bondarenko

Posted on

Include modules in Ruby classes with parameters

The problem:

We have 2 classes with different behaviors and appointments. But some methods have the same code:

class User
  def greeting
    puts "Hello!"
  end
end

class AdminUser
  def greeting
    puts "Hello!"
  end
end

user = User.new
user.greeting # => "Hello!"

admin = AdminUser.new
admin.greeting # => "Hello!"
Enter fullscreen mode Exit fullscreen mode

Ruby is a single inheritance language and if classes User and AdminUser already have different base classes it is impossible to extend from the second one. Anyway, if these classes don't have a base class, it is a bad way to create a base class for both of them special for inheriting the greeting method.

WORST solution:

class Base
  def greeting
    puts "Hello!"
  end
end

class User < Base
end

class AdminUser < Base
end
Enter fullscreen mode Exit fullscreen mode

Basic solution:

Modules in ruby are a way to get around single inheritance restrictions:

module Greetable
  def greeting
    puts "Hello!"
  end
end

class User
  include Greetable
end

class AdminUser
  include Greetable
end
Enter fullscreen mode Exit fullscreen mode

Complicate the task:

Our app is growing every day. I remind classes User and AdminUser have different behavior and appointment. We have in each class a person name, but in User it is full_name and in AdminUser it is just nick:

class User
  include Greetable

  attr_reader :full_name

  def initialize(full_name)
    @full_name = full_name
  end
end

class AdminUser
  include Greetable

  attr_reader :nick

  def initialize(nick)
    @nick = nick
  end
end
Enter fullscreen mode Exit fullscreen mode

And we decide to add the nick or the full name to the greeting method. In this architecture it is impossible, but we can create some third method like name and override it in each class:

module Greetable
  def greeting
    puts "Hello, #{name}!"
  end

  def name
    raise NotImplementedError, "not implemented method name"
  end
end

class User
  include Greetable

  attr_reader :full_name

  def initialize(full_name)
    @full_name = full_name
  end

  def name
    full_name
  end
end

class AdminUser
  include Greetable

  attr_reader :nick

  def initialize(nick)
    @nick = nick
  end

  def name
    nick
  end
end
Enter fullscreen mode Exit fullscreen mode

It does not look like a good variant, because if we have more than 2 classes we need to implement some hidden method in each one, and need to remember about method name whenever we include it. Also if for include some behavior we need to implement some other things in our main class it is a big pain and doesn't make our developer life easy.

Solution:

Create method [] in module Greetable with one parameter name_attribute which returns a new module with method greeting who is using our parameter for puts message:

module Greetable
  def self.[](name_attribute)
    Module.new do
      define_method :greeting do
        puts "Hello, #{public_send(name_attribute)}!"
      end
    end
  end
end

class User
  include Greetable[:full_name]

  attr_reader :full_name

  def initialize(full_name)
    @full_name = full_name
  end
end

class AdminUser
  include Greetable[:nick]

  attr_reader :nick

  def initialize(nick)
    @nick = nick
  end
end

User.new("Bob").greeting # => "Hello, Bob!"
AdminUser.new("root").greeting # => "Hello, root!"
Enter fullscreen mode Exit fullscreen mode

Notes:

It is important to use define_method in the module instead of just the usual def method_name because we need need to use closure for getting the name_attribute value. In the case when we use def method_name closure is not working inside the def code because it has independent scope.

Have a nice coding, guys!

Top comments (0)