55 votes

Apprendre le principe de responsabilité unique avec C#

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

J'ai cherché sur Google des documents et des exemples de code, mais la plupart des documents que j'ai trouvés, au lieu de faciliter la compréhension, la rendaient difficile.

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 à un utilisateur. qui fait beaucoup de choses comme envoyer un message d'accueil et d'adieu quand un message d'accueil et d'adieu quand un utilisateur entre/sort, vérifier si l'utilisateur vérifier si l'utilisateur peut entrer ou non et le renvoyer, recevoir les commandes et les messages de l'utilisateur, etc.

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

Si je comprends le SRP, j'aurais une classe pour rejoindre le canal, pour les salutations et les adieux, une classe pour la vérification de l'utilisateur, une classe pour lire les commandes, n'est-ce pas ?

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 d'utilisateur là-dedans, y compris si un utilisateur doit être renvoyé ou non.

Donc la fonction kick serait à l'intérieur de la classe channel join 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 que vous me donniez un coup de main en me proposant des documents C# faciles à comprendre qui sont en ligne et gratuits ou en me montrant comment diviser l'exemple cité et, si possible, des exemples de codes, des conseils, etc.

60voto

BrokenGlass Points 91618

Commençons par ce que fait 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) doit avoir une seule responsabilité, si une classe a plus d'une responsabilité, ces responsabilités deviennent 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.

Pour cela, il faut absolument lire la source elle-même (chapitre pdf de "Développement logiciel agile, principes, modèles et pratiques". ) : Le principe de la responsabilité unique

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

Tout d'abord, réfléchissez aux "entités" que vous avez, dans votre exemple je peux voir User y Channel et le support entre eux par lequel ils communiquent ("messages"). Ces entités ont certaines relations entre elles :

  • Un utilisateur a un certain nombre de canaux qu'il a rejoint.
  • Un canal a un certain nombre d'utilisateurs

Cela conduit naturellement à la liste suivante de fonctionnalités :

  • Un utilisateur peut demander à rejoindre un canal.
  • Un utilisateur peut envoyer un message à un canal qu'il a rejoint.
  • Un utilisateur peut quitter un canal
  • Un canal peut refuser ou autoriser la demande d'un utilisateur de se joindre à lui.
  • Un canal peut renvoyer un utilisateur
  • Un canal peut diffuser un message à tous les utilisateurs du canal.
  • Un canal peut envoyer un message d'accueil aux utilisateurs individuels dans le canal

L'ASR est un concept important, mais il ne doit pas se suffire à lui-même - l'ASR est tout aussi important pour votre conception. Principe d'inversion des dépendances (DIP). Pour incorporer cela dans la conception, n'oubliez pas que vos implémentations particulières de l'algorithme de l'arbre à cames (DIP) ne peuvent être utilisées. User , Message y Channel doivent dépendre d'une abstraction ou interface plutôt qu'une implémentation concrète particulière. C'est pourquoi 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 est préférable de confier la responsabilité du "pourquoi" (gestion et contrôle des utilisateurs) à une entité distincte - de cette façon, l User y Channel Les entités ne doivent pas changer si le "pourquoi" change. Nous pouvons tirer parti du modèle de stratégie et de DI ici et avoir n'importe quelle implémentation concrète de IChannel dépendent d'un IUserControl entité 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, l'ASR n'est même pas proche de la perfection, c'est-à-dire qu'une IChannel dépend toujours des abstractions IUser y IMessage .

En fin de compte, il faut s'efforcer d'obtenir une conception flexible et faiblement couplée, mais il y a toujours des compromis à faire et des zones d'ombre, en fonction de l'endroit où l'on se trouve. s'attendre à votre application à changer.

L'ASR prise au extrême à mon avis, cela conduit à un code très flexible, mais aussi fragmenté et complexe, qui peut ne pas être aussi facile à comprendre qu'un code plus simple mais un peu plus étroitement couplé.

En fait, si deux responsabilités sont toujours sont censés changer en même temps, vous ne devriez 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 invariable, et il n'y a pas besoin de les séparer.

L'idée principale ici est que vous devez déterminer les responsabilités/comportements qui pourraient changer indépendamment à l'avenir, les comportements qui dépendent les uns des autres et qui changeront toujours en même temps ("liés par la hanche") et les comportements qui ne changeront jamais.

21voto

Andrew Gray Points 565

J'ai eu beaucoup de facilité à apprendre ce principe. Il m'a été présenté en trois petites parties, de la taille d'une bouchée :

  • Faites une chose
  • Faites cette chose uniquement
  • Faites cette chose bien

Un code qui remplit ces critères satisfait au 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 la SRP ; il fait deux choses, à savoir, accueillir l'utilisateur s'il peut se joindre, 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);
}

D'un point de vue fonctionnel, il n'est pas différent du code initialement publié. Toutefois, ce code est plus facile à maintenir ; que se passerait-il si une nouvelle règle de gestion était établie selon laquelle, en raison des récentes attaques de cybersécurité, vous souhaitez enregistrer l'adresse IP de l'utilisateur rejeté ? Il vous suffirait de modifier 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.

D'après mon expérience, l'ASR permet d'obtenir un code facile à maintenir. Et un code facile à maintenir a tendance à contribuer grandement à la réalisation des autres parties de SOLID.

3voto

Igor Pashchuk Points 546

Je vous recommande de commencer par l'essentiel : qu'est-ce qui choses avez-vous ? Vous avez mentionné plusieurs choses comme Message , User , Channel etc. Au-delà de la simple choses vous avez aussi comportements qui appartiennent à votre 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)
  • un canal peut virer un utilisateur
  • et ainsi de suite...

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 signifie tout et rien ! Mais, une couche d'abstraction ne fait généralement pas de mal.

À partir de là, il existe deux écoles de pensée communes dans la POO : l'encapsulation complète et la responsabilité unique. La première vous conduirait à encapsuler tous les comportements liés à l'objet qui lui appartient (ce qui entraîne une conception inflexible), tandis que la seconde vous le déconseille (ce qui entraîne un couplage lâche et une flexibilité).

Je pourrais continuer, mais il est tard et j'ai besoin de dormir... J'en fais un post communautaire, pour que quelqu'un puisse finir ce que j'ai commencé et améliorer ce que j'ai fait jusqu'à présent...

Bon apprentissage !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X