Un constructeur qui valide ses arguments viole-t-il SRP?

J'essaie d'adhérer autant que possible au principe de responsabilité unique (SRP) et je me suis habitué à un certain schéma (pour le SRP sur les méthodes) qui repose fortement sur les délégués. J'aimerais savoir si cette approche est valable ou si elle pose de graves problèmes.

Par exemple, pour vérifier l'entrée dans un constructeur, je pourrais introduire la méthode suivante (l'entrée Stream est aléatoire, peut être n'importe quoi)

private void CheckInput(Stream stream)
{
    if(stream == null)
    {
        throw new ArgumentNullException();
    }

    if(!stream.CanWrite)
    {
        throw new ArgumentException();
    }
}

Cette méthode (sans doute) fait plus d'une chose

  • Vérifiez les entrées
  • Lance différentes exceptions

Pour adhérer au SRP, j’ai donc changé la logique pour

private void CheckInput(Stream stream, 
                        params (Predicate predicate, Action action)[] inputCheckers)
{
    foreach(var inputChecker in inputCheckers)
    {
        if(inputChecker.predicate(stream))
        {
            inputChecker.action();
        }
    }
}

Ce qui est censé ne faire qu’une chose (le fait-il?): Vérifiez l’entrée. Pour le contrôle effectif des entrées et la levée des exceptions, j'ai introduit des méthodes telles que

bool StreamIsNull(Stream s)
{
    return s == null;
}

bool StreamIsReadonly(Stream s)
{
    return !s.CanWrite;
}

void Throw() where TException : Exception, new()
{
    throw new TException();
}

et peut appeler CheckInput comme

CheckInput(stream,
    (this.StreamIsNull, this.Throw),
    (this.StreamIsReadonly, this.Throw))

Est-ce mieux que la première option, ou est-ce que j'introduis une complexité inutile? Est-il possible d'améliorer encore ce modèle, s'il est viable?

66
J'ai réfléchi à cela et je suppose que cela oblige l'appelant de CheckInput à connaître tous les types de validateurs existants; ce qui rend en réalité l’existence de la méthode inutile à l’OMI. Vous pouvez revenir au formulaire précédent dans le premier exemple et renommer la méthode en ValidateStream. Le constructeur l’appellerait ainsi que d’autres méthodes similaires pour la validation de chaque paramètre. Si vous aimez cette approche, je peux modifier ma réponse et donner quelques exemples.
ajouté l'auteur iMatoria, source
Je pourrais soutenir que CheckInput fait toujours plusieurs choses: il itère à la fois sur un tableau et en appelant une fonction de prédicat et en appelant une fonction d'action. N'est-ce pas alors une violation du PÉR?
ajouté l'auteur Dee, source
Oui, c'est ce que j'essayais de dire.
ajouté l'auteur Dee, source
A part: je considère que jeter l'exception générale sans aucun message est horrible. Faites une faveur à vos lecteurs de journaux (== vous) et mettez autant d’informations que possible dans le message, début , pour un Argument * Exception , avec le nom du code source de l'argument. Si votre mécanisme de vérification des entrées ne le permet pas, il est cassé.
ajouté l'auteur Brock Woolf, source
@ Casey Le point de «responsabilité unique» est que toutes les personnes impliquées dans un projet (toutes les parties prenantes, et en particulier celles travaillant activement dans l'équipe de projet) ont une compréhension commune de la ou des responsabilités différentes de chaque composant du système. SRP n'est inutile que si tout le monde a une vision totalement différente de l'architecture globale du système, auquel cas il s'agit d'un problème humain/de communication. Il est extrêmement important de disposer de limites bien identifiables et bien comprises pour les «responsabilités» dans toute conception, et c'est le noeud du PRS.
ajouté l'auteur Ben Cottrell, source
@Andy Je pense que je comprends ce que vous dites, mais je dirais que vous le regardez sous le mauvais angle - contrairement à LSP/DIP/etc, il faut plus de contexte que de code ou d'architecture - la définition de "raison de changement "découle des exigences et des attentes spécifiques à un projet. Cela signifie que quelqu'un en dehors de votre équipe ne peut probablement pas vous dire si votre conception enfreint le PRS, mais au sein de votre équipe, il devrait être possible de convenir d'une définition du terme "raison de changer" pour un composant, sinon le drapeau est un indicateur rouge. lui-même peut avoir des problèmes plus larges de communication
ajouté l'auteur Ben Cottrell, source
@Andy, je pense que vous comprenez mal - les responsabilités sont dictées par les attentes des parties prenantes d'un projet. C'est pourquoi Bob Martin utilise la définition "Une raison de changer" ; En d'autres termes, les parties prenantes d'un projet s'attendent à des améliorations et à des changements particuliers, spécifiques aux exigences . L'objectif principal de SRP est qu'une équipe de projet doit comprendre ces attentes et que tous les membres de cette équipe doivent partager la même compréhension de la "responsabilité" des composants de leur solution. La définition est hors de propos pour quiconque en dehors de cette équipe.
ajouté l'auteur Ben Cottrell, source
Il semble que la seule responsabilité de SRP soit de créer un maximum de confusion parmi les apprenants, et cette responsabilité est très bien gérée.
ajouté l'auteur gnasher729, source
@BenCottrell Je comprends, mais ce que vous suggérez ne mappe pas facilement au code et à l'architecture. C'est tellement vague qu'il est inutile.
ajouté l'auteur Andy, source
@BenCottrell Et pourtant, nous avons des centaines de questions sur la distance qui sépare SRP. Il semble peu probable que "SR" puisse être facilement défini par une équipe arbitraire non plus.
ajouté l'auteur Andy, source
Indépendamment de la valeur du "principe" de responsabilité unique, l'exactitude du code est toujours plus importante que de tels principes, et la validation des arguments est l'un des moyens les plus rapides de trouver des erreurs dans votre code.
ajouté l'auteur Frank Hileman, source
Le titre précédent "Cette approche du principe de responsabilité unique est-il valable" n'était pas une bonne description de la question, mais le titre actuel est tout simplement faux. La question n'est pas de savoir comment faire un bon constructeur. En fait, je ne l'ai regardée que parce que je pensais que c'était une question différente.
ajouté l'auteur R. Schmitz, source
N'oubliez pas que le but de ces principes logiciels est de rendre le code plus lisible et maintenable. Votre CheckInput original est beaucoup plus facile à lire et à gérer que votre version refactorisée. En fait, si je rencontrais votre dernière méthode CheckInput dans une base de code, je la mettrais au rebut et la récrirais pour qu'elle corresponde à ce que vous aviez initialement.
ajouté l'auteur Minihawk, source
il est important de se rappeler que c'est le principe de la responsabilité : pas le principe d'action . Il a une responsabilité: vérifier que le flux est défini et accessible en écriture.
ajouté l'auteur David Arno, source
... votre méthode fait toujours plus d'une chose. Il génère des 1 et des 0.
ajouté l'auteur dwtorres, source
Ces "principes" sont pratiquement inutiles car vous pouvez simplement définir la "responsabilité unique" de la manière que vous souhaitez aller de l'avant, quelle que soit votre idée initiale. Mais si vous essayez de les appliquer de manière rigide, je suppose que vous vous retrouvez avec ce type de code qui, pour être franc, est difficile à comprendre.
ajouté l'auteur Casey, source
@KAlanBates - Oui, nous avons déjà eu cet argument;) Voir les commentaires d'IngenSchenau
ajouté l'auteur user49630, source
@ Casey Merci, compris. Évidemment, j'ai dépassé le stade et j'ai amené le tout à un point préjudiciable à la compréhension.
ajouté l'auteur user49630, source
@ BartvanIngenSchenau Oui, j'ai effectivement réfléchi à cela. En fait, c’est la raison pour laquelle j’ai écrit "(le fait-il?)" après avoir déclaré que cela faisait une chose. Essayez-vous de faire comprendre que vous pouvez conduire le SRP trop loin?
ajouté l'auteur user49630, source

12 Réponses

SRP est peut-être le principe logiciel le plus mal compris.

Une application logicielle est construite à partir de modules, qui sont construits à partir de modules, qui sont construits à partir de ...

En bas, une seule fonction telle que CheckInput ne contiendra qu'un tout petit peu de logique, mais chaque module successif encapsulera de plus en plus de logique , ce qui est normal

SRP ne consiste pas en une seule action atomique . Il s’agit d’une responsabilité unique, même si cette responsabilité nécessite plusieurs actions ... et finalement, il s’agit de la maintenance et de la testabilité :

  • il favorise l'encapsulation (éviter les objets divins),
  • il favorise la séparation des préoccupations (en évitant les changements dans toute la base de code),
  • cela facilite la testabilité en réduisant l'étendue des responsabilités.

Le fait que CheckInput soit mis en œuvre avec deux contrôles et soulève deux exceptions différentes est non pertinent .

CheckInput has a narrow responsibility: ensuring that the input complies with the requirements. Yes, there are multiple requirements, but this does not mean there are multiple responsibilities. Yes, you could split the checks, but how would that help? At some point the checks must be listed in some way.

Comparons:

Constructor(Stream stream) {
    CheckInput(stream);
   //...
}

contre:

Constructor(Stream stream) {
    CheckInput(stream,
        (this.StreamIsNull, this.Throw),
        (this.StreamIsReadonly, this.Throw));
   //...
}

Maintenant, CheckInput en fait moins ... mais son appelant en fait plus!

Vous avez déplacé la liste des exigences de CheckInput , où elles sont encapsulées, à Constructeur , où elles sont visibles.

Est-ce un bon changement? Ça dépend:

  • Si CheckInput n'y est appelé que: c'est discutable, d'une part, il rend les exigences visibles, d'autre part, il encombre le code;
  • Si CheckInput est appelé plusieurs fois avec les mêmes exigences , il viole DRY et vous avez un problème d'encapsulation.

Il est important de comprendre qu'une seule responsabilité peut impliquer un lot de travail. Le "cerveau" d'une voiture autonome a une seule responsabilité:

Conduire le véhicule jusqu'à sa destination.

C’est une responsabilité unique, mais il faut coordonner une tonne de capteurs et d’acteurs, prendre de nombreuses décisions, et même avoir des exigences éventuellement contradictoires 1 ...

... Cependant, tout est encapsulé. Donc, le client s'en fiche.

1 safety of the passengers, safety of others, respect of regulations, ...

148
ajouté
"SRP est peut-être le principe du logiciel le plus mal compris." Comme en témoigne cette réponse :)
ajouté l'auteur Mithun Nair, source
@ user949300: Bon point, est-ce mieux après le montage?
ajouté l'auteur Owen, source
@SavaB .: Bien sûr, mais le principe reste le même. Un module devrait avoir une responsabilité unique, bien que de portée plus large que ses constituants.
ajouté l'auteur Owen, source
L'exemple de voiture à la fin est médiocre, car "conduire ... en toute sécurité" est significativement différent du "respect des réglementations", et peut même être en conflit! Ils ont sûrement différentes parties prenantes. Cela enfreint le code SRP.
ajouté l'auteur MrG, source
Je suis d’accord avec votre réponse, mais l’argument du cerveau d’une voiture qui conduit à l’autosuffisance pousse souvent les gens à casser le PÉR. Comme vous l'avez dit, il s'agit de modules composés de modules composés de modules. Vous pouvez identifier le but de tout ce système, mais ce système devrait être séparé de lui-même. Vous pouvez résoudre presque tous les problèmes.
ajouté l'auteur Sava B., source
(... suite) Ainsi, la version finale du SRP est la suivante: Un module devrait être responsable devant un et un seul acteur .
ajouté l'auteur CJ Dennis, source
@ user949300 Le dernier livre de Uncle Bob (2017), Clean Architecture, parle des parties prenantes par rapport au PRS à la p.62: Un module devrait être responsable pour un, et un seul, utilisateur ou partie prenante. Malheureusement, les mots "utilisateur" et "partie prenante" ne sont pas vraiment les bons mots à utiliser ici. Plusieurs utilisateurs ou parties prenantes souhaiteront probablement que le système soit modifié de la même manière. Au lieu de cela, nous nous référons vraiment à un groupe - une ou plusieurs personnes qui ont besoin de ce changement. Nous désignerons ce groupe comme un acteur . (a continué...)
ajouté l'auteur CJ Dennis, source
Les citations amélioreraient cette réponse.
ajouté l'auteur Aaron Hall, source
Je pense que la façon dont vous utilisez le mot "encapsulation" et ses dérivés prête à confusion. Autre que cela, bonne réponse!
ajouté l'auteur Marie Simm, source
@ user949300 D'accord, pourquoi pas simplement "conduire". Vraiment, "conduire" est la responsabilité et "en toute sécurité" et "légalement" des exigences sur la façon dont elle s'acquitte de cette responsabilité. Et nous énumérons souvent les exigences pour énoncer une responsabilité.
ajouté l'auteur Jason McCarrell, source

Citation de Oncle Bob à propos du PÉR ( https://8thlight.com/ blog/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html ):

Le principe de responsabilité unique (SRP) stipule que chaque module logiciel doit avoir une et une seule raison de changer.

     

... Ce principe concerne les gens.

     

... Lorsque vous écrivez un module logiciel, vous voulez vous assurer que, lorsque des modifications sont demandées, ces modifications ne peuvent émaner que d'une seule personne, ou plutôt d'un seul groupe de personnes étroitement liées représentant une seule entreprise étroitement définie. fonction.

     

... C'est la raison pour laquelle nous ne mettons pas SQL dans les JSP. C’est la raison pour laquelle nous ne générons pas de code HTML dans les modules qui calculent les résultats. C'est la raison pour laquelle les règles métier ne doivent pas connaître le schéma de base de données. C’est la raison pour laquelle nous séparons les préoccupations.

Il explique que les modules logiciels doivent répondre aux préoccupations spécifiques des parties prenantes. Donc, répondant à votre question:

Est-ce que cette solution est meilleure que la première option ou est-ce que je présente une complexité inutile? Est-il possible d'améliorer encore ce modèle, s'il est viable?

OMI, vous ne considérez qu'une méthode, vous devez rechercher un niveau supérieur (niveau de classe dans ce cas). Nous devrions peut-être jeter un coup d'œil sur ce que votre classe fait actuellement (et cela nécessite davantage d'explications sur votre scénario). Pour le moment, votre classe fait toujours la même chose. Par exemple, si demain il y a une demande de changement concernant une validation (par exemple: "le flux peut maintenant être nul"), vous devez toujours accéder à cette classe et y modifier les éléments qu'elle contient.

41
ajouté
Meilleure réponse. Pour en savoir plus sur l'OP, si les contrôles de garde proviennent de deux parties prenantes/départements différents, checkInputs() doit être scindé, par exemple dans checkMarketingInputs() et checkRegulatoryInputs ( ) . Sinon, il est bon de les combiner tous en une seule méthode.
ajouté l'auteur MrG, source

Non, cette modification n’est pas informée par le PÉR.

Demandez-vous pourquoi, dans votre vérificateur, il n’ya pas de vérification pour "l’objet transmis est un flux" . La réponse est évidente: le langage empêche l’appelant de compiler un programme qui passe dans un flux autre que le flux.

Le système de types de C# est insuffisant pour répondre à vos besoins; vos chèques sont implémentant l’application des invariants qui ne peuvent pas être exprimés dans le système de types aujourd’hui . S'il y avait un moyen de dire que la méthode utilise un flux inscriptible non nullable, vous l'auriez écrit, mais ce n'est pas le cas. Vous avez donc opté pour la meilleure solution: vous avez appliqué la restriction de type au moment de l'exécution. J'espère que vous l'avez également documentée, afin que les développeurs qui utilisent votre méthode n'aient pas à la violer, à faire échouer leurs scénarios de test, puis à résoudre le problème.

Mettre des types sur une méthode n'est pas une violation du principe de responsabilité unique; la méthode n'applique pas non plus ses conditions préalables ou n'affirme ses conditions postérieures.

36
ajouté
De plus, laisser l’objet créé dans un état valide est la seule responsabilité qu’un constructeur a toujours. Si, comme vous l'avez mentionné plus haut, nécessite des vérifications supplémentaires que le moteur d'exécution et/ou le compilateur ne peuvent pas fournir, il n'existe aucun moyen de le contourner.
ajouté l'auteur tony gil, source

Toutes les responsabilités ne sont pas créées égales.

enter image description here

enter image description here

Voici deux tiroirs. Ils ont tous deux une responsabilité. Ils ont chacun des noms qui vous permettent de savoir ce qui leur appartient. L'un est le tiroir à couverts. L'autre est le tiroir à ordures.

Alors quelle est la différence? Le tiroir à couverts indique clairement ce qui ne lui appartient pas. Le tiroir indésirable accepte toutefois tout ce qui conviendra. Sortir les cuillères du tiroir à couverts semble très faux. Pourtant, j'ai du mal à penser à tout ce qui pourrait nous échapper s'il était retiré du tiroir pour ordures. La vérité est que vous pouvez affirmer que tout a une seule responsabilité, mais selon vous, quelle est la responsabilité la plus ciblée?

Un objet ayant une seule responsabilité ne signifie pas qu'une seule chose peut arriver ici. Les responsabilités peuvent nid. Mais ces responsabilités en matière de nidification devraient avoir un sens, elles ne devraient pas vous surprendre quand vous les trouverez ici et vous devriez les manquer si elles avaient disparu.

Alors quand tu offres

CheckInput (flux de données);

Je ne me trouve pas préoccupé par le fait qu’il s’agit à la fois de vérifier les entrées et de lever les exceptions. Je serais inquiet s'il s'agissait à la fois de vérifier l'entrée et de sauvegarder l'entrée. C'est une mauvaise surprise. Un que je ne manquerais pas s'il était parti.

21
ajouté

Lorsque vous vous faites des nœuds et que vous écrivez un code étrange afin de vous conformer à un principe important du logiciel, vous avez généralement mal compris le principe (bien que le principe soit parfois erroné). Comme le souligne l'excellente réponse de Matthieu, toute la signification de SRP dépend de la définition de "responsabilité".

Les programmeurs expérimentés voient ces principes et les relient à des mémoires de code que nous avons bousillés; les programmeurs moins expérimentés les voient et n'ont peut-être rien à voir avec eux. C'est une abstraction qui flotte dans l'espace, tout sourire et pas de chat. Alors ils devinent, et ça va généralement mal. Avant que vous ayez développé le sens du langage de programmation, la différence entre un code bizarre et trop compliqué et un code normal n’est pas évidente.

Ce n'est pas un commandement religieux auquel vous devez obéir quelles que soient les conséquences personnelles. Il s’agit plus d’une règle empirique formelle qui vise à formaliser un élément de la programmation sensorielle et à vous aider à garder votre code aussi simple et clair que possible. Si cela a l'effet opposé, vous avez raison de rechercher une entrée extérieure.

En programmation, vous ne pouvez pas vous tromper d'essayer de déduire la signification d'un identifiant à partir de principes premiers en l'observant, et cela vaut pour les identifiants en écrivant à propos de la programmation, autant que les identifiants dans code actuel.

20
ajouté

Rôle CheckInput

Tout d’abord, permettez-moi d’énoncer ce qui est évident, CheckInput fait quelque chose, même s’il vérifie divers aspects. En fin de compte, il vérifie les entrées . On pourrait soutenir que ce n’est pas une chose si vous utilisez des méthodes appelées DoSomething , mais je pense qu’il est prudent de supposer que la vérification des entrées n’est pas trop vague.

Ajouter ce modèle pour les prédicats peut être utile si vous souhaitez que la logique de contrôle de l'entrée soit placée dans votre classe, mais ce modèle semble plutôt détaillé pour ce que vous essayez d'atteindre. Il pourrait être beaucoup plus direct de simplement passer une interface IStreamValidator avec la méthode unique isValid (Stream) si c'est ce que vous souhaitez obtenir. Toute classe implémentant IStreamValidator peut utiliser des prédicats tels que StreamIsNull ou StreamIsReadonly si elle le souhaite, mais revenir au point central est une modification plutôt ridicule. faire dans l’intérêt du maintien du principe de responsabilité unique.

Verification sanitaire

It is my idea that we're all allowed a "Verification sanitaire" to ensure that you're at least dealing with a Stream that is non-null and writeable, and this basic check is not somehow making your class a validator of streams. Mind you, more sophisticated checks would be best left outside your class, but that's where the line is drawn. Once you need to begin changing state of your stream by reading from it or dedicating resources towards validation, you've started performing a formal validation of your stream and this is what should be pulled into its own class.

Conclusion

Mes pensées sont que si vous appliquez un modèle pour mieux organiser un aspect de votre classe, il vaut la peine d'être dans sa propre classe. Puisqu'un motif ne correspond pas, vous devez également vous demander s'il appartient ou non à sa propre classe. Je pense que, sauf si vous pensez que la validation du flux va probablement être modifiée à l'avenir, et particulièrement si vous pensez que cette validation peut même être de nature dynamique, le modèle que vous avez décrit est une bonne idée, même si être d'abord trivial. Sinon, il n'est pas nécessaire de rendre votre programme plus complexe de manière arbitraire. Permet d'appeler un chat un chat. La validation est une chose, mais la vérification de l'entrée nulle n'est pas une validation. Par conséquent, je pense que vous pouvez être sûr de la conserver dans votre classe sans enfreindre le principe de responsabilité unique.

14
ajouté

Le principe n'énonce pas catégoriquement qu'un morceau de code ne devrait "faire qu'une seule chose".

La "responsabilité" dans SRP doit être comprise au niveau des exigences. La responsabilité du code est de satisfaire les exigences commerciales. SRP est violé si un objet satisfait à plusieurs exigences professionnelles indépendantes . Indépendant, cela signifie qu'une exigence peut changer tandis que l'autre exigence reste en place.

Il est concevable qu’une nouvelle exigence métier soit introduite, ce qui signifie que cet objet particulier ne devrait pas en vérifier la lecture, alors qu’une autre exigence opérationnelle exige toujours que l’objet vérifie si elle est lisible. Non, car les exigences métier ne spécifient pas les détails de la mise en œuvre à ce niveau.

Un exemple concret de violation de SRP serait un code comme celui-ci:

var message = "Your package will arrive before " + DateTime.Now.AddDays(14);

Ce code est très simple, mais il est néanmoins concevable que le texte change indépendamment de la date de livraison prévue, car ceux-ci sont définis par différentes parties de l’entreprise.

4
ajouté
Je pense que c’est davantage que le PÉR exige un élément de jugement de la situation qu’il est difficile de décrire en une phrase accrocheuse.
ajouté l'auteur whatsisname, source
Une classe différente pour pratiquement toutes les exigences ressemble à un cauchemar impie.
ajouté l'auteur whatsisname, source
@whatsisname: Alors peut-être que le SRP n'est pas pour vous. Aucun principe de conception ne s'applique à tous les types et tailles de projets. (Mais sachez que nous ne parlons que d'exigences indépendantes (c'est-à-dire qu'elles peuvent changer de manière indépendante), et non de n'importe quelle exigence, car cela dépendrait simplement de la précision avec laquelle elles sont spécifiées.)
ajouté l'auteur JacquesB, source
@whatsisname: je suis totalement d'accord.
ajouté l'auteur JacquesB, source
+1 pour SRP est violé si un objet satisfait à plusieurs exigences commerciales indépendantes. Indépendant, cela signifie qu’une exigence peut changer tandis que l’autre exigence reste en place
ajouté l'auteur Willeke, source

Votre approche est actuellement procédurale. Vous séparez l'objet Stream et le validez de l'extérieur. Ne faites pas ça - ça casse l'encapsulation. Laissez le Stream être responsable de sa propre validation. Nous ne pouvons pas appliquer le PÉR tant que nous n’aurons pas quelques classes pour l’appliquer.

Voici un Stream qui effectue une action uniquement s'il passe la validation:

class Stream
{
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }

        System.out.println("My action");
    }
}

Mais maintenant , nous violons SRP! "Une classe ne devrait avoir qu'une seule raison de changer." Nous avons un mélange de 1) validation et 2) de logique réelle. Nous avons deux raisons pour lesquelles il pourrait être nécessaire de changer.

Nous pouvons résoudre ce problème avec la la validation des décorateurs . Tout d’abord, nous devons convertir notre Stream en une interface et l’implémenter en tant que classe concrète.

interface Stream
{
    void someAction();
}

class DefaultStream implements Stream
{
    @Override
    public void someAction()
    {
        System.out.println("My action");
    }
}

Nous pouvons maintenant écrire un décorateur qui encapsule un Stream , effectue la validation et diffère du Stream donné pour la logique réelle de l'action.

class WritableStream implements Stream
{
    private final Stream stream;

    public WritableStream(final Stream stream)
    {
        this.stream = stream;
    }

    @Override
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }
        stream.someAction();
    }
}

Nous pouvons maintenant les composer comme bon nous semble:

final Stream myStream = new WritableStream(
    new DefaultStream()
);

Voulez-vous une validation supplémentaire? Ajouter un autre décorateur.

3
ajouté

En parlant de SRP, oncle Bob n'aime pas les chèques nuls parsemés partout. En général, vous, en tant qu'équipe, devriez éviter d'utiliser des paramètres nuls pour les constructeurs dans la mesure du possible. Lorsque vous publiez votre code en dehors de votre équipe, les choses peuvent changer.

L'application de la non-nullabilité des paramètres du constructeur sans d'abord assurer la cohésion de la classe en question entraîne une surcharge du code appelant, notamment des tests.

Si vous souhaitez réellement appliquer de tels contrats, envisagez d’utiliser Debug.Assert ou quelque chose de similaire pour réduire l’encombrement:

public AClassThatDefinitelyNeedsAWritableStream(Stream stream)
{
   Assert.That(stream.CanWrite, "Put crucial information here, and not inane bloat.");

  //Go on normal operation.
}
3
ajouté

J'aime le point tiré de la réponse d'EricLippert :

Demandez-vous pourquoi, dans votre vérificateur, il n'y a pas de contrôle pour l'objet transmis est un flux . La réponse est évidente: le langage empêche l’appelant de compiler un programme qui passe dans un flux autre que le flux.

     

Le système de types de C# est insuffisant pour répondre à vos besoins. vos chèques sont implémentant l’application des invariants qui ne peuvent pas être exprimés dans le système de types aujourd’hui . S'il y avait un moyen de dire que la méthode utilise un flux inscriptible non nullable, vous l'auriez écrit, mais ce n'est pas le cas. Vous avez donc opté pour la meilleure solution: vous avez appliqué la restriction de type au moment de l'exécution. J'espère que vous l'avez également documenté, afin que les développeurs qui utilisent votre méthode n'aient pas à le violer, à faire échouer leurs scénarios de test, puis à résoudre le problème.

EricLippert a raison de dire que c'est un problème pour le système de types. Et puisque vous voulez utiliser le principe de responsabilité unique (SRP), vous avez essentiellement besoin que le système de types soit responsable de ce travail.

Il est en fait possible de le faire en C #. Nous pouvons capturer les nuls littéraux au moment de la compilation, puis les nuls non littéraux au moment de l'exécution. Ce n'est pas aussi bon qu'un contrôle complet à la compilation, mais c'est une amélioration stricte par rapport à ne jamais attraper au moment de la compilation.

So, ya know how C# has Nullable? Let's reverse that and make a NonNullable:

public struct NonNullable where T : class
{
    public T Value { get; private set; }
    public NonNullable(T value)
    {
        if (value == null) { throw new NullArgumentException(); }
        this.Value = value;
    }
   // Ease-of-use:
    public static implicit operator T(NonNullable value) { return value.Value; }
    public static implicit operator NonNullable(T value) { return new NonNullable(value); }

   // Hack-ish overloads that prevent null-literals from being implicitly converted into NonNullable's.
    public static implicit operator NonNullable(Tuple value) { return new NonNullable(value.Item1); }
    public static implicit operator NonNullable(Tuple value) { return new NonNullable(value.Item1); }
}

Maintenant, au lieu d'écrire

public void Foo(Stream stream)
{
  if (stream == null) { throw new NullArgumentException(); }

 //...method code...
}

, Ecrivez:

public void Foo(NonNullable stream)
{
 //...method code...
}

Ensuite, il y a trois cas d'utilisation:

  1. User calls Foo() with a non-null Stream:

    Stream stream = new Stream();
    Foo(stream);
    

    This is the desired use-case, and it works with-or-without NonNullable<>.

  2. User calls Foo() with a null Stream:

    Stream stream = null;
    Foo(stream);
    

    This is a calling error. Here NonNullable<> helps inform the user that they shouldn't be doing this, but it doesn't actually stop them. Either way, this results in a run-time NullArgumentException.

  3. User calls Foo() with null:

    Foo(null);
    

    null won't implicitly convert into a NonNullable<>, so the user gets an error in the IDE, before run-time. This is delegating the null-checking to the type system, just as the SRP would advise.

You can extend this method to assert other things about your arguments, too. For example, since you want a writable stream, you can define a struct WriteableStream where T:Stream that checks for both null and stream.CanWrite in the constructor. This would still be a run-time type check, but:

  1. Décore le type avec le qualificatif WriteableStream , signalant la nécessité d'appeler les appelants.

  2. Il effectue la vérification à un endroit unique dans le code, de sorte que vous n'avez pas à répéter la vérification et lève InvalidArgumentException à chaque fois.

  3. Il se conforme mieux à la SRP en transférant les tâches de vérification de type vers le système de types (tel qu'étendu par les décorateurs génériques).

3
ajouté

Le travail d'une classe consiste à fournir un service qui respecte un contrat . Une classe a toujours un contrat: un ensemble d’exigences pour l’utiliser et des promesses qu’elle fait sur son état et ses résultats à condition que les exigences soient remplies. Ce contrat peut être explicite, sous forme de documentation et/ou d’assertions, ou implicite, mais il existe toujours.

Une partie du contrat de votre classe est que l'appelant donne au constructeur des arguments qui ne doivent pas être nuls. La mise en œuvre du contrat est la responsabilité de la classe. Par conséquent, vérifier que l'appelant a rempli sa partie du contrat peut facilement être considéré comme relevant de la responsabilité de la classe.

L’idée qu’une classe mette en œuvre un contrat est due à Bertrand Meyer , concepteur de la programmation Eiffel langue et de l’idée de la conception par contrat . Le langage Eiffel fait de la spécification et de la vérification du contrat une partie de la langue.

1
ajouté

Comme cela a été souligné dans d'autres réponses, le PRS est souvent mal compris. Il ne s'agit pas d'avoir du code atomique qui ne remplit qu'une fonction. Il s'agit de s'assurer que vos objets et méthodes ne font qu'une seule chose, et que la seule chose soit faite au même endroit.

Regardons un exemple pauvre en pseudo-code.

class Math
    private int a;
    private int b;
    def constructor(int x, int y) 
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Dans notre exemple plutôt absurde, la "responsabilité" du constructeur Math # est de rendre l'objet mathématique utilisable. Pour ce faire, désinfectez d'abord l'entrée, puis assurez-vous que les valeurs ne sont pas -1.

C'est un SRP valide car le constructeur ne fait qu'une chose. C'est préparer l'objet Math. Cependant ce n'est pas très maintenable. Il viole DRY.

Prenons donc une autre passe

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        cleanX(x)
        cleanY(y)
    end
    def cleanX(int x)
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
   end
   def cleanY(int y)
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Dans cette passe, nous sommes un peu mieux au sujet de DRY, mais nous avons encore du chemin à faire avec DRY. La SRP par contre semble un peu en retrait. Nous avons maintenant deux fonctions avec le même travail. CleanX et cleanY nettoient les entrées.

Permet de lui donner un autre coup

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i != null)
          return i
        else if(i < 0)
          return abs(i)
        else if (i == -1)
          throw "Some Silly Error"
        else
          return 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Maintenant, nous étions enfin mieux à propos de DRY et SRP semble être d’accord. Nous avons un seul endroit qui fait le travail "d'assainissement".

Le code est théoriquement plus facile à gérer et à améliorer, même lorsque nous corrigeons le bogue et le resserrions, nous n’avons besoin de le faire qu’à un seul endroit.

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i == null)
          return 0
        else if (i == -1)
          throw "Some Silly Error"
        else
          return abs(i)
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Dans la plupart des cas réels, les objets seraient plus complexes et SRP serait appliqué à un tas d'objets. Par exemple, age peut appartenir à Père, Mère, Fils, Fille. Ainsi, au lieu d’avoir 4 classes qui calculent l’âge à partir de la date de naissance, vous avez une classe Personne qui fait cela et les 4 classes en héritent. Mais j'espère que cet exemple aide à expliquer. SRP ne concerne pas les actions atomiques, mais un "travail" accompli.

0
ajouté