Article Image
read

I get asked, quite often: "Where do you put your business logic in a Rails model?" This question typically comes from .NET developers who are dabbling with Rails/Ruby and trying to wrap their heads around it. A bit of a slippery-slope - I'm no expert here - but I'll tell you what I do...

##The Problem

Let's say you have a web app where your customers pay a fee to access your content each month. Subscribers, in other words. There's a fair bit of logic here...

Typically, with a .NET app, you might kick up various classes to handle various concerns. Off the top of my head:*a
Customer. Perhaps inheriting from a User

*a Subscription, with some type of relationship to the Customer

*an AccountsReceivable class - something that monitors payments in and out and handles the "Business Stuff" for the Customer/Subscription bits

*a Charger class of some kind - something that heads out to your payment gateway and charges the Customer

*a BillingRun class - something that wraps up the idea of a batch of charges executed periodically (let's not worry about the mechanism)

*an interface or two - ISubscriber, IAccountant, ICharger - so you could dress up your app with Dependency InjectionI'm sure you can fill in the blanks as needed. The details aren't terribly important - what I want to focus on is the Structure and Approach. How would you do this with Rails and Ruby?

Well first off -

you can do exactly this if you like.

Ruby is completely object-oriented and supports all the Big OO concepts. But is this "The Ruby Way"? I'm not totally certain - but I have a feeling the answer is "no".

The reason, simply, is that Ruby is

capable of so much more

both aesthetically and functionally. I'll do my best to decribe why in just a second but first - a bit of a preamble.

If you were to look at my first pass at Tekpub - well you'd laugh at me I'm sure. I'm not a Ruby whiz and I still don't know very much - but I'm getting there. Some times I feel like I'm better at Ruby than I am at C# - which really isn't saying much.

I quipped on Twitter the other day:

The more Ruby I write, the more I realize I suck as a programmer.

That's not to say Ruby's hard. In fact it's just about the opposite. The language is mighty concise and every time I write something I know there

simply must be a clearer, more elegant way.

I bring this all up simply to say that what you're about to read is my attempt to make things clearer and more elegant. I'd love your opinion if you see something wonky...

What It Does, Not What It Is

That concept - focusing on what something is supposed to

do

in your app as opposed to what type of class it is... well that's sort of at the core of the whole dynamic language thing.

To that end - Ruby has

A very nifty feature called "mixins" which many of you will immediately recognize as "modules". In fact that's what they're called: modules.

Let's come back to our problem and think about it a bit more. If you boil this down - we have the need to define some behaviors in our app:*Something that Acts As A Subscriber

*Something that reacts to the presence of a new transaction

*Some type of payment processorSo, keeping our heads in Ruby land - we need to

define abilities

rather than shaping objects and type hierarchies. And you can do just that with Ruby Modules. Let's take a look...

Acts As Subscriber

I'm sure you've seen the "Acts As" naming convention in some of the gems that you can use for various functionality in Rails. The idea is that you're "bolting on" some ability to your class - something you can reuse as needed. There might be gems that do this - but bear with me for this example.

Let's define our module:

module ActsAsSubscriber

def start_subscription(plan) #... end

def is_current? #... end

def cancel_subscription #... end

def sync_subscription #... end end

If you're a C# developer, you might be curious as to why I'm naming these methods the way I am. The simple answer is that I'm defining some abilities here - it'll make more sense if we plug it in:

class Customer

Now, if I kick up the Rails console and play around:

rails c
customer = Customer.new
customer.startsubscription "monthly"
customer.is
current?
customer.cancel_subscription

As you can see, the methods I pop in my module are "mixed in" to the instance methods available on Customer. This allows me to do all kinds of fun things - like work with Braintree to create a recurring billing:

module ActsAsSubscriber

def startsubscription(plan) #vault the customer customerresult = Braintree::Customer.create(:id => self.id, :firstname => self.first, :lastname => self.last)

#you would also pass in credit card and billing info
#and get a token back... which you would pass in below

sub_result = Braintree::Subscription.create(:plan_id => plan, 
                                        :payment_method_token => "...")
self.subscription_id = sub_result.subscription.id
self.save                                    

end

def iscurrent? sub = Braintree::Subscription.find(self.subscriptionid) rescue nil return sub.status == "active" unless sub.nil? end

def cancelsubscription Braintree::Subscription.cancel(self.subscriptionid) end def sync_subscription #... end end

This is the Braintree payments API at work - and I can tell you it's a bit more involved then this :). Also, it's a better habit to

pass around hashes as arguments. So consider this a bit of pseudo-code.

At first it might seem like I'm embedding business logic inside my Customer class. But am I? No - I'm creating the ability for the Customer to become a Subscriber - and I'm working with the Braintree API in its own module - so it's clean.

I have exactly this module in production with Tekpub - and I called it "ActsBrainy" since it's directly tied to Braintree. I don't need to do this - but the behavior in the module is directly tied to the quirks of the Braintree API (good and bad).

Keeping Models Clean With Observers

So we have a shiney module that will interface with Braintree and manage our subscriber. Now we need the ability to somehow update the Customer when the subscription is paid.

With most recurring payment solutions (like Recurly) - they'll send you a ping (aka "WebHook") when a payment is successfully made. Braintree doesn't do that - but that's OK this approach will work in either scenario (which is splendid).

The first thing I want to do is create an Observer for my Customer. You can think of this as a simple "package of Events" - but that's not entirely what it is. An Observer is essentially a "Subscriber" (don't get confused - I mean this in a PubSub way) to events published by an ActiveRecord class. You can also define your own if you want.

So let's kick up an Observer:

rails g observer Customer

This will drop the observer (called CustomerObserver) into my models directory. For fun and good housekeeping - I typically move observers into an "observers" directory and make sure Rails will load it up:

in /config/application.rb

config.autoload_paths += %W(# {config.root }/app/observers)

Now let's make sure we load up our CustomerObserver when our app starts. Again, in /config/application.rb:

note that this is referencing the file_name, not the ClassName

config.activerecord.observers = :customerobserver

Inside the observer we can subscribe to various published events - in fact we have the

entire set of ActiveRecord callbacks that we can subscribe to right here.

I could probably subscribe to "after_save" or some other callback - but I'd have to write some code to figure out what it is that we're wanting to watch - which is simply

did they just pay their subscription?

So why not be explicit?

There is probably a better way to do this - if you know of one let me know...

I'll define a method in my CustomerObserver called "subscription_paid"

class CustomerObserver

This method does whatever needs to happen when a Customer has paid - which is primarily to create a Transaction in our database. Again - there's a lot more that goes on here in terms of finding the order and creating the transaction - but just assume it happens for now.

To call this Observer method, I pop it right into my ActsAsSubscriber module:

def syncsubscription
if self.payment
due? #ask Braintree if all's well - then pop our Observer notifyobservers(:subscriptionpaid) if is_current? end end

The method "notifyobservers" is private - but it works with the module here because, well, it's essentially part of the Customer class. Calling this method and passing in ":subscriptionpaid" as the method to fire will also send in the current instance (self) - and a new transaction is recorded against the subscription order.

But what about updating Customer privileges and so on?

Hooks On Hooks

This can get tricky quickly. Events triggering events can quickly become snarled and I'm not all that certain this is a good thing to do. HOWEVER, if we're keeping things simple and have lots of tests, we should be OK.

Let's create another Observer - this time for the Transaction - and I'll do it the same as above with the Rails generator (also making sure to load it in config/application.rb). This time I'll hook into the ActiveRecord callbacks so I don't need to define a custom method:

class TransactionObserver

This method gets fired automatically whenever a transaction is created. And it's bleedingly simple to read and understand. The "handlesubscriptionpayment" method here looks at the items in the order and sends a message to the customer to bump their privileges:

def handlesubscriptionpayment
@order.isyearly? ? @order.customer.bumpyearlyprivvies : @order.customer.bumpmonthly_privvies end

The "handle_refund" bits do the opposite.

I Probably Don't Know What I'm Doing

The nice thing about this whole setup is I've offloaded the processing to Braintree, so they can handle the monthlies. I've added a somewhat "organic" feel to the application - rather than wave my arms and ponder over high-concept models.

And I mean that literally - no kidding -

I walk around my office and wave my damn arms in the air as if grasping for architectural enlightenment

and every single time (usually after I give up and drive home) the answer hits me:

You're over-thinking it. Use the tools at your disposal, make it simpler.

It's disorienting to say the least, to write what amounts to a lot less code that does quite a lot. Now, I know you're going to fire up Visual Studio and we'll have ourselves a lovely back and forth about the merits of "your language" vs. Ruby. I love you for it.

Speaking of love - I trust my Spidey-senses and my experience, and I really like the way I've pealed out otherwise bloaty logic from my Customer model. I'd love to hear your thoughts!

Blog Logo

Rob Conery

I am the Co-founder of Tekpub.com, Creator of This Developer's Life, an Author, Speaker, and sometimes a little bit opinionated.


Published