単一責任原則(SRP) の実際の意味から始めましょう。
クラスを変更する理由は 1 つだけである必要があります。
これは事実上、すべてのオブジェクト (クラス) が単一の責任を持つ必要があることを意味します。クラスに複数の責任がある場合、これらの責任は結合され、独立して実行できなくなります。つまり、特定の実装では、一方の変更が他方に影響を与えたり、壊したりする可能性があります。
これについて明確に読む必要があるのは、ソース自体です( 「アジャイルソフトウェア開発、原則、パターン、および実践」のpdfの章):単一の責任の原則
そうは言っても、クラスが理想的には1つのことだけを行い、1つのことをうまく行うようにクラスを設計する必要があります。
最初に、あなたが持っている「エンティティ」について考えてください。あなたの例では、それらが通信するための媒体(「メッセージ」)を見ることができます。これらのエンティティは、互いに特定の関係を持っています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 は完全にはほど遠いものであることがわかります。つまり、 anはまだ抽象化およびIChannel
に依存しています。IUser
IMessage
最終的には、柔軟で疎結合の設計を目指して努力する必要がありますが、常にトレードオフが必要であり、アプリケーションの変更が予想される場所によっては灰色の領域もあります。
私の意見では、SRP を極端に使用すると、コードは非常に柔軟になりますが、断片化された複雑なコードになり、単純ではあるが幾分緊密に結合されたコードほど容易に理解できない可能性があります。
実際、2 つの責任が常に同時に変化することが予想される場合、Martin の言葉を引用すると、「不必要な複雑さの匂い」がするため、それらを異なるクラスに分けるべきではありません。同じことが、決して変わらない責任の場合です。動作は不変であり、分割する必要はありません。
ここでの主な考え方は、責任/行動が将来的に独立して変化する可能性がある場合に判断を下す必要があるということです。その行動は相互に依存しており、常に同時に変化します (「股関節で結ばれている」)。そして、そもそもどの行動が変わることはありません。