Thursday, January 29, 2015

A process to identify when the SRP principle is broken

Without concrete examples, software principles and practices can be hard to understand. Take the principles of SOLID - for a developer who is new to these principles, I could attempt to describe them, but without showing concrete examples it would be difficult to understand what each principle means.

Excluding SRP - after some quick Googling, it is easy to find examples when OLID principles are broken (and how to adhere to them). 

I would argue that SRP is the most subjective out the five principles: "the single responsibility principle states that every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility."

Ensuring that a class has only a single responsibility is not an exact science. Asking multiple developers to decompose functionality into responsibilities will nearly always result in different classes. Comparing to another principle like Dependency Inversion, you would expect most of the time developers would adhere to this principle the same way. 

So the following is an attempt to demonstrate a thought process that I use when building classes - and it is a way of having a science behind determining when a new responsibility has popped up.

It can basically be summed as - Inheritance vs Composition.

When adding a new behaviour / function to a existing class (responsibility) - I will ask myself: if I wanted to extend this function later on, would it make sense to create a new derived class (i.e. inherit from the existing class, and then override the function), or would it make sense that this new function would be encapsulated in new class (responsibility) resulting in the existing class being composed of this new class (or interface if you're correctly adhering to the DI principle).

The way to work that is to ask yourself another question - could the implementation of the function be extended / changed regardless of the inheritance hierarchy?

If the answer is yes, then it's looking very likely to be a new responsibility.

Answering no means the function does belong to the existing class (responsibility) since answering this question as no means you have identified you are specifically creating a new class to extend the function - i.e its the only reason why you would create a new class. You have determined that the behaviour is part of the responsibility.  Here is a concrete example:

Below is a trivial Address class, along with a derived MailingAddress. FormattedStreet is what we will focus on.

So, assuming you need to add a formatting behaviour somewhere - the Address class might seem the appropriate place to implement it (as it is below). However, if you ask the question: "could the implementation of the function be extended / changed regardless of the inheritance hierarchy", the answer should be yes.

This is because MailingAddress will automatically inherit this behaviour, and if we need to change the formatting, extending Address (i.e. overriding the FormattedStreet property in a new class) will mean MailingAddress will miss out and that may not be acceptable. Therefore you could safely say that SRP has been broken as the class is implementing more than one responsibility.  

 

The following is an implementation which still appears the same to the consumer of the class - i.e. there is still a FormattedStreet property, however the formatting implementation lives on a new class / interface, and Address is now composed of this class - since we've discovered it is actually it's own responsibility. The best part is not only will MailingAddress inherit the concrete version of IAddressFormatter through Address, you can also change the concrete version specific to MailingAddress (i.e. a concrete version of IAddressFormatter for mailing addresses). So it's the best of both worlds.

 

No comments:

Post a Comment