web-dev-qa-db-ja.com

C#で単一責任の原則を学ぶ

私は単一責任原則(SRP)を学習しようとしていますが、いつどのようなことを1つのクラスから削除し、どこに配置/整理する必要があるのか​​を理解するのが非常に難しいため、非常に困難です。

私はいくつかの資料とコード例を探し回っていましたが、見つけたほとんどの資料は理解しやすくするのではなく、理解しにくくしました。

たとえば、ユーザーのリストがあり、そのリストから、Called Controlクラスがあり、ユーザーが出入りしたときに挨拶やさようならメッセージを送信するなど、ユーザーが入室できるかどうかを確認します。そして彼を蹴り、ユーザーのコマンドやメッセージなどを受け取ります。

この例から、私はすでに1つのクラスに多すぎることを理解している必要はありませんが、後でそれを分割して再編成する方法については十分に明確ではありません。

私がSRPを理解していれば、チャネルに参加するためのクラス、挨拶とさようならのためのクラス、ユーザー検証のためのクラス、コマンドを読み取るためのクラスがありますよね?

しかし、たとえばどこでどのようにキックを使用しますか?

私は検証クラスを持っているので、天気を含め、ユーザーがキックされるべきではないなど、あらゆる種類のユーザー検証があるはずです。

したがって、キック関数はチャネル結合クラス内にあり、検証が失敗した場合に呼び出されますか?

例えば:

public void UserJoin(User user)
{
    if (verify.CanJoin(user))
    {
        messages.Greeting(user);
    }
    else
    {
        this.kick(user);
    }
}

オンラインで無料のわかりやすいC#資料、または引用された例を分割する方法と、可能であればいくつかのサンプルコード、アドバイスなどを提供していただければ幸いです。

55
Guapo

単一の責任の原則 (SRP)が実際に何を意味するかから始めましょう:

クラスの変更理由は1つだけです。

これは事実上、すべてのオブジェクト(クラス)が単一の責任を持つ必要があることを意味します。クラスに複数の責任がある場合、これらの責任は結合され、独立して実行できません。

これについて明確に読んでおく必要があるのはソース自体です(pdfの章 "アジャイルソフトウェア開発、原則、パターン、および実践" ): 単一の責任の原則

そうは言っても、クラスは理想的には1つのことだけを行い、1つのことをうまく行うようにクラスを設計する必要があります。

まず、あなたが持っている「エンティティ」について考えてみましょう。この例では、UserChannel、およびそれらの間の通信手段(「メッセージ」)を確認できます。これらのエンティティは、お互い:

  • ユーザーが参加しているチャンネルがいくつかある
  • チャンネルには多数のユーザーがいます

これにより、次のリストの機能が自然に実行されます。

  • ユーザーはチャンネルへの参加をリクエストできます。
  • ユーザーは自分が参加しているチャネルにメッセージを送信できます
  • ユーザーはチャンネルを離れることができます
  • チャネルは、ユーザーの参加リクエストを拒否または許可できます
  • チャンネルはユーザーをキックすることができます
  • チャネルは、チャネル内のすべてのユーザーにメッセージをブロードキャストできます
  • チャネルは、チャネル内の個々のユーザーに挨拶メッセージを送信できます

SRPは重要な概念ですが、それ自体ではほとんど成り立たないはずです。設計にとって同様に重要なのは 依存関係の逆転の原則 (DIP)です。これを設計に組み込むには、UserMessageChannelエンティティの特定の実装が抽象に依存する必要があることに注意してくださいまたは特定の具体的な実装ではなくインターフェース。このため、具体的なクラスではなくインターフェイスの設計から始めます。

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に依存しています。

結局のところ、柔軟で疎結合の設計を目指して努力する必要がありますが、アプリケーションを変更する場所を予想する場所によっては、トレードオフと灰色の領域が常に存在します。

私の意見ではextremeを採用したSRPは、非常に柔軟であるが断片化された複雑なコードにつながり、単純ではあるが多少密結合のコードほど容易に理解できない可能性があります。

実際、2つの責任がalways同時に変更されることが予想される場合、間違いなくそれらを異なるクラスに分割しないでください。これにより、Martinが引用されます。 「不必要な複雑さの匂い」へ。責任が変わらない場合も同様です。動作は不変であり、分割する必要はありません。

ここでの主なアイデアは、責任/行動が将来独立して変化する可能性がある場合に判断の呼びかけを行う必要があるということです。この行動は相互に依存し、常に同時に変化します(「ヒップで結ばれる」)そもそもどの振る舞いも決して変わらない。

59
BrokenGlass

私はこの原則を学ぶのにとても簡単な時間を過ごしました。それは私に3つの小さな一口サイズの部分で提示されました:

  • 1つのことを行う
  • そのことを行うのみ
  • そのことをしてくださいまあ

これらの基準を満たすコードは、単一責任の原則を満たします。

上記のコードでは、

public void UserJoin(User user)
{
  if (verify.CanJoin(user))
  {
    messages.Greeting(user);
  }
  else
  {
    this.kick(user);
  }
}

UserJoinはSRPを実行しません。つまり、参加できる場合はユーザーに挨拶し、参加できない場合はユーザーを拒否します。メソッドを再編成した方がよい場合があります。

public void UserJoin(User user)
{
  user.CanJoin
    ? GreetUser(user)
    : RejectUser(user);
}

public void Greetuser(User user)
{
  messages.Greeting(user);
}

public void RejectUser(User user)
{
  messages.Reject(user);
  this.kick(user);
}

機能的には、これは最初に投稿されたコードと同じです。ただし、このコードは保守が容易です。最近のサイバーセキュリティ攻撃のために、新しいビジネスルールがそれに当てはまった場合、拒否されたユーザーのIPアドレスを記録したいと思いますか?メソッドRejectUserを変更するだけです。ユーザーのログイン時に追加のメッセージを表示したい場合はどうなりますか?メソッドGreetUserを更新するだけです。

私の経験ではSRPは保守可能なコードを作成します。また、保守可能なコードは、SOLIDの他の部分を満たすために長い道のりをたどる傾向があります。

21
Andrew Gray

私の推奨は、基本から始めることです:何thingsを持っていますか? MessageUserChannelなどの複数のthingsについて言及しました。単純なthingsに加えて、 behaviorsあなたのthingsに属しています。動作のいくつかの例:

  • メッセージを送信できます
  • チャネルはユーザーを受け入れることができます(または、ユーザーがチャネルに参加できると言うかもしれません)
  • チャンネルはユーザーをキックすることができます
  • 等々...

これはそれを見る方法の1つにすぎないことに注意してください。抽象化が何も何も意味しないまで、これらの動作のいずれかを抽象化できます!しかし、抽象化の層は通常害を及ぼしません。

ここから、OOPには2つの一般的な考え方があります。完全なカプセル化と単一の責任です。前者は関連するすべての動作をその所有するオブジェクト内にカプセル化し(柔軟性のない設計をもたらす)、後者はそれに対してアドバイスします(疎結合と柔軟性をもたらす)。

続行しますが、遅いので少し眠る必要があります...私はこれをコミュニティ投稿にしています。誰かが私が始めたものを完了し、これまでに得たものを改善できるようにしています...

幸せな学習!

3
Igor Pashchuk