Tuesday, April 30, 2013

A Dynamic Factory

During my last iteration, one of my stories was to implement a way to start a new game for my iOS/Java/Ruby hodgepodge of a Tic Tac Toe game. This meant that I would need to have my server respond to multiple commands.

At that point, the only command it responded to was to make a move. I would post a request with the body equal to something like "square=1" and it would make a move in square 1. I didn't think it would be much work to allow it to respond to another command, but I quickly noticed a problem. If I added an if statement to check the command I was sending, what would happen when I needed to add yet another command? This looked like a clear open/closed violation.

I struggled to find a solution. After a couple missteps, I almost decided to put off fixing the issue and just implement the if statement, delaying the decision. Thankfully I decided to talk to my mentor, who suggested using a factory. I was familiar this pattern, having used it in the past. This is what my factory looked like.

require "newgame_handler"
require "square_handler"

class RequestHandler
  def self.create(request)
    name = find_key(request)

    if name == "newgame"
      NewgameHandler.new
    else
      SquareHandler.new
    end
  end

  ...
end

For every command I needed, I could just create a new class. My factory did make it easier to add new commands to my server, but it still felt wrong. This solution didn't really conform to the open/closed principle. Whenever I needed to add a new command, I would need yet another if statement. I really wasn't thrilled at that prospect. When I brought this up with my mentor, he showed me a Ruby function called Module.const_get.

Module.const_get allows you to pull in a constant that is loaded and floating around while your Ruby program is running by specifying its name. What made this so useful in my situation is that classes are constants. So now, using this function, I could grab a reference to my NewgameHandler by passing const_get the string "NewgameHandler." This gave me a way out of wrestling with my potential if/else chain.

require "newgame_handler"
require "square_handler"

class RequestHandler
  def self.create(request)
    name = find_key(request)
    Module.const_get(format_name(name)).new
  end

  ...
end

Here is my class after another iteration. As it stands, it makes use of Ruby's Module.const_get method and is now a dynamic factory. It instantiates classes on the fly, based on the command sent, and uses those created handlers to process the game. I'm starting to sense a pattern though. Maybe you can see it too. I still haven't gotten away from explicitly modifying the class for every new command. The new wrinkle, that I didn't notice before, is that I still have to require the file that the class lives in.

After slightly more searching, I ended up with current solution. I was able to remove the explicit requires by requiring the files based on the command name.

require "nil_handler"

class RequestHandler
  def self.create(request)
    name = find_key(request)
    begin
      require "#{name}_handler"
      Module.const_get(format_name(name)).new
    rescue NameError, LoadError
      NilHandler.new
    end
  end

  def self.find_key(request)
    request["Body"].keys.first
  rescue
    ""
  end

  def self.format_name(name)
    name.length > 0 ? "#{name.capitalize}Handler" : ""
  end
end

This solution does have its drawbacks. What if I can't load the right file or try to get a constant that doesn't exist? That's why I added in a NilHandler that does nothing with the request. That way my server keeps chugging along when there's a load error or a name error. There are still some potential security concerns because this handler could instantiate a class that I wasn't ready for. The worst I can see happening though is my server crashing because the returned class doesn't respond to the same interface as the other handlers.

It's possible that this solution was a bit of overkill for two commands or that it was premature. While, I'm not entirely sure about either of those, I did think it was pretty cool.

No comments:

Post a Comment