The following was appropriately published a week before I was to begin my new role as a Ruby developer at Sparkbox:
Now is a fantastic time to get into Ruby, in my opinion, whatever your background is.
My background is as a Java developer, one who spent a lot of time writing Spring bean configurations and SOAP web services. I was learning Ruby and felt drawn to the language for its permissive syntax.
I must now solve real-world problems—with only a basic understanding of Ruby’s ways. Could I use my basic OOP knowledge earned in the Java world to help bridge the gap to lambdas and modules?
Working It Out
Ruby is an object-oriented language. (I know this because I looked it up.) It stands to reason that I should be able to apply the principles of object-oriented design to programs written in Ruby. However, we aren’t only talking about Ruby, and Rails is opinionated software where certain techniques are valued over others. It’s also object-oriented; but like all frameworks, there’s no safety net once you’ve veered off the happy path.
External APIs are a common case for generalization in Java applications. Define the inputs and outputs, implement the external API, and use dependency injection to pass around the functionality where needed.
I recently had an opportunity to write some code around the Stripe API. Many Rails apps put this type of code into the model, but I didn’t make it down that path very far before I realized the mess it had caused. I know that tools exist to make this manageable. However, I was deep into a client project, and that’s not the best time to invest effort into learning those new tools. Others on our team suggested encapsulating the feature in a service class. This being a familiar concept from Spring, I had just what I needed to clean up the mess I made.
Without sharing the exact code, I can give an idea of how the final result took shape:
class GatewayService
def self.register(cc_token, customer = {})
account = Gateway::Account.create(name: user[:name].to_s, email: user[:email].to_s, credit_card: cc_token.to_s)
account.id
end
def self.pay(amount, customer = {})
account = Gateway::Account.get(customer[:external_id].to_s)
account.charge(amount.to_i)
end
end
class User < ActiveRecord::Base
## Omitting Rails stuff...
attr_accessor :service, :cc_token
def pay(amount)
service.pay(amount, external_id: self.external_id)
end
def register
self.external_id = service.register(self.cc_token, name: self.name, email: self.email)
self.save
end
def service
@service ||= GatewayService
end
end
Since I wanted to use the service class as a singleton, I implemented its methods as class methods. (Since then, I’ve learned about the singleton module in the standard library, which is an opportunity for improvement later or in another project.) I particularly liked how Ruby let me call class methods on a variable set to the value of the class itself. This stands in stark contrast to Java, where the class name must be prefixed. Or worse yet, calling them through reflection—but this isn’t how you’d do a singleton anyway. This made the final piece of the puzzle fall into place easily. Each instance of User has an instance of the service class with a sane default. Alternate implementations may also be injected in the future if we need to support other services of the same type.
There are certainly more Rails-like approaches to this problem. This approach also isn’t as refined as it could be, but I don’t think that detracts from the solution. Like with all opinionated software, there tends to arise debate when you step outside of its norms. However, this code let me close a story and move on to the next one. And that’s what matters most to the people who pay us to do what we love.