Don't mix in your privates

Written . Tagged Ruby.

Say we have this module:

1
2
3
4
5
6
7
8
9
10
11
module Greeter
  def greet(name)
    "HELLO, #{normalize(name)}!"
  end

  private

  def normalize(name)
    name.strip.upcase
  end
end

We can include it to make instances of a class correspond to a “greeter” interface:

1
2
3
4
5
6
class Person
  include Greeter
end

person = Person.new
person.greet("Joe")  # => "HELLO, JOE!"

Is greet the whole interface?

It is the only public methods the module gives us, but it also has a private normalize method, part of its internal API.

The risk of collision

The private method has a pretty generic name, so there’s some risk of collision:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
  include Greeter

  def initialize(age)
    @age = normalize(age)
  end

  private

  def normalize(age)
    [age.to_i, 25].min
  end
end

person = Person.new(12)
person.greet("Joe")  # => "HELLO, 0!"

The module’s greet method will call Person’s normalize method instead of the module’s – modules are much like superclasses in this respect.

You could reduce the risk by making the method names unique enough, but it’s easy to forget and reads poorly.

Extract a helper

Instead, you can move the module’s internals into a separate module or class that is not mixed in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Greeter
  module Mixin
    def greet(name)
      "HELLO, #{Name.normalize(name)}!"
    end
  end

  module Name
    def self.normalize(name)
      name.strip.upcase
    end
  end
end

class Person
  include Greeter::Mixin

  # …
end

Since the helper class is outside the mixin, collisions are highly unlikely.

This is for example how my Traco gem does it.

Introducing additional objects also makes it easier to refactor the code further.

Note that if the helper object is defined inside the mixin itself, there is a collision risk as Gregory Brown pointed out in a comment.

Intentionally mixing in privates

Sometimes, it does make sense to mix in private methods. Namely when they’re part of the interface that you want to mix in, and not just internal details of the module.

You often see this with the Template Method pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module Greeter
  def greet(name)
    "#{greeting_phrase}, #{name}!#{post_greeting}"
  end

  private

  def greeting_phrase
    raise "You must implement this method!"
  end

  def post_greeting
    # Defaults to empty.
  end
end

class Person
  include Greeter

  private

  def greeting_phrase
    "Hello"
  end

  def post_greeting
    "!!1"
  end
end

Summary

Mind the private methods of your modules, since they are mixed in along with the public methods. If they’re not part of the interface you intend to mix in, they should probably be extracted to some helper object.