Ruby 101 for .NET Developers: Use blocks to help you refactor 5

Posted by jeff Tuesday, March 13, 2007 15:35:00 GMT

As I've learned more about blocks in Ruby, I've had more fun refactoring my code. I first learned about refactoring from Martin Fowler's excellent book about six years ago when I was doing C++ programming for a living. Although the examples were in Java, it was very readable, and that was my first nudge down the agile path. I then started to understand what unit tests were all about, because I could refactor if I already had unit tests written... and then it was on to extreme programming... and the rest is history.

But back to refactoring... I loved refactoring in C#. My refactoring mainly consisted of just breaking up long methods into shorter ones. Once in a while I would create a new class out of a bunch of methods if I felt the existing class was getting too many responsibilities (in other words, more than one :-). But mainly, I was just cutting and pasting code into new methods, to keep each method short and as high-level-looking as possible.

In Ruby, I pretty much do the same thing, but utilizing blocks gives me a weapon I didn't really have before. I suppose the best analogy in C#/VB.NET would be the use of a delegate, but in Ruby the syntax is nicer and easier to understand, at least for me.

Here's an example... I've been working on a payment gateway that processes credit card transactions. First I worked on the part that "preauthorizes" the credit card:


class PaymentGateway
  def self.preauth!(payment)
    begin
      if available? 
        inputs = create_inputs_for_preauth(payment)
        result = call_trust_commerce_api(inputs)
      else 
        result = unavailable_result(:preauth) 
      end

      rescue => e
    RAILS_DEFAULT_LOGGER.warn "Exception during PaymentGateway.preauth!: #{e.message}"
    result = { 'status' => 'error', 'error_type' => e.message }
      end

     create_authorization_from_result(:preauth, payment, result) 

     result
   end
end

No need to worry about the details here - basically I create the inputs I need to send to our payment processor, call the api with the inputs, and capture the result (the result is a hash); then I create an Authorization row in our database with the results; and finally I return the raw results I got from the payment processor.

Anyway, my tests passed, and I was happy. A few days later I started to work on the part where we "capture" the credit card (our payment processor calls it a "post authorization"):


def self.capture!(payment)
   begin
     if available? 
       inputs = create_inputs_for_capture(payment)
       result = call_trust_commerce_api(inputs)
     else 
       result = unavailable_result(:postauth) 
     end

  rescue => e
    RAILS_DEFAULT_LOGGER.warn "Exception during PaymentGateway.postauth!: #{e.message}"
    result = { 'status' => 'error', 'error_type' => e.message }
  end

  create_authorization_from_result(:postauth, payment, result) 

  result
 end

The alert reader will now be almost asleep, as the capture! code looks almost identical to, and just as boring as, the preauth! method. As good developers, we hate redundancy, right? I wanted to refactor all the common elements of these two methods into a single method that could be called to do the pre- and post-authorization steps, plus any other kind of credit card action I might need later.

As it stands, the methods have only two differences:

  1. The use of :preauth or :postauth as a parameter to a couple of functions
  2. The call to create the inputs is either create_inputs_for_preauth or create_inputs_for_capture

The first one was easy - I can just pass that parameter into the new function. It was the second part - creating the input object - that gave me pause. How would the new, genericized method know which method to call? Here's one way:


def self.authorize!(action, payment)
  begin
    if available? 
      inputs = action == :preauth ? create_inputs_for_preauth(payment) : create_inputs_for_capture(payment)
      result = call_trust_commerce_api(inputs)
    else 
      result = unavailable_result(action) 
    end

  rescue => e
    RAILS_DEFAULT_LOGGER.warn "Exception during PaymentGateway.#{action.to_s}!: #{e.message}"
    result = { 'status' => 'error', 'error_type' => e.message }
  end

  create_authorization_from_result(action, payment, result) 

  result
end

Ugly. What if I add a third action? And I knew I would be adding a third action later on called :credit, which will credit someone's credit card in case they return the product. Should I write a switch statement and update it every time I need to support a new kind of action? No, that didn't sound right. So I decided that the code calling this method would be responsible for creating the input data.

However, I didn't just want to pass the input data as another parameter, because the inputs aren't always needed. I only need the inputs if the payment processing is available at that moment (the available? method controls this behavior, and can be overridden when desired in certain circumstances). So what I needed was a way for the authorize! method to collaborate with the original calling code, and only when needed.

Ruby blocks to the rescue! Now the ternary-operator-turned-ugly-switch-statement code gets replaced with this:


inputs = yield

This means that the authorize! method will call back to the client code at that point. The client code will now responsible for supplying a block of code that can generate the appropriate input data:


self.authorize(:preauth, payment) { create_inputs_for_preauth(payment) }

So now, the final code looks like this:


  def self.preauth!(payment)
    self.authorize(:preauth, payment) { create_inputs_for_preauth(payment) }
  end

  def self.postauth!(payment)
    self.authorize(:postauth, payment) { create_inputs_for_capture(payment) }
  end

private

  def self.authorize!(action, payment)
    begin
      if available?
        inputs = yield
        result = call_trust_commerce_api(inputs)
      else
        result = tclink_unavailable_result(action)
      end

    rescue => e
      RAILS_DEFAULT_LOGGER.warn "Exception during PaymentGateway.#{action.to_s}!: #{e.message}"
      result = { 'status' => 'error', 'error_type' => e.message }
    end

    create_authorization_from_result(action, payment, result) 

    result
  end

Now I can easily add more actions without having to change the authorize! method, even though the input data might need to be generated in a different way for each eaction.

Keep in mind, there are many ways I could have refactored this stuff. I chose to use a block to help me come up with a solution this time, but there are certainly other ways you could have removed the redundancies, especially since refactoring techniques is often a matter of taste and style. There's no one right way to do it. But using a block is a technique that those of us new to Ruby might not use too often, so I thought I'd show an example of it here.

And now I get to refactor the authorize! method! :-)

Comments

Leave a response

  1. Nic Wise   March 26, 2007 @ 10:12 PM

    Oh, thanks very much. Thats just tipped me over to learning Ruby and RoR....

    ;-)

    Nice post - nice and easy to read (for a C# person), well explained etc. All good.

    /me heads off to find a good Ruby tutorial or book, and install RoR on my Mac.

  2. Blair   March 27, 2007 @ 06:28 AM

    Great post Jeff not being fluent in Ruby I still had the 'Ah Ha' moment when I read the final code. The idea of being able to pass code into something that you are calling has so much potential sort of a mini implementation of the Strategy Pattern.

    Cheers

  3. Jeff   March 27, 2007 @ 02:20 PM

    @Nic: Excellent! :-) I recommend the "Agile Web Development with Rails" by pragprog.com, and/or "Ruby for Rails" by David A. Black (best price is probably at amazon.com, though I'm not positive). For installing Rails on your Mac, try this: http://hivelogic.com/narrative/articles/ruby-rails-mongrel-mysql-osx.

    @Blair: Yes, Strategy is a probably a very good way to describe it. Hmmm. That would have saved me about five paragraphs if I had thought of that first :-)

  4. kino   May 24, 2008 @ 01:13 AM

    I have the reflection that the cogitatum is given continuously as an objective unity in a multi-form and changeable multiplicity of noematic descriptions, which belong determinately to it.

  5. Ellroy   May 30, 2008 @ 11:23 PM

    I now shift the weight of transcendental evidence of the consciousness of internal time from the ego to noetic acts.

Comment


(won't be published)