Single Responsibility Principle on Rails Explained

A few weeks back we had a small drama about SRP. There were some smart comments, some stupid ones and a few funny jokes even, like that for example:

If I remember correctly it all started with this post. I’ve seen criticism on twitter saying that the post shows shitty code, that it’s more complex than it should be, that User class is definitely the best place to put code that creates a user and so on.

My general reply to this was that it is really hard to explain OO principles in a blog post with a few short code examples. The benefits of following SOLID and other principles are clearly visible after your project reached a certain level of complexity. When you start a new project and you immediately want to start rigidly applying things like SRP then your code will probably look awkward.

In this post I’ll try to explain my approach to SRP and what I do to follow that principle in a non-extreme but pragmatic way.

Understanding What Responsibility Is

First of all “responsibility” is a flexible term and it should be treated this way. You need to keep redefining it depending on the current state of your project. I like to think that the more complex your project becomes the more narrow the definition of responsibilities should be. What do I mean by that? It’s simple. Let’s use the example from the mentioned post: creating a user. This can be a trivial thing to do, a matter of 1 line of code in Rails. If that’s the case would you say that creating a user must be treated as a single responsibility? Hmm I don’t think so. It’s very likely that when you start a project then entire CRUD for User is trivial, basic operations handled by Rails itself without any work from your side. In such cases I think that CRUD can be treated as a single responsibility. It is, in fact, a pretty wide definition of responsibility but that’s ok since what we do is very basic.

To get it right and to avoid overcomplicating your code you need to pay close attention to the requirements of your project. Every time you need to add a new method or dependency to a class it will be reflected in your tests as they will become more complicated. If you do TDD then you will immediately notice that moment when a class becomes too complex and it should be broken down. If you don’t do that then you have a problem because you will notice overly coupled and complicated code eventually but it’s going to happen way later than if you did TDD from the beginning.

Narrowing Down Responsibility

If creating a user must involve things like sending out notification emails then it’s definitely a good reason to narrow down our definition of responsibility as now we have 2 responsibilities: saving user data in the database and sending notification email. We have a user class responsible for the former and a mailer class for the latter. If you add an after_create observer it will have the same effect as if you added an after_create hook to your User class. Having a standalone, explicit service class that handles creation of a user and sending a notification email is a better option.

It’s not exactly The Rails Way, I know, then I why is it better? It’s better because the code is nicely decoupled as each object is responsible for just one thing. User knows how to persist its data, Mailer knows how to send a notification email and “User Creator” service is responsible for this special case when we want to create a user and have a notification email sent. In your tests you don’t have to care about turning off observers, you always have an explicit way of creating a user without any side effects. You also have an explicit way of creating a user and sending a notification email. There are no “magic moments” in your code when some observer does something special. It’s all explicit, decoupled and easy to change.

Redefining Responsibilities

As your project evolves you will have to redefine various responsibilities. It’s not that hard especially when you do TDD. Here’s a short list of things that can help you in defining and redefining responsibilities:

  • Write pure unit tests isolated from Rails – this reveals object responsibilities, pay attention to number of methods and their length
  • In your isolated unit tests explicitly require dependencies – this will show you if a class depends on too many things and probably should be broken down
  • Watch carefully if test setup doesn’t become too complicated – setting up 10 mocks just to test one method? That means SRP is probably violated
  • Document your classes and methods – you should always be able to describe what your code does in a short sentence

Try following these guidelines and you shouldn’t have problems with defining and redefining responsibilities and it should help in following SRP in a sane way.

Summing Up

SRP is not about having 100 single-method classes in your code base. It’s about decoupled code that’s easy to change and extend. It’s your job to make sure SRP is not violated in a way that causes tight-coupling. You need to be sensitive to every addition to your classes. Every time you add a method to a class try to think if it’s maybe not that moment where “responsibility” should be redefined and narrowed down.

  • http://twitter.com/mostruszka Michał Ostruszka

    Excellent write-up. All that stuff covered here applies entirely to other object oriented languages and their frameworks, not only to Ruby and Rails. I’m Java dev on daily basis and agree completely with what you’ve described.

    • http://solnic.eu/ solnic

      Thanks! That’s true but I realized that after I hit “publish” button haha

  • http://coderberry.me Eric Berry

    Excellent article. This is how Grails implements business logic (using services). I wish more ruby devs understood this.

    • http://twitter.com/mostruszka Michał Ostruszka

      Yep , grails have it done the nice way. Sadly “services” are still so java-ish for some ruby devs.

  • http://twitter.com/elight Evan Light

    Generally agree.  However, TDD doesn’t necessarily make us any more aware of SRP violations than not TDDing.  What it does provide, however, is another *opportunity* to notice that we violated SRP.

    Definitely agree regarding a flexible definition of “responsibility”. In my experience, that definition tends to become more strict the larger a system gets.  While the system grows, it is useful to be more strict about it with respect to “core” objects in a system than newer objects.

    For a “core” object example, the “User” class is frequently core to the behavior of a system as most use cases/user stories begin with a User.  

    Newer classes can afford to be a little… well… sloppier.  Not every object/class is reused!  As a class is used in more parts of your app it then becomes far more worthwhile to define a stricter notion of its responsibilities.

    All classes are equal but some are more equal than others. ;-)

    TL;DR: the more important a class is to your app, the more important it is that it has a strictly defined notion of its responsibilities. 

    • http://solnic.eu/ solnic

      Re TDD – yeah I meant that it helps in catching SRP violations much earlier than when you don’t TDD :) Well, at least that’s my experience.

      In general it’s good to listen to your tests. They always tell you a lot of interesting and important stories about your code.

      • http://twitter.com/elight Evan Light

        Sure. But that’s for people who listen to their tests. ;-)

        Listening to your tests (and your code in general) is a more essential skill than even TDD.  And a shockingly large number of people don’t have that skill…

        • http://github.com/dkubb Dan Kubb

          Yeah, there was some initial question on twitter on whether or not Piotr should post this article. Like maybe this had been done to death.

          I can understand why some people might feel that way, but at the same time, like you said, there are a shockingly large number of people who haven’t developed basic TDD skills. They still view it as a primarily verification tool, rather than a design tool.

          Maybe it’s not a basic skill then, maybe it’s an advanced one.. I dunno. I just know we need more articles attacking things from different angles, so we can discuss our approaches out in the open and come to a better understanding of what does and doesn’t work for people.

          I’m not interested in finding “the one true way”, but I am interested in hearing the things that work for people and what doesn’t so that I can try them out for myself.

          • http://solnic.eu/ solnic

            Yesterday I asked on twitter if people consider TDD as a basic skill or rather an advanced one. Most people replied that it’s definitely advanced.

            I think the problem with TDD is that people think it must be very strict. As in “ALWAYS test first”. Which is often impossible. When you’re experimenting, learning your domain, trying to understand how things are supposed to work, it’s hard to write tests first. I think it’s fine to write code first in order to explore possible ways of solving some problem. What’s important though is that once you know what you should do then it’s time to TDD :) This should result in the final implementation that has good test-driven design. I do that and it works for me.RE: “the one true way” – I agree, there’s no such thing. There are many good ways of solving same problems. The best we can do is to share our knowledge so we can learn from each other!

        • http://solnic.eu/ solnic

          Well there are even people telling you it’s OK to skip some tests because it costs money #trololol

  • http://www.mikepackdev.com/ Mike Pack

    Distinguished article. The Ruby world harps heavily on startups, bootstrapping and “shipping it.” Likely the reason there’s a good bit of opposition to enterprise-level design.

    • http://solnic.eu/ solnic

      I think what’s happening now is that people start noticing problems with maintaining all those startups we’ve bootstrapped! That’s why topics like better OO design, following SOLID, TDD, separating persistence etc. are becoming more important than they used to be.

      It’s no longer “hey look I built this in 2 weeks!” – now it is “hey, we’ve been working on this for over 2 years and the codebase is a pile of mud. How do we fix that?”.

  • Paul Leader

    A nice balanced post.

    I think one of the things that has annoyed many people about the “SRP Police” is the perception that you should do all the things, all the time, right from the start.

    The mark of experience isn’t knowing al the OO patterns and principals, it’s knowing when and how to apply them. If you have one callback on a simple model, moving it into an observer doesn’t simplify things, it just means you have two short files a maintainer has to look in to find some logic. When that code grows you may hit the point where moving callbacks out into an observer.

    However I’ve been going off callbacks more and more recently, and prefer using service objects. Callbacks create implicit control flow that gets increasingly difficult to debug, your model ends up breaking the principle of least surprise. My rule of thumb is that callbacks should never have side-effects outside of the model. Using a before_save to ensure the integrity of the model and its relations or update caches/accumulators is fine, but using them to send emails, or poke Facebook etc is bad.

  • http://tersesystems.com Will Sargent

    I don’t know what drama and what code you’re referring to. This article would be much clearer if you had code snippets or gists.