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!"
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
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
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
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
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!"
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)