It is very common, specially in legacy code, that we can find classes whose interface does "too much," or even worse, we define the "god class" where we dump a big part of the implementation of an application. A major problem is that when we need to update or extend functionality, specially when working in a team, it is the same single class that is modified over and over.
Responsibility: A reason for change.
The Single Responsibility principle targets cohesion. There should never be more than ONE reason for a class to change.
Each responsibility is an axis of change. If a class has more than one responsibility, then they become coupled. Changes to one responsibility may impair or inhibit the class' ability to meet the others.
In the following UML class diagrams we can picture this principle:
A class AB implements two responsibilities: A and B.
By separating responsibility B from A, it is clear for the class C what instance is the information expert to provide each one of the responsibilities.
Code sample:
Despite the following code is in C# it can be easily ported to Java or other OOP language.Wrong implementation of this principle:
1: interface IModem
2: {
3: void Dial(String number);
4: void Hangup();
5: void Send(char c);
6: char Receive();
7: }
8:
9: class Modem : IModem
10: {
11: Random random = new Random((int)DateTime.Now.Ticks);
12:
13: public void Dial(String number)
14: {
15: Console.WriteLine(
16: string.Format("Modem is dialing number {0}", number));
17: }
18:
19: public void Hangup()
20: {
21: Console.WriteLine("Call finished");
22: }
23:
24: public void Send(char c)
25: {
26: Console.WriteLine(string.Format("Sending {0}...", c));
27: }
28:
29: public char Receive()
30: {
31: return (char)(random.Next() % 256);
32: }
33: }
Good implementation of this principle:
1: interface IDataChannel
2: {
3: void Send(char c);
4: char Receive();
5: }
6:
7: class DataChannel : IDataChannel
8: {
9: Random random = new Random((int)DateTime.Now.Ticks);
10:
11: public void Send(char c)
12: {
13: Console.WriteLine(string.Format("Sending {0}...", c));
14: }
15:
16: public char Receive()
17: {
18: return (char)(random.Next() % 256);
19: }
20: }
1: interface IConnection
2: {
3: void Dial(string number);
4: void Hangup();
5: }
6:
7: class Connection : IConnection
8: {
9: public void Dial(string number)
10: {
11: Console.WriteLine(
12: string.Format("Dialing number {0}", number));
13: }
14:
15: public void Hangup()
16: {
17: Console.WriteLine("Call finished");
18: }
19: }
Now, we can implement the class Modem and delegate to the encapsulated members, the implementation of the responsibilities of data channel and connection.
1: class Modem : IDataChannel, IConnection
2: {
3: IDataChannel dataChannel;
4: IConnection connection;
5:
6: public Modem()
7: {
8: this.dataChannel = new DataChannel();
9: this.connection = new Connection();
10: }
11:
12: public void Send(char c)
13: {
14: this.dataChannel.Send(c);
15: }
16:
17: public char Receive()
18: {
19: return this.dataChannel.Receive();
20: }
21:
22: public void Dial(string number)
23: {
24: this.connection.Dial(number);
25: }
26:
27: public void Hangup()
28: {
29: this.connection.Hangup();
30: }
31: }
Conclusions:
We have seen the benefits of giving a single responsibility to a class, a low index in the complexity of code supportability and maintainability is always desired. By separating a responsibility we can extend such functionality without affecting the class client that uses it, this is a topic that I will cover in my next post about the Open-Closed principle.
[RobertMartin96] SRP: The Single Responsibility Principle, Robert Martin, 1996
Why is Modem implementing IDataChannel and IConnection? These should be passed into the constructor. Also, this way, any concrete implementations of IDataChannel and IConnection can be used by Modem.
ReplyDeleteOr perhaps I am misunderstand?
Delete