56

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

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

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

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

SRP を理解していれば、チャンネルに参加するためのクラス、あいさつとさようならのためのクラス、ユーザー認証のためのクラス、コマンドを読むためのクラスが必要ですよね?

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

私は検証クラスを持っているので、天気を含むあらゆる種類のユーザー検証がそこにあると確信しています。ユーザーをキックする必要はありません。

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

例えば:

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

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

4

3 に答える 3

60

単一責任原則(SRP) の実際の意味から始めましょう。

クラスを変更する理由は 1 つだけである必要があります。

これは事実上、すべてのオブジェクト (クラス) が単一の責任を持つ必要があることを意味します。クラスに複数の責任がある場合、これらの責任は結合され、独立して実行できなくなります。つまり、特定の実装では、一方の変更が他方に影響を与えたり、壊したりする可能性があります。

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

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

最初に、あなたが持っている「エンティティ」について考えてください。あなたの例では、それらが通信するための媒体(「メッセージ」)を見ることができます。これらのエンティティは、互いに特定の関係を持っていますUserChannel

  • ユーザーは、参加している多数のチャンネルを持っています
  • チャネルには多くのユーザーがいます

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

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

SRP は重要な概念ですが、それ自体で成り立つことはほとんどありません。設計にとって同様に重要なのは、依存性逆転の原則(DIP) です。これを設計に組み込むには、UserMessageおよびエンティティの特定の実装は、特定の具体的な実装ではなく、抽象化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);
}

このリストが教えてくれないのは、これらの機能が実行される理由です。「理由」(ユーザーの管理と制御)の責任を別のエンティティに置く方がよいでしょう。この方法でUserChannel、「理由」が変わってもエンティティを変更する必要はありません。ここで戦略パターンと 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に依存しています。IUserIMessage

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

私の意見では、SRP を極端に使用すると、コードは非常に柔軟になりますが、断片化された複雑なコードになり、単純ではあるが幾分緊密に結合されたコードほど容易に理解できない可能性があります。

実際、2 つの責任が常に同時に変化することが予想される場合、Martin の言葉を引用すると、「不必要な複雑さの匂い」がするため、それらを異なるクラスに分けるべきではありません。同じことが、決して変わらない責任の場合です。動作は不変であり、分割する必要はありません。

ここでの主な考え方は、責任/行動が将来的に独立して変化する可能性がある場合に判断を下す必要があるということです。その行動は相互に依存しており、常に同時に変化します (「股関節で結ばれている」)。そして、そもそもどの行動が変わることはありません。

于 2011-09-29T02:40:47.497 に答える
21

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

  • 1つのことをする
  • そのことだけをする
  • それをうまくやる

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

上記のコードでは、

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

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

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の他の部分を実現するのに大いに役立つ傾向があります。

于 2012-05-18T20:35:19.930 に答える
3

私の推奨事項は、基本から始めることです:あなたは何を持っていますか?あなたは、、、などのような複数のことについて言及ました。単純なこと以外に、あなたはあなたのに属する行動も持っています。動作のいくつかの例:MessageUserChannel

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

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

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

先に進みますが、遅くて少し眠る必要があります...これをコミュニティ投稿にしているので、誰かが私が始めたことを終えて、これまでに得たものを改善することができます...

幸せな学習!

于 2011-09-27T04:25:43.050 に答える