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