Let's start with what the principle of single responsibility (SRP) really means:
A class should have only one reason to change.
This effectively means that each object (class) must have one responsibility, if the class has more than one responsibility, these responsibilities become related and cannot be performed independently, that is, changes in them can affect or even violate others in a particular implementation.
A certain person should read that this is the source itself (pdf chapter of Agile Software Development, Principles, Patterns and Practice ): The principle of shared responsibility
Having said that, you should develop your classes so that they ideally do just one thing and do one thing.
First, think about what “entities” you have, in your example I see User and Channel and the environment between them through which they exchange data (“messages”). These objects have certain relationships with each other:
- The user has several channels that he has joined.
- Channel has multiple users
This also leads, naturally, to the following list of functions:
- The user can request a connection to the channel.
- The user can send a message to the channel to which he has joined.
- User can leave channel
- A channel may prohibit or allow users to request a connection
- Channel can hit user
- A channel can broadcast a message to all users in a channel
- A channel can send a greeting message to individual users in a channel
SRP is an important concept, but it is hardly worth standing by itself. Equally important for your design is the Dependency Inversion Principle (DIP). To include this in your project, remember that your specific implementations of the User , Message and Channel objects must depend on an abstraction or interface, and not on a specific implementation. For this reason, we start by designing interfaces for non-specific classes:
public interface ICredentials {} public interface IMessage {
That this list does not tell us why these functions are performed. We'd better be responsible for the "why" (user management and control) in a separate object - thus, the User and Channel objects should not change if the "why" changes. We can use the strategy template and DI here and can have any specific implementation of IChannel , depending on the IUserControl object, which gives us the why.
public interface IUserControl { bool ShouldUserBeKicked(IUser user, IChannel channel); bool MayUserJoin(IUser user, IChannel channel); } public class Channel : IChannel { private IUserControl _userControl; public Channel(IUserControl userControl) { _userControl = userControl; } public bool Add(IUser user) { if (!_userControl.MayUserJoin(user, this)) return false;
You see that in the above project, SRP is not even close to perfection, i.e. a IChannel is still dependent on the abstractions of IUser and IMessage .
In the end, you need to strive for a flexible, loosely coupled design, but there are always trade-offs, and the gray areas also depend on where you expect your application to change.
SRP done to the extreme, in my opinion, leads to very flexible, but also fragmented and complex code, which may not be as clear as simpler, but somewhat more closely related code.
In fact, if two responsibilities are always expected to change at the same time, you might not need to separate them into different classes, as this could lead, quoting Martin, to “smell of Needless Complexity.” The same applies to duties that never change - the behavior is invariant, and there is no need to break it.
The main idea here is that you have to make a decision in which you will see that responsibilities / behavior can change independently in the future, the behavior of which depends on each other and will always change at the same time ("tied to hip" ), and what behavior will never change in the first place.