Robert C. Martin’s Clean Code Tip of the Week #1: An Accidental Doppelgänger in Ruby
While working onRubySlimI 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 anaccident. 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 ofslim_to_ruby_methodto know anything about package names; and we don't want the caller ofto_file_namemto know anything about function names.
We could rename the remaining function tocamel_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 namedcamel_to_underscorewith 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 invokecamel_to_underscorewould have caused them to do more thanone thingby 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.