labs programming

Write Private Functions Not Private Methods

During refactoring, private methods are created in order to:

  • Eliminate duplication within the class
  • Clarify confusing and/or complex fragments of related code (Extract Method)

These are both great refactorings, but be cautious of classes with an excessive amount of private class or instance methods. This is a smell that often indicates a class is doing too much work. Help maintain cohesion by refactoring private methods into separate objects. In this post, we’ll look at a way of writing private methods that encourages future extraction.

Iceberg Classes and Hidden Abstractions

An iceberg class is a class that has more private methods (3? 5? 10?) than public methods. Private methods are often an indication of additional, hidden responsibilities. By adopting a functional style, private methods can be easily extracted into separate objects.

A private function is a private method that:

  • Calculates its result only from its arguments
  • Does not rely on any instance (or global) state

An Example – From Methods, to Functions, to Extraction

Below is a modified portion of the User model from a Ruby on Rails Tutorial sample app.


class User < ActiveRecord::Base
  attr_accessor   :password
  attr_accessible :name, :email, :password, :password_confirmation

  before_create :encrypt_password

  # public methods...

  private

  def encrypt_password
    self.salt = make_salt
    self.encrypted_password = encrypt(password)
  end

  def make_salt
    secure_hash("#{Time.now.utc}--#{password}")
  end

  def encrypt(string)
    secure_hash("#{salt}--#{string}")
  end

  def secure_hash(string)
    Digest::SHA2.hexdigest(string)
  end
end

When a User is created their salt and encrypted password are set. Four private methods are used to implement this.User#encrypt_password is called by ActiveRecord, which means we have no control over sending arguments to it, so it will have to remain a method. Of the remaining three private methods, only one User#secure_hash, is a function.

Let’s refactor the other two into functions.


class User < ActiveRecord::Base
  # same implementation as above...

  private

  def encrypt_password
    self.salt = make_salt(password)
    self.encrypted_password = encrypt(salt, password)
  end

  def make_salt(password)
    secure_hash("#{Time.now.utc}--#{password}")
  end

  def encrypt(salt, password)
    secure_hash("#{salt}--#{password}")
  end

  def secure_hash(string)
    Digest::SHA2.hexdigest(string)
  end
end

Encryption doesn’t feel like a User responsibility, so let’s extract it into a separate object.


class Encryptor
  def self.make_salt(password)
    secure_hash("#{Time.now.utc}--#{password}")
  end

  def self.encrypt(salt, password)
    secure_hash("#{salt}--#{password}")
  end

  def self.secure_hash(string)
    Digest::SHA2.hexdigest(string)
  end

  private_class_method :secure_hash
end

We’re still left with one private class function. This seems ok because it’s related to Encryptor‘s core responsibility. Also, Encryptor.make_salt is not a function because it relies on global state, Time.now; this will make unit testing it difficult. Let’s punt on fixing that for now, because this class is already an improvement.

Finally let’s update User by having it collaborate with our new Encryptor class.


class User < ActiveRecord::Base
  # same implementation as above...

  private

  def encrypt_password
    self.salt = Encryptor.make_salt(password)
    self.encrypted_password = Encryptor.encrypt(salt, password)
  end
end

The Single Responsibility Principle and Object-Oriented Ceremony

There are two disadvantages of extracting private functions into new classes:

  • Naming the new abstraction is difficult because it’s often a verb and not a noun
  • The message sender now has to now instantiate a class and send it a message

Our above User refactoring resulted in a somewhat awkward, doer class (Encryptor). I also only used class methods in Encryptor, essentially creating a namespace of functions. This eliminates the need to instantiate a separate class, but it doesn’t feel very object-oriented.

I don’t see a solution for either of these two disadvanages. They’re by-products of modeling software in an object-oriented way.

Keeping Classes Cohesive

Cohesive, single responsibility classes are easy to understand and reuse. Private methods are one indication that a class is beginning to take on additional responsibilities. By writing private methods in a functional style, you take the first step in preserving a class’s true responsibility.