I can’t think of any other language or framework where plain object use is in any way mysterious, but the number of questions on Stack Overflow about Rails is quite shocking. It’s as though the framework does so much, when someone comes across an area not covered it triggers a large volume of questions.

There are numerous situations where business entities or business interaction needs to be developed outside the scope of a persistent model, and both Rails and Ruby supply multiple ways to implement solutions. ActiveModel is Rails’s set of mixins to add some ActiveRecord-like functionality to objects with no persistent storage.

I have split this article up into two parts. A general introduction and an implementation without ActiveModel here in Part 1, and a detailed walkthrough of every ActiveModel module in Part 2. If you want to skip ahead to look at just ActiveModel go now, hopefully you will find the background and samples to be enough to get you up and running, or improve your depth on this largely ignored topic.

In order to provide examples which you have a shot of meeting in the real world there’s a sample application used for the article. It’s available on Github:

Download zip from Github
View Github project page

Objects Without Tables

So you have something you don’t want to persist, what are your options?

  • Create a bunch of instance variables in your controller, take sick days whenever you hear the term code review.
  • Create a hash and add to it on the fly, then store in one instance variable in the controller. Find a fluorescent color scheme for Sublime and hope no one can read the code off your screen.
  • Put them in persistent models, the Rails Guides don’t mention anything other than models, so we have to use them. At all times. For evermore. Evermore.
  • Two words: Session hash
  • Use Plain Old Ruby Objects (POROs)
  • Use ActiveModel or ActiveResource

As an aside, for about a decade and a half, I’ve been irked by the use of Plain Old [insert language] Object as a term. It seemed to start when Java Enterprise Beans became so popular, so quickly that they were ubiquitous to the point where using features of the language which predated them by 5 years had to have a special acronym. Same with Ruby, you know the term for Plain Old Ruby Objects is…. object. As its written in Ruby, I’m going to use the term Ruby object going forward.

I’m usually not a fan of using framework features for the sake of it, but I’m going to state now that I believe that we should be using ActiveModel::Model for all non-persisted objects, whenever possible. Why? You will have a consistent interface across all of your business entities, and it will be much easier  to convert your model to an ActiveRecord::Base later if when you need to. I’ll go further and state that using ActiveModel improves your coding by coaxing you to use familiar patterns, it will also make you assess what you are doing and reveal flaws in your design when you start tripping over exceptional code handling. In short, all the benefits you already get from Rails are extended to your non ActiveRecord objects.

Finally, your current controllers and views, plus your future self will appreciate you for it!

Why use ActiveModel?

If you’re still not convinced, lets take a purely hypothetical, absolutely not inspired from real events example. You have a 3rd party service such as Paypal, Stripe or Braintree. Lets further say that you want to isolate yourself from the high likelihood of change every year when that contract comes up for renewal, so you abstract away the specificity of your vendor’s api to isolate your changes when you switch to a new service.

You create your own models which closely mimic your service’s data, and classes which know how to connect to your 3rd party. One of those models is a Wallet which contains basic account information, and is a repository for credit cards. You are simply taking the data returned from your remote credit card service, converting it to your data structure and passing it on to the view to display to the user. This is a fairly standard facade pattern, and as storing such data through ActiveRecord would rarely be useful, you implement non-persistent models using Ruby objects.

A month later you receive an emergency request, someone has suddenly realized that when you call your service’s delete function, it really is deleting that card data and not just marking cards as deleted.  If a customer deletes a card they have used for your recurring billing service you will lose every chargeback dispute. You are asked to immediately, as in don’t go home until it’s done, log every call to Wallet. So now you need your facade to be persisted as an ActiveRecord. If you’ve used a Ruby object, maybe you only have a couple of lines to change, but its more likely you are throwing everything you’ve done away and replicating functionality in ActiveRecord. If you had used ActiveModel you can be as smug as me and whip up those changes in a few hours and you can decline the delivery of napping cots for your team.

Now for some warnings, ActiveModel is not an exact replica of ActiveRecord, and there are some surprising omissions. You won’t be able to simply swap out includes, and you will still have some worthless code work to do. Its not particularly burdensome work, you’re not putting in a double shift at the salt mine, but a few additional minutes investment into your classes now will save you a great deal of time later.

ActiveModel’s biggest flaw may be the lack of documentation and the precious few resources on the web So, after a mixture of code reading and experimentation I have some non-trivial examples and experiences to share.

A sample application

So lets take a slightly simplified example from a fictitious credit card service. We are going to store our customer’s credit cards in that remote service and we want to incorporate CRUD operations on them in a Rails application. We’re going to isolate ourselves from the remote API  by creating a facade with a Wallet and a Card model. We will delegate the remote calls and data marshaling to a module. With this simple design, changing our 3rd party service would only require us to change the mixin which communicates with the service and marshals the data.

For the purposes of this sample, I have also created the service, but instead of being a remote api, its a couple of ActiveRecord models created for the purpose of mimicking a remote api. I am going to use an ActiveSupport::Concern for the remote delegate and mix that into Wallet so we  so that our models are isolated from the remote service itself. I am going to have the Wallet delegate Card functionality so that access and updates to cards is handled in a single class.

Below is a screenshot of our wallet page. It’s typical of two models with has_many/belongs_to relationship. The only functionality outside the norm is the warnings at the wallet and card level.

wallet_screenshot

For this part, I am going to build with Ruby objects, comment on some of the pain points then in Part 2 I will convert to ActiveModel, going through examples of each module as it applies to the application. Source code is available on Github.

Some caveats

  • When necessary, I will use ActionView::Helpers. The multiple options for decorators, compositors and presenters are arguments for another day.
  • There is no user authentication, remote wallets are accessed by tokens which are either set as the session id or an Authentication token if the request is made through json.
  • Wherever possible I have tried to use code as close to a Rails scaffold as possible

The remote service and delegate

If you look at a 3rd party service such as Stripe or Braintree, they have rich, full featured APIs lovingly crafted by teams of engineers. I have distilled all those person-years of work and beautifully nuanced functionality into two calls: find_by_wallet_token and create_or_update(token, wallet). There, Sean’s credit card service is ready to roll. I create two simple ActiveRecord models to handle this functionality, RemoteCard and RemoteWallet. I did add in all the usual validations one would expect as well as a Test suite. Finally, I created some dummy data for the wallet so that there will always be a wallet object in response. This cuts down on the amount of non-illustrative work necessary for a sample application.

Service facades and delegates are subject of a lengthy articles themselves. I will briefly say that for this task I prefer mixing in the remote api delegate rather than using dependency injection. This is more closely aligned with ActiveRecord, and for better or worse that’s what almost all of us have picked when we create a new Rails application.

Lets have a very quick look at the Credit Card Wallet Service Delegate, cunningly called RemoteSbbWalletDelegate.

I am ignoring most of the tasks associated with marshaling data, the structures remotely are going to be identical to our local model, but there are some changes which need to be made to outgoing objects. When connecting to a 3rd party I would consider splitting the translation of data from the API calls themselves and mix both into the model.

There’s not a lot going on here, one method per remote api call. It is quite possible that the delegate class is where one would simplify the more torturous remote apis. If you’re connecting to something from 2002 for example, you may inadvertently be plugging into someone’s half finished SOA composition vanity project, and you may have some work to do to isolate that type of garbage from your application.

Solution 1: Using Ruby objects

So lets implement the application using Ruby objects. You could probably scaffold an ActiveRecord implementation and keep 90% of the generated code to complete this solution. There are some subtle differences, most notably that your controllers no longer have any helpers associated with the model, but almost all of the views, routing table and controllers should be identical to an active model solution. Lets jump into the Card and look at a few methods.

There’s not much substance to it, as a dumb container of values there’s methods for setting and validating data. The only semi-useful logic in here is for setting the expiration date. PlainCard will take a date, a string which can be parsed to a date or a mm/yy format to create an expiration date. The rest of the code is boilerplate rubbish for storing, validating and setting error messages. There are some more methods in the source code, but I hope that you are seeing that there are very few lines which are not involved in either validation or field management.

Let me call out one note here as it is going to haunt me for the remainder of this project. I am calling this one the Card Number Paradox. The remote service never returns a card number during a find or an update, but needs card number for editing or adding a new card. That makes card_number both a required field and a non-required field.

Another aside: Why not more private methods? I am not a fan of private or protected keywords. Ruby is a language which gives us a rich set of tools to dissect and rearrange any class as we please. Aside from documentation, private means nothing. I am not a fan of the java-ization of Ruby where we have started to pretend the language is something it is not. If  you or your engineers are not responsible enough to use Ruby then using a paper seat belt like private is not going to help you. In short, if you think private is going to prevent those methods from being called then your engineers aren’t even close to being good enough. Document methods which should only be called internally!

With that out of the way, lets look at PlainWallet. The class is about 170 lines of code, and it does nothing interesting at all. All that code is required to handle attributes, maintain object state and delegate PlainCard creation and searching. There may be one line which is custom-ish; a regex to determine if an email is well formed and a couple of lines to flag and choose a warning string if a non critical message should be displayed about the wallet.

The rest of it is this kind of tedium:

You can look at the self-written validations and errors in the source code. I am going to point out the method I use for messages, not errors, and how message state is maintained.

None of this is terribly unreadable or particularly challenging, but note I am attaching what amounts to a callback after certain methods are called. I think its brittle and not particularly clear from the code why its being done, and thinking logically of a growing team and application this is probably the most likely to be misunderstood and broken.

Even though its just a couple of ruby objects, I did code as Rails-ish as possible. I created new, update, delete and save methods, and allow the passing of parameters through the constructors like ActiveRecord.

The Controller and Views

Writing the model was straightforward, how about the controller and views? A wallet is usually implemented as a singular resource with the key or token being linked to the current user. So my routes are drawn drawn with a singular resource contained the cards nested inside.

I don’t often nest, but when I do I prefer shallow nesting. However, even with user security turned on, there’s something unnerving about allowing access to a credit card by resource id, so I am going to fully nest and override the regular resource id creation for cards. This will prevent the remote card token from ever being used on the site, or a user finding a card not within their own Wallet. I am going to use the session.id as the wallet key so that we can skip a sign in page or any other user functionality.

Without the resource helper, you will have to manually create links in your controller, another area built for errors and enigmatic code., here’s an example from PlainCardsController. Imagine coming across this with no comments.

The source code has comments, but I want to draw attention to the kind of custom code you’ll find yourself writing and the potential for mystery one can inject into them. As for this index method? I want my card ids to start at 1 and be the card’s index within its array, so I always have to offset here by -1 and offset on the view by +1. Fun fun fun.

So far so good. For Show, the controller and view are close-ish to an ActiveRecord backed resource, I throw in some view logic to show a badge on an expired card and set up links to edit the wallet and cards.

I am taking the easiest way out with view logic. Again, there are tons of resources on how to do warnings and formatting based on model data better than this, but they are outside our scope here. If you feel that helpers or logical conditions in the view are evil, close your eyes and grit your teeth for a bit.

Implementing Forms

We can’t use model backed form_for, so instead will have to use form tags. Not terribly burdensome, but this can become a real pain with nested forms. Fortunately, we don’t need one here, but I am planning on visiting Nested Forms in the future and will include ruby objects and ActiveModels so that we can all share some collective pain.

In my Wallet form, I have to use tag_helpers instead of form_for, but if you look, this is surprisingly close to a regular rails form. I have adjusted the error querying as I do not have a complex error object inside my wallet.

Now for the cards themselves. A couple of wrinkles. Firstly, there’s no way for Rails to know if I need a PATCH or POST, so I set the form method within the view itself and pass it in to the partial.

I am not a fan of this kind of coding, but the alternative is to query the Card object in the view and switch the method there, I would rather pluck out a fingernail.

The other oddity is that a card cannot save itself with my remote interface, it only exists in the context of a wallet, and the entire wallet must be saved together. Typically, credit card services have card resources which can be acted upon independently, but I wanted to add in some challenges which demonstrate the strengths and weaknesses of each solution type.

If you are unfamiliar with nested resources… well I don’t think there is a definitive resource, but I really like Pull Review’s blog tutorial.

Testing

While I agree with DHH way more than not, I can’t get by on fixtures alone. I love Factory Girl, and using it with Shoulda has all but eliminated my use of rSpec, Mocha and any other testing libraries. I created factories for PlainWallet and PlainCard, and though they are just Ruby objects, I only had to add one line to the factory definition to get them to work.

The method skip_create prevents FactoryGirl from trying to invoke save! on the factory class. The after(:build) block is the same as I would use for a has_many model, so this test data portable across data strategies.

I think you can deduce a lot about code through looking at the tests necessary to keep it maintained. If you look at he PlainCard and PlainWallet tests it should be very, very clear, that my PlainWallet and PlainCard tests are more about making sure the classes are constructed than testing business logic. If you take a look at the tests for RemoteCard and RemoteWallet, you should see a dramatic difference, I don’t have to waste time testing whether a custom find or valid method work and the tests are focussed on custom code.

At some point when you write one, or ten of these Ruby objects it should occur to you that you could extract about 80% of the code into a reusable module or two, which could then be used whenever you have to make objects such as these. You might even start to think of macro definitions for validation and other generic functionality, as well as error handling, identifier creation etc. You will probably also think that a callback mechanism would be much safer than remembering to code method calls. Fortunately, someone has already created that framework, so now we’re going to look at how ActiveModel handles the same requirements and what impact it has on our code.

Click here to move to part 2 where we will write the same functionality with active models.

Rails ActiveModel – objects without persistence

Leave a Reply

Your email address will not be published. Required fields are marked *