Tuesday, August 15, 2006

Removing kind_of? and is_a? from your code

The Object class of Ruby contains the identical methods is_a? and kind_of?. They take a single argument, a class, and returns true if class is the class of the object, or if class is one of the superclasses of the object or modules included in object. The kind_of? (or is_a?) method is generally used for determining behavior based on the type of class of a parameter.

Most often I've seen kind_of? appearing in methods that need different behavior depending on the class type of an argument. For example, in a SQL DSL I was recently working on we had a method that would surround string objects with quotes, but would simply call to_s on other objects such as instances of Symbol or Fixnum.
def columns(*args)
args.each |element|
if element.kind_of? String
@column_array << "'#{element}'"
else
@column_array << element.to_s
end
end
This code does solve the problem; however, I believe that the code is structured in response to the languages inability to overload methods and behave differently based on type.

An alternative solution is to take advantage of duck typing. If the method were rewritten to execute a method that any class could respond to, the if..else statement becomes unnecessary.
def columns(*args)
args.each |element|
@column_array << element.to_sql
end
end
Clearly, for this example to work each argument must respond to the to_sql method. Because Ruby's classes can be re-opened this change is trivial.
class String
def to_sql
"'#{self}'"
end
end

class Fixnum
def to_sql
self
end
end

class Symbol
def to_sql
self.to_s
end
end
An added benefit to duck typing is that you can pass any object that responds to to_sql. For example, if you wanted a class that would represent null in the database you could define a class that only responds to the to_sql method.
class DBNull
def to_sql
'null'
end
end
For more information on Duck Typing see the chapter in Programming Ruby.

5 comments:

  1. Anonymous7:26 PM

    Jay, duck typing examples you gave here a great tip for avoiding the is_a? situations. It also simply looks cleaner and handles all cases you specify with a clear definition of types. - ben @ http://rubyonrailsblog.com

    ReplyDelete
  2. Anonymous9:56 PM

    Very handy tip. If you're feeling keen and have a few classes you need to add methods to, you can define a helper method to make things extra pretty - this isn't going to format nice :(

    def add_to_sql_method klass, &block
    klass.instance_eval { define_method :to_sql, &block }
    end

    add_to_sql_method(String) {"'#{self}'"}
    add_to_sql_method(Fixnum) {self}
    add_to_sql_method(Symbol) {self.to_s}

    ReplyDelete
  3. Good advice on using duck typing instead of class based decisions.

    Bad example in your use case. Perhaps the most common security problem with web apps is SQL Injection attacks and your String#to_sql method is vulnerable as it fails to escape quotes.

    Using parameterized sql binding is preferable, but if you must concat strings into sql statements at least do this:

    class String; def to_sql
      "'#{self.gsub(/\\/,'\&\&').gsub(/'/,"''")}'"
    end; end

    ReplyDelete
  4. Anonymous8:29 AM

    @dbenhur: Thanks for the code sample. I completely agree that if your app is public facing you have to look out for situations like you describe.

    I haven't had to deal with these types of issues recently since I've been working on internal only apps.

    Thanks again for the feedback.

    ReplyDelete
  5. Anonymous6:32 AM

    Frankly, even internal applications should not be written in a lazy, insecure way.

    ReplyDelete

Note: Only a member of this blog may post a comment.