web-dev-qa-db-fra.com

Apprendre le principe de responsabilité unique avec C #

J'essaie d'apprendre le principe de responsabilité unique (PRS) mais c'est assez difficile car j'ai beaucoup de mal à comprendre quand et ce que je dois retirer d'une classe et où je dois le mettre/l'organiser.

Je cherchais des documents et des exemples de code sur Google, mais la plupart des documents que j'ai trouvés, au lieu de le rendre plus facile à comprendre, le rendaient difficile à comprendre.

Par exemple, si j'ai une liste d'utilisateurs et à partir de cette liste, j'ai une classe appelée contrôle qui fait beaucoup de choses comme envoyer un message d'accueil et au revoir lorsqu'un utilisateur entre/sort, vérifiez la météo que l'utilisateur doit pouvoir entrer ou non et lui donner des coups de pied, recevoir des commandes et des messages utilisateur, etc.

D'après l'exemple, vous n'avez pas besoin de beaucoup de choses pour comprendre que j'en fais déjà trop en une seule classe, mais je ne suis pas encore assez clair sur la façon de la diviser et de la réorganiser par la suite.

Si je comprends le SRP, j'aurais une classe pour rejoindre le canal, pour le salut et au revoir, une classe pour la vérification de l'utilisateur, une classe pour lire les commandes, non?

Mais où et comment utiliser le kick par exemple?

J'ai la classe de vérification, donc je suis sûr que j'aurais toutes sortes de vérifications utilisateur, y compris la météo ou non, un utilisateur devrait être expulsé.

Ainsi, la fonction kick serait à l'intérieur de la classe de jointure de canal et serait appelée si la vérification échoue?

Par exemple:

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

J'apprécierais si vous pouviez me donner un coup de main ici avec du matériel C # facile à comprendre qui est en ligne et gratuit ou en me montrant comment je diviserais l'exemple cité et si possible quelques exemples de codes, des conseils, etc.

55
Guapo

Commençons par ce que signifie principe de responsabilité unique (SRP) signifie réellement:

Une classe ne devrait avoir qu'une seule raison de changer.

Cela signifie effectivement que chaque objet (classe) devrait avoir une seule responsabilité, si une classe a plus d'une responsabilité, ces responsabilités sont couplées et ne peuvent pas être exécutées indépendamment, c'est-à-dire que les changements dans l'une peuvent affecter ou même casser l'autre dans une implémentation particulière.

Une source incontournable à lire est la source elle-même (chapitre pdf de "Agile Software Development, Principles, Patterns, and Practices" ): The Single Responsibility Principle

Cela dit, vous devez concevoir vos classes de manière à ce qu'elles ne fassent idéalement qu'une seule chose et fassent bien une chose.

Pensez d'abord aux "entités" que vous avez, dans votre exemple, je peux voir User et Channel et le support entre elles par lequel elles communiquent ("messages"). Ces entités ont certaines relations avec L'une et l'autre:

  • Un utilisateur dispose d'un certain nombre de canaux qu'il a rejoint
  • Une chaîne compte plusieurs utilisateurs

Cela conduit aussi naturellement à faire la liste des fonctionnalités suivantes:

  • Un utilisateur peut demander à rejoindre une chaîne.
  • Un utilisateur peut envoyer un message à un canal qu'il a rejoint
  • Un utilisateur peut quitter une chaîne
  • Un canal peut refuser ou autoriser la demande d'un utilisateur à rejoindre
  • Une chaîne peut donner un coup de pied à un utilisateur
  • Une chaîne peut diffuser un message à tous les utilisateurs de la chaîne
  • Une chaîne peut envoyer un message d'accueil à des utilisateurs individuels dans la chaîne

SRP est un concept important mais ne devrait pas être autonome - tout aussi important pour votre conception est le Dependency Inversion Principle (DIP). Pour intégrer cela dans la conception, n'oubliez pas que vos implémentations particulières des entités User, Message et Channel doivent dépendre d'une interface abstraction ou plutôt qu’une mise en œuvre concrète particulière. Pour cette raison, nous commençons par concevoir des interfaces et non des classes concrètes:

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);
}

Ce que cette liste ne nous dit pas, c'est pour quelle raison ces fonctionnalités sont exécutées. Il vaut mieux placer la responsabilité du "pourquoi" (gestion et contrôle des utilisateurs) dans une entité distincte - de cette façon, les entités User et Channel ne doivent pas changer si le "pourquoi" change . Nous pouvons tirer parti du modèle de stratégie et de l'ID ici et nous pouvons faire en sorte que toute implémentation concrète de IChannel dépende d'une entité IUserControl qui nous donne le "pourquoi".

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;
        //..
    }
    //..
}

Vous voyez que dans la conception ci-dessus, SRP n'est même pas proche de la perfection, c'est-à-dire qu'un IChannel dépend toujours des abstractions IUser et iMessage.

En fin de compte, il faut s'efforcer d'avoir une conception flexible et faiblement couplée, mais il y a toujours des compromis à faire et des zones grises en fonction de l'endroit où vous attendez votre application à changer.

SRP pris à extrême à mon avis conduit à un code très flexible mais aussi fragmenté et complexe qui pourrait ne pas être aussi facilement compréhensible que du code plus simple mais un peu plus étroitement couplé.

En fait, si deux responsabilités doivent toujours changer en même temps, vous ne devriez sans doute pas les séparer en différentes classes, car cela conduirait, pour citer Martin, à une "odeur de complexité inutile". Il en va de même pour les responsabilités qui ne changent jamais - le comportement est invariant et il n'est pas nécessaire de le diviser.

L'idée principale ici est que vous devriez faire un appel au jugement où vous voyez que les responsabilités/comportements peuvent changer indépendamment à l'avenir, ce comportement est dépendant l'un de l'autre et changera toujours en même temps ("attaché à la hanche") et quel comportement ne changera jamais en premier lieu.

59
BrokenGlass

J'ai eu un temps très facile à apprendre ce principe. Il m'a été présenté en trois petites parties bouchées:

  • Fais une chose
  • Faites cette chose seulement
  • Faites cette chose enfin

Le code qui remplit ces critères remplit le principe de responsabilité unique.

Dans votre code ci-dessus,

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

UserJoin ne remplit pas le SRP; cela fait deux choses, à savoir saluer l'utilisateur s'il peut le rejoindre ou le rejeter s'il ne le peut pas. Il serait peut-être préférable de réorganiser la méthode:

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);
}

Fonctionnellement, ce n'est pas différent du code publié à l'origine. Cependant, ce code est plus facile à gérer; que se passe-t-il si une nouvelle règle commerciale est entrée en vigueur selon laquelle, en raison des récentes attaques de cybersécurité, vous souhaitez enregistrer l'adresse IP de l'utilisateur rejeté? Vous modifieriez simplement la méthode RejectUser. Et si vous vouliez afficher des messages supplémentaires lors de la connexion de l'utilisateur? Il suffit de mettre à jour la méthode GreetUser.

SRP, d'après mon expérience, permet un code maintenable. Et le code maintenable a tendance à faire beaucoup pour satisfaire les autres parties de SOLID.

21
Andrew Gray

Ma recommandation est de commencer par les bases: qu'est-ce que choses avez-vous? Vous avez mentionné plusieurs choses comme Message, User, Channel, etc. Au-delà du simple choses, vous avez également comportements qui appartiennent à vos choses. Quelques exemples de comportements:

  • un message peut être envoyé
  • un canal peut accepter un utilisateur (ou vous pourriez dire qu'un utilisateur peut rejoindre un canal)
  • une chaîne peut donner un coup de pied à un utilisateur
  • etc...

Notez que ce n'est qu'une façon de voir les choses. Vous pouvez abstraire n'importe lequel de ces comportements jusqu'à ce que l'abstraction ne signifie rien et tout! Mais, une couche d'abstraction ne fait généralement pas de mal.

De là, il y a deux écoles de pensée communes en POO: encapsulation complète et responsabilité unique. Le premier vous amènerait à encapsuler tous les comportements associés au sein de son objet propriétaire (résultant en une conception inflexible), tandis que le second le déconseillerait (résultant en un couplage et une flexibilité lâches).

Je continuerais mais il est tard et j'ai besoin de dormir ... Je fais de ceci un post communautaire, donc quelqu'un peut terminer ce que j'ai commencé et améliorer ce que j'ai jusqu'à présent ...

Bon apprentissage!

3
Igor Pashchuk