Refactoring in Ruby

I am reading Refactoring by Martin Fowler and wanted to share some of his refactoring strategies that I think will be most useful.

  1. Introduce Explaining Variable
  2. Extract Class
  3. Replace Data Value with Object
  4. Replace Nested Conditional with Guard Clauses
  5. Replace Parameter with Method
  6. Extract Interface

1) Introduce Explaining Variable

Why? You have a complicated expression.
What? Put the result of the expression, or parts of the expression, in a temporary variable with a name that explains the purpose.
Let me explain: It’s not fun to have a complicated method that’s difficult to read. To make it easier to read, you should pull out magic numbers and expressions into temporary variables that makes them method easier to read. For example, if I have to pass complciated parameters to a method, sometimes I’ll pull those parameters into a temporary variables first:

return method(customer_transactions - customer_returns)

customer_total = customer_transactions - customer_returns
return method(customer_total)

2) Extract Class

Why? You have one class doing work that should be done by two
What? Create a new class and move the relevant fields and methods from the old class into the new class.
Let me explain: The single responsbility rule tells us that a class should only be responsible for one thing. But it’s not always so easy to keep it that way. It’s very common for classes to start out with one responsbility and slowly but surely start taking on other responsbilities. When you refactor, it’s time to separate that one class into two (or more!). I think the hardest thing about this refactoring is trying to determine what should be in a new class.

For example: I can easily see a customer class starting out with data just about the customer like name, date created, and address. But then you add a loyalty rewards number. And before you know it you’ve added the loyalty rewards balance and other loyalty rewards data. Hopefully it becomes obvious that loyalty rewards should probably be its own class which a customer can instantiate.

3) Replace Data Value with Object

Why? You have a data item that needs additional data or behavior
What? Turn the data item into an object.
Let me explain: This one happens to me a lot. When you start out, data often seems simple enough to be an instance variable. A customer’s telephone number, for example, seems like it could just be a simple string. But eventually the client wants you to format the telephone number in a bunch of different ways depending on the type of report. Instead of working magic with a variable, you should go ahead and create a telephone_number class that holds all the related methods.

I am always reluctant to create a new class, as I suspect many other newb programmers are. We’ve got to go over that hesitancy.

4) Replace Nested Conditional with Guard Clauses

Why? You have a complicated conditional (if-then-else) statement.
What? A method has conditional behavior that does not make clear the normal path of execution.
Let me explain: Nested if-statements have a “bad smell”. You want to try to eliminate them with polymorphism or calling explanatory methods. This book shows another interesting alternative with use of guard clauses.

Guard clauses say “This is rare, and if it happens, do something and get out.” It means that if something unusual happens, you should return something and break from the method. Here’s an example:

def getPayAmount()
  if is_dead? == true
  else
    if is_separated? == true
    else
      if is_retired? == true
      else
        normalPayAmount
      end
    end
  end
end

def getPayAmount()
  return 0 if is_dead?
  return 0 if is_separated?
  return 0 if is_retired?
  normalPayAmount
end

One thing I find interesting is that this refactoring strategy encourages you to have multiple exits. I’ve always been told to minimize the use of multiple exits in one method. Fowler explains that it’s more important for your code to make sense than to care about how many exits exist. Guard clauses are special scenarios that require a special action, so it makes sense to return early in that situation.

5) Replace Parameter with Method

Why? An object invokes a method, then passes the result as a parameter for a method. The receiver can also invoke this method.
What? Remove the parameter and let the receiver invoke the method.
Let me explain: This is one that I really like doing to simplify code. While you need to decide how best to structure your code to make it easiest to read, I often like to eliminate local variables and call methods as parameters. And if your methods are named well then you should have no problem reading the code.

Example:

def get_total(price, quantity)
  base_price = price * quantity
  discount = get_discount(quantity)
  get_final_price(base_price, discount)
end

def get_total(price, quantity)
  base_price = price * quantity
  get_final_price(base_price, get_discount(quantity))
end

6) Extract Interface

Why?Several clients use the same subset of a class’s interface, or two classes have part of their interfaces in common.
What?Extract the subset into an interface.
Let me explain: Interfaces are good to use whenever a class has distinct roles in different situations. Extract Interface can be used for each role. Another useful case is that in which you want to describe the outbound interface of a class, that is, the operations the class makes on its server. If you want to allow other kinds of servers in the future, all they need do is implement the interface.

An interface is a helpful way to access only necessary data from an object without making objects too dependent on each other. Here’s an example of Extract Interface:

Let’s say that the customer class has a lot of different information about the customer, but the Loyalty System only cares about two things: the length of time a customer has been around and how many purchases the customer has made. We’re going to create an interface for the Loyalty system.

class LoyaltyInterface
  def get_customer_length(customer)
    date_today - customer.sign_up_date
  end

  def get_number_of_customer_purchase(customer)
    customer.purchases.size
  end
end

Now the loyalty system can interact with the LoyaltyInterface and not have to worry about parsing through the customer data to find the correct data and methods.

Hope you enjoyed these! Refactoring is one of my favorite things.  I’m sure you agree that it feels so good to clean up code!

Leave a Reply

Your email address will not be published. Required fields are marked *