Robert C. Martin’s Clean Code Tip of the Week #1: An Accidental Doppelgänger in Ruby
While working on RubySlim I came across an interesting dilemma. Consider the following two ruby functions:
def slim_to_ruby_method(method_name) value = method_name[0..0].downcase + method_name[1..-1] value.gsub(/[A-Z]/) { |cap| "_#{cap.downcase}" } end
def to_file_name(module_name) value = module_name[0..0].downcase + module_name[1..-1] value.gsub(/[A-Z]/) { |cap| "_#{cap.downcase}" } end
The first takes a method name in the SLIM syntax (which is equivalent to the Java syntax), and converts it to a method name in the Ruby syntax. The second translate the SLIM package name syntax into the Ruby file name syntax. The best way to explain this is to show you the specs (in RSpec form).
it "should translate slim method names to ruby method names" do @statement.slim_to_ruby_method("myMethod").should == "my_method" end
it "should convert module names to file names" do @executor.to_file_name("MyModuleName").should == "my_module_name" end
Notice that the implementation of these two functions is identical, yet their intent is completely different. So the question is: Is this duplicate code?
But the situation above gave me pause. The fact that these two functions do the same thing is an accident. One transforms function names, and the other transforms package names. It would be an ugly form of coupling to eliminate this duplication by deleting one of the functions and just using the other. We don't want the caller of slim_to_ruby_method to know anything about package names; and we don't want the caller of to_file_namem to know anything about function names.
We could rename the remaining function to camel_to_underscore, but we don't want either of the two callers to know anything about the implementation of the syntax transformation.
So perhaps this isn't duplication. . .
Of course it is! And the solution is simple enough. We create a function named camel_to_underscore with the same implementation as the others. Then we have the two other functions call it. This keeps the callers of the original functions from getting coupled to the implementation, while getting rid of the duplication.
def camel_to_underscore(camel_namme) value = camel_name[0..0].downcase + camel_name[1..-1] value.gsub(/[A-Z]/) { |cap| "_#{cap.downcase}" } end
def slim_to_ruby_method(method_name) camel_to_underscore(method_name) end
def to_file_name(module_name) camel_to_underscore(module_name) end
This is really an example of maintaining a single level of abstraction per function. Duplicate code always represents a missing abstraction. But having the original callers invoke camel_to_underscore would have caused them to do more than one thing by mixing high level concepts with the low level notions of camel case and underscores. By creating the two delegating functions, we manage to keep the level of abstraction consistent with the callers, while getting rid of the low level duplication.