SOLID vs méthodes statiques

11

Voici un problème que je rencontre fréquemment: qu'il y ait un projet de boutique en ligne ayant une classe de produit. Je souhaite ajouter une fonctionnalité qui permet aux utilisateurs de publier des avis sur un produit. J'ai donc une classe Review qui fait référence à un produit. Maintenant, j'ai besoin d'une méthode qui répertorie toutes les critiques d'un produit. Il y a deux possibilités:

(UNE)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

En regardant le code, je choisirais (A): Ce n'est pas statique et il n'a pas besoin de paramètre. Cependant, je sens que (A) viole le principe de responsabilité unique (SRP) et le principe ouvert-fermé (OCP) tandis que (B) ne:

  • (SRP) Lorsque je veux changer la façon dont les avis sont collectés pour un produit, je dois changer la classe de produit. Mais il ne devrait y avoir qu'une seule raison pour changer la classe de produit. Et ce n'est certainement pas les critiques. Si j'emballe toutes les fonctionnalités qui ont quelque chose à voir avec les produits dans Product, elles seront bientôt claquées.

  • (OCP) Je dois changer la classe de produit pour l'étendre avec cette fonctionnalité. Je pense que cela viole la partie «Fermé pour le changement» du principe. Avant de recevoir la demande du client pour la mise en œuvre des avis, j'ai considéré le produit comme terminé et je l'ai "fermé".

Quoi de plus important: suivre les principes SOLID, ou avoir une interface plus simple?

Ou est-ce que je fais quelque chose de mal ici?

Résultat

Wow, merci pour toutes vos bonnes réponses! Il est difficile d'en choisir une comme réponse officielle.

Permettez-moi de résumer les principaux arguments des réponses:

  • pro (A): OCP n'est pas une loi et la lisibilité du code est également importante.
  • pro (A): la relation d'entité doit être navigable. Les deux classes peuvent connaître cette relation.
  • pro (A) + (B): faites les deux et déléguez (A) à (B) afin que le produit soit moins susceptible d'être modifié à nouveau.
  • pro (C): mettez les méthodes de recherche dans la troisième classe (service) où elle n'est pas statique.
  • contra (B): empêche la moquerie dans les tests.

Quelques choses supplémentaires que mes collèges au travail ont apportées:

  • pro (B): notre framework ORM peut générer automatiquement le code pour (B).
  • pro (A): pour des raisons techniques de notre framework ORM, il faudra dans certains cas modifier l'entité "fermée", indépendamment de l'endroit où se trouve le chercheur. Je ne pourrai donc pas toujours m'en tenir à SOLID de toute façon.
  • contra (C): trop de bruit ;-)

Conclusion

J'utilise à la fois (A) + (B) avec délégation pour mon projet actuel. Dans un environnement axé sur les services, cependant, j'irai avec (C).

Wolfgang
la source
2
Tant que ce n'est pas une variable statique, tout est cool. Les méthodes statiques sont simples à tester et simples à tracer.
Coder
3
Pourquoi ne pas simplement avoir une classe ProductsReviews? Le produit et la revue restent les mêmes. Ou peut-être que je me méprends.
ElGringoGrande
2
@Coder "Les méthodes statiques sont simples à tester", vraiment? Ils ne peuvent pas être moqués, voir: googletesting.blogspot.com/2008/12/… pour plus de détails.
StuperUser
1
@StuperUser: Il n'y a rien à se moquer. Assert(5 = Math.Abs(-5));
Coder
2
Tester Abs()n'est pas le problème, tester quelque chose qui en dépend. Vous n'avez pas de couture pour isoler le code sous test (CUT) dépendant pour utiliser une maquette. Cela signifie que vous ne pouvez pas le tester en tant qu'unité atomique et que tous vos tests deviennent des tests d'intégration qui testent la logique de l'unité. Un échec dans un test peut être en CUT ou en Abs()(ou son code dépendant) et supprime les avantages de diagnostic des tests unitaires.
StuperUser

Réponses:

7

Quoi de plus important: suivre les principes SOLID, ou avoir une interface plus simple?

Interface face à SOLID

Ceux-ci ne sont pas mutuellement exclusifs. L'interface doit exprimer la nature de votre modèle d'entreprise idéalement en termes de modèle d'entreprise. Les principes SOLID sont un Koan pour maximiser la maintenabilité du code orienté objet (et je veux dire "maintenabilité" au sens le plus large). Le premier prend en charge l'utilisation et la manipulation de votre modèle d'entreprise et le second optimise la maintenance du code.

Principe ouvert / fermé

"ne touche pas à ça!" est une interprétation trop simpliste. Et en supposant que nous voulons dire «la classe» est arbitraire et pas nécessairement juste. OCP signifie plutôt que vous avez conçu votre code de telle sorte que la modification de son comportement ne vous oblige pas (ne devrait pas) modifier directement le code existant. En outre, ne pas toucher au code en premier lieu est le moyen idéal pour préserver l'intégrité des interfaces existantes; c'est un corollaire significatif de l'OCP à mon avis.

Enfin, je vois OCP comme un indicateur de la qualité de conception existante. Si je me retrouve trop souvent à casser des classes ouvertes (ou des méthodes) et / ou sans des raisons vraiment solides (ha, ha) de le faire, cela peut me dire que j'ai une mauvaise conception (et / ou je ne le fais pas) savoir coder OO).

Donnez-vous votre meilleur coup, nous avons une équipe de médecins à vos côtés

Si votre analyse des exigences vous indique que vous devez exprimer la relation Product-Review des deux points de vue, faites-le.

Par conséquent, Wolfgang, vous pouvez avoir une bonne raison de modifier ces classes existantes. Compte tenu des nouvelles exigences, si une révision est désormais une partie fondamentale d'un produit, si chaque extension du produit doit être revue, si cela rend le code client correctement expressif, intégrez-le dans le produit.

radarbob
la source
1
+1 pour avoir noté qu'OCP est plus un indicateur de bonne qualité, pas une règle rapide pour n'importe quel code. Si vous trouvez qu'il est difficile ou impossible de suivre correctement l'OCP, c'est un signe que votre modèle doit être refactorisé pour permettre une réutilisation plus flexible.
CodexArcanum
J'ai choisi l'exemple de Product and Review pour souligner que le produit est une entité centrale du projet tandis que Review est un simple module complémentaire. Ainsi, le produit est déjà existant, terminé et Review vient plus tard et devrait être introduit sans ouvrir le code existant (y compris le produit).
Wolfgang
10

SOLIDES sont des lignes directrices, donc influencez la prise de décision plutôt que de la dicter.

Une chose à savoir lors de l'utilisation de méthodes statiques est leur impact sur la testabilité .


Les tests forProduct(Product product)ne seront pas un problème.

Tester quelque chose qui en dépendra sera.

Vous n'aurez pas de couture pour isoler le code sous test (CUT) dépendant pour utiliser une maquette, car lorsque l'application est en cours d'exécution, les méthodes statiques existent nécessairement.

Ayons une méthode appelée CUT()qui appelleforProduct()

Si forProduct()c'est le cas, staticvous ne pouvez pas tester en CUT()tant qu'unité atomique et tous vos tests deviennent des tests d'intégration qui testent la logique de l'unité.

Un échec dans un test de CUT pourrait être causé par un problème dans CUT()ou dans forProduct()(ou l'un de ses codes dépendants) qui supprime les avantages de diagnostic des tests unitaires.

Voir cet excellent article de blog pour des informations plus détaillées: http://googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


Cela peut conduire à la frustration de l'échec des tests et à l'abandon des bonnes pratiques et des avantages qui les entourent.

StuperUser
la source
1
C'est un très bon point. En général, pas pour moi cependant. ;-) Je ne suis pas si strict sur l'écriture de tests unitaires qui couvrent plus d'une classe. Ils descendent tous dans la base de données, ça me va. Je ne me moquerai donc pas de l'objet métier ou de son chercheur.
Wolfgang
+1, merci d'avoir placé le drapeau rouge pour les tests! Je suis d'accord avec @Wolfgang, généralement je ne suis pas aussi strict mais quand j'ai besoin de ce genre de tests, je déteste vraiment les méthodes statiques. Habituellement, si une méthode statique interagit trop avec ses paramètres ou si elle interagit avec toute autre méthode statique, je préfère en faire une méthode d'instance.
Adriano Repetti
Cela ne dépend-il pas entièrement de la langue utilisée? L'OP a utilisé un exemple Java, mais n'a jamais mentionné de langage dans la question, et aucun n'est spécifié dans les balises.
Izkata
1
@StuperUser If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.- Je crois que Javascript et Python permettent tous deux de remplacer / mocker les méthodes statiques. Je ne suis cependant pas sûr à 100%.
Izkata
1
@Izkata JS est typé dynamiquement, ce n'est donc pas le cas static, vous le simulez avec une fermeture et le motif singleton. En lisant sur Python (en particulier stackoverflow.com/questions/893015/… ), vous devez hériter et étendre. Le dépassement n'est pas moqueur; on dirait que vous n'avez toujours pas de couture pour tester le code en tant qu'unité atomique.
StuperUser
4

Si vous pensez que le produit est le bon endroit pour trouver les avis sur le produit, vous pouvez toujours donner au produit une classe d'aide pour faire le travail pour lui. (Vous pouvez le dire parce que votre entreprise ne parlera jamais d'un avis, sauf en termes de produit).

Par exemple, je serais tenté d'injecter quelque chose qui a joué le rôle d'un retriever de révision. Je lui donnerais probablement l'interface IRetrieveReviews. Vous pouvez le mettre dans le constructeur du produit (injection de dépendance). Si vous souhaitez modifier la façon dont les avis sont récupérés, vous pouvez le faire facilement en injectant un collaborateur différent - un TwitterReviewRetrieverou un AmazonReviewRetrieverou MultipleSourceReviewRetrieverou tout ce dont vous avez besoin.

Les deux ont désormais une seule responsabilité (être la référence pour tout ce qui concerne le produit et récupérer les avis, respectivement), et à l'avenir le comportement du produit par rapport aux avis peut être modifié sans réellement changer le produit (vous pouvez l'étendre comme ProductWithReviewssi vous vouliez vraiment être pédant sur vos principes SOLIDES, mais ce serait assez bon pour moi).

Lunivore
la source
Cela ressemble au modèle DAO qui est très courant dans les logiciels orientés services / composants. J'aime cette idée car elle exprime que la récupération d'objets n'est pas la responsabilité de ces objets. Cependant, puisque je préfère une manière orientée objet à celle orientée service.
Wolfgang
1
L'utilisation d'une interface telle que l' IRetrieveReviewsempêche d'être orientée service - elle ne détermine pas ce qui obtient les avis, ni comment, ni quand. C'est peut-être un service avec beaucoup de méthodes pour des choses comme ça. C'est peut-être une classe qui fait cette seule chose. Il s'agit peut-être d'un référentiel ou d'une requête HTTP vers un serveur. Tu ne sais pas. Tu ne devrais pas savoir. C'est le but.
Lunivore
Oui, ce serait donc une mise en œuvre du modèle de stratégie. Ce qui serait un argument pour une troisième classe. (A) et (B) ne soutiendraient pas cela. Le chercheur utilisera certainement un ORM, il n'y a donc aucune raison de remplacer l'algorithme. Désolé si je n'ai pas été clair à ce sujet dans ma question.
Wolfgang
3

J'aurais une classe ProductsReview. Vous dites que l'examen est nouveau de toute façon. Cela ne veut pas dire que ça peut être n'importe quoi. Il ne doit encore avoir qu'une seule raison de changer. Si vous modifiez la façon dont vous obtenez les avis pour une raison quelconque, vous devrez changer la classe d'avis.

Ce n'est pas correct.

Vous mettez la méthode statique dans la classe Review parce que ... pourquoi? N'est-ce pas ce avec quoi vous luttez? N'est-ce pas là tout le problème?

Alors ne le fais pas. Faites une classe dont la seule responsabilité est d'obtenir les avis sur les produits. Vous pouvez ensuite le sous-classer dans ProductReviewsByStartRating. Ou sous-classe pour obtenir des avis sur une classe de produits.

ElGringoGrande
la source
Je ne suis pas d'accord que le fait d'avoir la méthode sur Review violerait SRP. Sur le produit, ce serait mais pas sur la révision. Mon problème, c'est qu'il est statique. Si je déplaçais la méthode vers une troisième classe, elle serait toujours statique et aurait le paramètre produit.
Wolfgang
Vous dites donc que la seule responsabilité de la classe Review, la seule raison pour laquelle elle change, est si la méthode statique forProduct devait changer? Il n'y a pas d'autre fonctionnalité dans la classe Review?
ElGringoGrande
Il y a beaucoup de choses sur Review. Mais je pense qu'un forProduct (produit) lui conviendrait parfaitement. La recherche d'avis dépend beaucoup des attributs et de la structure de l'examen (quels identificateurs uniques, quels champs changent d'attributs?). Cependant, le produit ne connaît pas et ne devrait pas connaître les avis.
Wolfgang
3

Je ne mettrais pas la fonctionnalité "Get Reviews For Product" dans la Productclasse ni dans la Reviewclasse ...

Vous avez un endroit où récupérer vos produits, non? Quelque chose avec GetProductById(int productId)et peut-être GetProductsByCategory(int categoryId)et ainsi de suite.

De même, vous devriez avoir un endroit pour récupérer vos avis, avec un GetReviewbyId(int reviewId)et peut-être un GetReviewsForProduct(int productId).

Lorsque je souhaite modifier la façon dont les avis sont collectés pour un produit, je dois changer la classe de produit.

Si vous séparez votre accès aux données de vos classes de domaine, vous n'aurez pas besoin de modifier l'une ou l' autre classe de domaine lorsque vous modifiez la façon dont les avis sont collectés.

Eric King
la source
C'est ainsi que je gérerais cela, et je suis confus (et inquiet de savoir si mes propres idées sont correctes) par l'absence de cette réponse jusqu'à votre message. De toute évidence, ni la classe représentant un rapport ni la représentation d'un produit ne devraient être responsables de la récupération effective de l'autre. Une sorte de données fournissant des services devrait gérer cela.
CodexArcanum
@CodexArcanum Eh bien, vous n'êtes pas seul. :-)
Eric King
2

Les modèles et les principes sont des lignes directrices, pas des règles écrites dans la pierre. À mon avis, la question n'est pas de savoir s'il vaut mieux suivre les principes SOLIDES ou garder une interface plus simple. Ce que vous devez vous demander, c'est ce qui est plus lisible et compréhensible pour la plupart des gens. Cela signifie souvent qu'il doit être aussi proche que possible du domaine.

Dans ce cas, je préférerais la solution (B) car pour moi, le point de départ est le produit, pas la critique, mais imaginez que vous écrivez un logiciel pour gérer les critiques. Dans ce cas, le centre est la revue, la solution (A) peut donc être préférable.

Lorsque j'ai beaucoup de méthodes comme celle-ci ("connexions" entre les classes), je les supprime toutes à l'extérieur et je crée une (ou plusieurs) nouvelle classe statique pour les organiser. Habituellement, vous pouvez les voir sous forme de requêtes ou de type de référentiel.

Adriano Repetti
la source
Je réfléchissais aussi à cette idée de la sémantique des deux extrémités et laquelle est "plus forte" pour qu'elle revendique le chercheur. C'est pourquoi j'ai trouvé (B). Cela aiderait également à créer une hiérarchie «d'abstraction» des classes affaires. Le produit était un simple objet de base où Review est supérieur. Ainsi, Review peut référencer le produit mais pas l'inverse. De cette façon, les cycles de références entre les classes affaires peuvent être évités, ce qui résoudrait un autre problème sur ma liste.
Wolfgang
Je pense que pour un petit ensemble de classes, il pourrait être agréable de fournir les DEUX méthodes (A) et (B). Trop souvent, l'interface utilisateur n'est pas bien connue au moment d'écrire cette logique.
Adriano Repetti
1

Votre produit peut simplement déléguer à votre méthode de révision statique, auquel cas vous fournissez une interface pratique dans un emplacement naturel (Product.getReviews), mais vos détails d'implémentation se trouvent dans Review.getForProduct.

SOLID sont des directives et devraient aboutir à une interface simple et sensée. Alternativement, vous pouvez dériver SOLID à partir d'interfaces simples et sensibles. Il s'agit de la gestion des dépendances dans le code. L'objectif est de minimiser les dépendances qui créent des frictions et créent des obstacles à des changements inévitables.

frites
la source
Je ne suis pas sûr de vouloir cela. De cette façon, j'ai la méthode aux deux endroits, ce qui est bien car les autres développeurs n'ont pas besoin de regarder dans les deux classes pour voir où je la mets. En revanche, j'aurais à la fois des inconvénients de la méthode statique et le changement de la classe de produit "fermé". Je ne suis pas sûr que la délégation aide à sortir du problème de friction. S'il y a jamais eu une raison de changer le chercheur, cela entraînera très certainement le changement de signature et donc à nouveau un changement de produit.
Wolfgang
La clé d'OCP est que vous pouvez remplacer les implémentations sans changer votre code. Dans la pratique, cela est étroitement lié à la substitution de Liskov. Une mise en œuvre doit être substituable à une autre. Vous pouvez avoir DatabaseReviews et SoapReviews en tant que classes différentes implémentant IGetReviews.getReviews et getReviewsForProducts. Ensuite, votre système est ouvert pour l'extension (changer la façon dont les avis sont obtenus) et fermé pour modification (les dépendances sur IGetReviews ne se cassent pas). C'est ce que je veux dire par gestion de la dépendance. Dans votre cas, vous allez devoir modifier votre code pour changer son comportement.
pfries
N'est-ce pas le principe d'inversion de dépendance (DIP) que vous décrivez?
Wolfgang
Ce sont des principes liés. Votre point de substitution est atteint par DIP.
pfries
1

J'ai un point de vue légèrement différent à ce sujet que la plupart des autres réponses. Je pense que Productet Reviewsont essentiellement des objets de transfert de données (DTO). Dans mon code, j'essaie de faire en sorte que mes DTO / Entités évitent d'avoir des comportements. Ils sont juste une belle API pour stocker l'état actuel de mon modèle.

Lorsque vous parlez d'OO et de SOLID, vous parlez généralement d'un "objet" qui ne représente pas l'état (nécessairement), mais représente plutôt une sorte de service qui répond à vos questions ou auquel vous pouvez déléguer une partie de votre travail. . Par exemple:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

Ensuite, votre réel ProductRepositoryretournerait un ExistingProductpour la GetProductByProductIdméthode, etc.

Vous suivez maintenant le principe de la responsabilité unique (tout ce qui hérite ne IProductfait que s'accrocher à l'état, et tout ce qui hérite IProductRepositoryest responsable de savoir comment persister et réhydrater votre modèle de données).

Si vous modifiez votre schéma de base de données, vous pouvez modifier l'implémentation de votre référentiel sans modifier vos DTO, etc.

Donc, en bref, je suppose que je ne choisirais aucune de vos options. :)

Scott Whitlock
la source
0

Les méthodes statiques peuvent être plus difficiles à tester, mais cela ne signifie pas que vous ne pouvez pas les utiliser - il vous suffit de les configurer de sorte que vous n'ayez pas besoin de les tester.

Créez les deux méthodes product.GetReviews et Review.ForProduct d'une ligne qui ressemblent à quelque chose

new ReviewService().GetReviews(productID);

ReviewService contient le code le plus complexe et possède une interface conçue pour la testabilité, mais qui n'est pas exposée directement à l'utilisateur.

Si vous devez avoir une couverture à 100%, demandez à vos tests d'intégration d'appeler les méthodes produit / classe de révision.

Cela peut être utile si vous pensez à la conception d'API publique plutôt qu'à la conception de classes - dans ce contexte, une interface simple avec un regroupement logique est tout ce qui compte - la structure de code réelle n'a d'importance que pour les développeurs.

Tom Clarkson
la source