Where to put POROs in Rails apps

So, I’ve got this conundrum.

The more I learn about OO design (a journey which started in 2007, mind you!), the more I want to apply what I know about good design in my daily work. But it’s hard, being the sole developer building an MVP for a startup where every second counts. There’s an increasing cognitive dissonance between what I know I want to do, like unit testing, tdd, building small POROs for business logic and so on just like Sandi Metz and Avdi Grimm encourage us to do, and what Rails encourages through its file structure and what I end up doing in practice, which is just throwing another method into the User or CourseModule class and getting the feature out the door.

The conundrum is pretty simple; the Rails way doesn’t of itself lead you to write good code, it encourages you to think about everything within its structure, and doesn’t make obvious places for your own classes outside of Controllers and Models. I want to extract stuff out into single responsibility classes, but if I do that, what about Rails? Rails is the reason I’m using Ruby, after all; I want the productivity and programmer joy that comes from using it. But I also want to develop the craft of software design.

So I was overjoyed to come across this blog post by Vladimir Rybas on just this topic. Here he quickly outlines the problem and describes a few current approaches, most of which just don’t feel rails-y – and more importantly, mean you end up creating a completely custom file structure which makes it really hard to predict where stuff is going to be. That’s one of the joys of Rails, that pretty much everything has a home and that it’s consistent from Rails app to Rails app. It’s such a small but important thing to give up.

It’s his fourth one which hit the spot for me – using namespaces and subdirectories within the existing default Rails directory structure. Here’s a quick summary, but do read his post and his rationale, it’s great.

Here’s one of his examples which shows it in action

class User < ActiveRecord::Base
  include Lockable
  include Settings

  def can_follow?(user)
    FollowingPolicy.new(self, user).can_follow?
  end
end

This is where the files actually live

models
├── user
│ └── following_policy.rb
│ └── lockable.rb
│ └── settings.rb
└── user.rb

And this is the code in each of the files

# app/models/user/lockable.rb
module User::Lockable
  def lock_access!
    update(locked_at: Time.now)
    UserMailer.account_locked_email(self).deliver
  end

  # ...
end

# app/models/user/settings.rb
module User::Settings
  extend ActiveSupport::Concern

  included do
    store_accessor :settings
  end

  # ...
end

# app/models/user/following_policy.rb
class User::FollowingPolicy
  attr_reader :current_user, :other_user, :account_verification

  def initialize(current_user, other_user)
    @current_user = current_user
    @other_user = other_user
    @account_verification = current_user.account_verification
  end

  # ...
end
Advertisements