During the last months I had a few controversial chats about the “Single Responsibility Principle” (SRP), which is a concept in object-oriented programming for better encapsulation. Interestingly, the same conversation flamed up again and again when discussing Reform’s validate method.
Since that “validate” method does a bunch of things I was accused of exposing “a method that breaks SRP”.
What Is Not SRP?
A Reform form object comes with a handful of public methods, only. Their names are ::new
, #validate
, #errors
, and #save
. There are a couple more but that’s not relevant now. As their names are pretty self-explaining let me briefly talk about #validate
.
Here’s how you use this obscure method.
result = form.validate(params)
So, what validate does is it first populates the form’s internal attributes with the incoming params
. It then runs the defined validations in that form instance and returns the result.
Several people complained that this is not a good API as it breaks SRP – the validate method was “doing too much”.
I don’t really know if SRP only applies to classes but I can say one thing for sure: SRP can in no way be used with methods. If you say “this method breaks single responsiblity” you are talking about private concerns within a class.
You’re right, because it’s a good thing to break up logic into small methods. But you’re wrong, because you’re talking about the private method stack and not the public API of a class.
In my understanding, when talking about SRP you talk about classes.
### What Is SRP?
I had this eye-opening moment in a brillant keynote by “Uncle Bob” at Lonestar RubyConf a few years back: An SRP’ed class is reflected by having exactly one public method.
Having this pretty simple rule, I admit that Reform is not SRP. To have a clean architecture, I should split Reform into one class per public method: Reform::Setup
, Reform::Validate
, and so on.
form = Reform::Setup.new(model).call result = Reform::Validate(form).call(params)
Each class would only expose the #call
method, in an SRP setup there’s no need to name the only public method, the class name tells you what’s going to happen.
Of course, this is super clumsy and no one wants to work with a single responsible “API”. 😀
As a side note, Reform does exactly that behind it’s manly back – it provides you all the necessary methods via one instance, then orchestrates to separate objects. You don’t need to know how it works as a user.
### About API Design.
I have no clue where it comes from, nevertheless, exposing as many methods as possible to your class’ user seems to be “OK” or even “cool”, coming from Rails where an unconfigured ActiveRecord model offers you 284 public methods right away.
During +10 years of designing open-source frameworks I realised that the more public methods I allow my users to call the more work it gets to change my framework’s API later. Deprecating public methods is a pain in the ass.
Coming back to Reform, people suggested to split #validate
into two public methods: One to populate (or “fill out”) the form instance, one to actually validate it afterwards.
The word “after” indicates only one of the problems you introduce by extending the API:
- Users will fuck it up. They will call
#validate
without calling#fill_out
then ask why validate doesn’t validate and then someone else will reply that they forgot to call#fill_out
before. - They will call
#validate
, then#fill_out
– in the wrong order. - Reform is a form object – there simply is no case where you want to fill out a form but then leave it un-validated.
I decided to leave the validate method as it is and I do not regret it. Acceptance for this rebelious method increased after improving its documentation.
Sum Up.
Don’t use SRP when talking about methods. It’s a concept to be used with classes that expose a single public method.
The more methods you expose, the more things can go wrong due to wrong order, not calling a method or general confusion. Don’t make methods public because they “could be helpful”. A good API has a limited set of methods, only. If people ask for more, think about moving it to a separate class.
Applying SRP to workflows and generally to objects in (Rails) apps, and orchestrating those, is one of the numerous interesting topics discussed in my upcoming book. Sign up for the mailing list!
I’m with you, there is no point in filling out a form with out validating it
My public API would be
form = Reform::Form.new()
isValid, errors = form.validate(params)
form.validate could easily delegate to a form validator class etc
And yes SRP is about a class not methods. If the form class starts dealing with the rendering of the form then yeah that may need looking at. The method validate may make the class violate SRP if it starts for example sending alerts on failed validation etc
LikeLike
Dead on as usual.
I am not sure why Rubyists in particular seem afraid of returning tuples and arrays 😦
I for one am fighting the nasty vein of imperative, stateful garbage I see in Ruby. To me that is the first step in SRP and SOLID in general.
Functional programming mixed with a bit of OO structure without state modification combined with TDD is the holy grail for me.
LikeLike
Wait what? Class has a single responsibility if it has just one method? That can be true but not always. What about String? It has many public methods. Should we split it into smaller classes? Of course not. Or what about a class that exposes a query interface. It will have many public methods as well, does it break SRP? Not really.
SRP is a really awkward principle, it can lead to ridiculous code just like putting too many responsibilities in a class can.
If you can explain what a given object is doing in a simple, short sentence or if it’s easy to look at a class and figure it out w/o any explanation then you did a good job. That’s pretty much it.
LikeLike
Thanks for your responses, guys! 🙂
Mike: Good point with the rendering. Actually, my next step (I also presented this at some conferences already) is to have the validate signatur like tihs.
…which is pretty much what you suggest!
thatrubylove: Welcome to my blog! Exactly, the problem with all those “DSLs” that many Rubyists create is the statefulness. Both user and maintainer have to keep track of what happened, it is really painful.
solnic: Ha, you didn’t read the post to “What Is SRP?” 😉 I know that a pure SRP-architecture would be awkward and you need orchestrating classes (like
String
, orReform::Form
) otherwise it will be super-functional and clumsy.However, I disagree that a class has a single responsibility just because it “does only one thing”, like
String
. This class has one domain, ok, but that’s another story.I guess it’s as always, solnic, some people use SRP when talking conceptually about a class, some only use it when the golden rule applies.
The
ActiveRecord::find
is a good example of SRP (if it was encapsulated inAR::Find
): it’s a gigantic method and all the other finders around it are just sugar that get delegated to the actual find method.LikeLike
Yeah all those rules are pretty vague anyway so you can’t be puristic about them. It will only lead you astray. Knowing rules + experience + a ton of common sense == winning 🙂
LikeLike