Monday, December 29, 2008

Single Responsibility Principle

I really do like what Robert C. Martin has to say about OO but I just don't agree with his position on the Single Responsibility Principle. It sounds like a good principle but then when I try to apply it in my mind, problems arise.

Let's look at a few of the classes he ends up with in his paper on SRP. The Connection interface has two methods: dial (which takes a string) and hangup. Does this class really have only one reason to change? No, there are still a number of reasons why this class might need to change. There might be different ways of hanging up ("politely" for valid connections and abruptly for would-be hackers). There might be different ways of dialing (with or without the speaker, supplying credentials, selecting a protocol, specifying minimum modem speed, etc.). There might be modems that never hang up.

I would posit that most classes have more than one reason for change. Here are a few examples.

Rectangle - the data types for the coordinates may change (16-bit to 32-bit), we may want polar coordinates instead of Cartesian. We may even want non-euclidean rectangles.

String - The character set may need to change. The storage mechanism may change (e.g. list, array, linked blocks of memory). The allocation scheme may change.

Just as easily as someone can say there is only one reason for a class to change, someone can find out multiple reasons for that same class to change (with very few exceptions). It's quite subjective.

If you could split all your classes into single responsibility objects, you'd have a huge number of classes that each did very little. To do anything useful, you would need to enlist the help of many classes. This makes for poor comprehension of the system and poor usability.

I want there to be a one to one mapping between concepts and classes. If I have a concept of a file, and I want to read and write from a file, I will expect there to be a file object with read and write methods. If I have to create input streams and output streams and file-opener objects, I will not enjoy the experience. I want a file object. I don't care if, behind the scenes the file is using an input stream and a output stream and a file-opener. I just don't want to see them or need to know about them.

With this one concept per class approach, there is a greater chance of complexity if precautions aren't taken. To combat that complexity, I would consider breaking up the main concept into sub concepts (e.g. input, output, seeking, etc.) and then coding each sub concept as a separate class and then hide that class behind the facade of the main concept's class.

This approach also helps with testing. If each of the sub concept classes can be mocked and injected into the main concept's class, the class is easy to use and easy to test.

You still need to guard against combining incompatible concepts into one class (e.g. file management with searching directories) mostly to avoid confusing the user. Remember that class size and complexity can be managed by decomposing the main concept's class into other classes.