Давайте начнем с того, что на самом деле означает Принцип единой ответственности (SRP):
У класса должна быть только одна причина для изменения.
Это фактически означает, что каждый объект (класс) должен нести одну ответственность, если у класса есть более одной ответственности, эти обязанности становятся связанными и не могут выполняться независимо, т.е. изменения одного могут повлиять или даже сломать другой в конкретной реализации. .
Определенное обязательное прочтение для этого является самим источником (глава в формате pdf от «Гибкая разработка программного обеспечения, принципы, шаблоны и практики» ): Принцип единой ответственности
Сказав это, вы должны разработать свои классы так, чтобы они в идеале делали только одну вещь и делали одну вещь хорошо.
Сначала подумайте о том, какие «сущности» у вас есть, в вашем примере я вижу User
и Channel
и среду между ними, через которую они общаются («сообщения»). Эти сущности имеют определенные отношения друг с другом:
- У пользователя есть несколько каналов, к которым он присоединился
- Канал имеет количество пользователей
Это, естественно, также приводит к следующему списку функций:
- Пользователь может запросить присоединение к каналу.
- Пользователь может отправить сообщение на канал, к которому он присоединился
- Пользователь может покинуть канал
- Канал может отклонить или разрешить запрос пользователя на присоединение
- Канал может пнуть пользователя
- Канал может транслировать сообщение всем пользователям в канале
- Канал может отправлять приветствие отдельным пользователям в
канал
SRP - это важная концепция, но вряд ли она должна стоять сама по себе - не менее важной для вашего дизайна является Принцип инверсии зависимостей (DIP). Чтобы включить это в проект, помните, что ваши конкретные реализации сущностей User
, Message
и Channel
должны зависеть от абстракции или интерфейса, а не от конкретной конкретной реализации. По этой причине мы начинаем с разработки интерфейсов, а не конкретных классов:
public interface ICredentials {}
public interface IMessage
{
//properties
string Text {get;set;}
DateTime TimeStamp { get; set; }
IChannel Channel { get; set; }
}
public interface IChannel
{
//properties
ReadOnlyCollection<IUser> Users {get;}
ReadOnlyCollection<IMessage> MessageHistory { get; }
//abilities
bool Add(IUser user);
void Remove(IUser user);
void BroadcastMessage(IMessage message);
void UnicastMessage(IMessage message);
}
public interface IUser
{
string Name {get;}
ICredentials Credentials { get; }
bool Add(IChannel channel);
void Remove(IChannel channel);
void ReceiveMessage(IMessage message);
void SendMessage(IMessage message);
}
Этот список не сообщает нам: , по какой причине эти функции выполняются. Нам лучше отнести ответственность «почему» (управление пользователями и контроль) в отдельную сущность - таким образом, сущности User
и Channel
не должны меняться в случае изменения «почему». Мы можем использовать паттерн стратегии и DI здесь и можем иметь любую конкретную реализацию IChannel
в зависимости от IUserControl
сущности, которая дает нам «почему».
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;
//..
}
//..
}
Вы видите, что в вышеприведенном дизайне SRP даже не близок к идеальному, то есть IChannel
все еще зависит от абстракций IUser
и IMessage
.
В конце следует стремиться к гибкой, слабо связанной конструкции, но всегда есть компромиссы, и серые области также зависят от того, где вы ожидаете изменения вашего приложения.
SRP, взятый до extreme , на мой взгляд, приводит к очень гибкому, но также фрагментированному и сложному коду, который может быть не так легко понятен, как более простой, но несколько более тесно связанный код.
На самом деле, если две обязанности всегда ожидаются, чтобы измениться в одно и то же время, вы, вероятно, не должны разделять их на разные классы, поскольку это привело бы, по словам Мартина, к "запаху ненужной сложности". То же самое относится и к обязанностям, которые никогда не меняются - поведение инвариантно, и нет необходимости его разделять.
Основная идея здесь заключается в том, что вы должны сделать суждение, когда вы увидите, что обязанности / поведение могут измениться независимо в будущем, причем это поведение зависит друг от друга и всегда будет изменяться в одно и то же время ( бедра ") и какое поведение никогда не изменится в первую очередь.