web-dev-qa-db-fra.com

Objets à comportement zéro dans OOP - mon dilemme de conception

L'idée de base derrière OOP est que les données et le comportement (sur ces données) sont inséparables et ils sont couplés par l'idée d'un objet d'une classe. Les objets ont des données et des méthodes qui fonctionnent avec cela ( Évidemment, selon les principes de la POO, les objets qui ne sont que des données (comme les structures C) sont considérés comme un anti-modèle.

Jusqu'ici tout va bien.

Le problème est que j'ai remarqué que mon code semble aller de plus en plus dans le sens de cet anti-modèle ces derniers temps. Il me semble que plus j'essaie de masquer les informations entre les classes et les conceptions faiblement couplées, plus mes classes deviennent un mélange de données pures sans classes de comportement et de tout comportement sans classes de données.

Je conçois généralement des cours d'une manière qui minimise leur conscience de l'existence d'autres classes et minimise leur connaissance des interfaces des autres classes. J'applique particulièrement cela de façon descendante, les classes de niveau inférieur ne connaissent pas les classes de niveau supérieur. Par exemple.:

Supposons que vous ayez une API de jeu de cartes générale. Vous avez une classe Card. Maintenant, cette classe Card doit déterminer la visibilité pour les joueurs.

Une façon consiste à avoir boolean isVisible(Player p) sur Card classe.

Une autre consiste à avoir boolean isVisible(Card c) sur Player classe.

Je n'aime pas la première approche en particulier car elle accorde des connaissances sur la classe Player de niveau supérieur à une classe Card de niveau inférieur.

Au lieu de cela, j'ai opté pour la troisième option où nous avons une classe Viewport qui, étant donné un Player et une liste de cartes détermine quelles cartes sont visibles.

Cependant, cette approche prive les classes Card et Player d'une fonction membre possible. Une fois que vous faites cela pour autre chose que la visibilité des cartes, vous vous retrouvez avec des classes Card et Player qui contiennent uniquement des données car toutes les fonctionnalités sont implémentées dans d'autres classes, qui sont principalement des classes sans données , juste des méthodes, comme Viewport ci-dessus.

Ceci est clairement contraire à l'idée principale de la POO.

Quelle est la bonne façon? Comment dois-je entreprendre la tâche de minimiser les interdépendances de classe et de minimiser les connaissances et le couplage supposés, mais sans se retrouver avec une conception bizarre où toutes les classes de bas niveau contiennent uniquement des données et les classes de haut niveau contiennent toutes les méthodes? Quelqu'un a-t-il une troisième solution ou perspective sur la conception des classes qui évite tout le problème?

P.S. Voici un autre exemple:

Supposons que vous ayez la classe DocumentId qui est immuable, n'a qu'un seul membre BigDecimal id Et un getter pour ce membre. Maintenant, vous devez avoir une méthode quelque part, qui étant donnée un DocumentId renvoie Document pour cet identifiant à partir d'une base de données.

Le faites vous:

  • Ajoutez la méthode Document getDocument(SqlSession) à la classe DocumentId, introduisant soudainement des connaissances sur votre persistance ("we're using a database and this query is used to retrieve document by id"), L'API utilisée pour accéder à DB et autres. De plus, cette classe nécessite désormais un fichier JAR de persistance juste pour être compilé.
  • Ajoutez une autre classe avec la méthode Document getDocument(DocumentId id), en laissant la classe DocumentId morte, sans comportement, classe de structure.
96
RokL

Ce que vous décrivez est connu comme un modèle de domaine anémique . Comme pour de nombreux principes de conception OOP (comme la loi de Demeter, etc.), cela ne vaut pas la peine de se pencher en arrière juste pour satisfaire une règle.

Rien de mal à avoir des sacs de valeurs, tant qu'ils n'encombrent pas tout le paysage et ne comptent pas sur d'autres objets pour faire le ménage qu'ils pourraient faire pour eux-mêmes .

Ce serait certainement une odeur de code si vous aviez une classe distincte juste pour modifier les propriétés de Card - si l'on pouvait raisonnablement s'attendre à les prendre en charge par elle-même.

Mais est-ce vraiment le travail d'un Card de savoir à quel Player il est visible?

Et pourquoi implémenter Card.isVisibleTo(Player p), mais pas Player.isVisibleTo(Card c)? Ou vice versa?

Oui, vous pouvez essayer de trouver une sorte de règle pour cela comme vous l'avez fait - comme Player étant plus haut qu'un Card (?) - mais ce n'est pas si simple à deviner et je devrai chercher dans plus d'un endroit pour trouver la méthode.

Au fil du temps, cela peut conduire à un compromis de conception pourri de l'implémentation de isVisibleTo sur à la fois Card et Player classe, qui je crois est un non-non. Pourquoi Parce que j'imagine déjà le jour honteux où player1.isVisibleTo(card1) renverra une valeur différente de card1.isVisibleTo(player1). Je pense - c'est subjectif - cela devrait être rendu impossible par conception .

La visibilité mutuelle des cartes et des joueurs devrait mieux être régie par une sorte d'objet contextuel - que ce soit Viewport, Deal ou Game.

Ce n'est pas égal à avoir des fonctions globales. Après tout, il peut y avoir de nombreux jeux simultanés. Notez que la même carte peut être utilisée simultanément sur plusieurs tables. Allons-nous créer de nombreuses instances de Card pour chaque as de pique?

Je pourrais toujours implémenter isVisibleTo sur Card, mais lui passer un objet contextuel et faire Card déléguer la requête. Programme d'interface pour éviter un couplage élevé.

Quant à votre deuxième exemple - si l'ID de document se compose uniquement d'un BigDecimal, pourquoi créer une classe wrapper pour cela?

Je dirais que tout ce dont vous avez besoin est une DocumentRepository.getDocument(BigDecimal documentID);

Par ailleurs, bien qu'il soit absent de Java, il y a structs en C #.

Voir

pour référence. C'est un langage hautement orienté objet, mais personne n'en fait grand cas.

42
Konrad Morawski

L'idée de base derrière OOP est que les données et le comportement (sur ces données) sont inséparables et ils sont couplés par l'idée d'un objet d'une classe.

Vous faites l'erreur courante de supposer que les classes sont un concept fondamental dans la POO. Les classes ne sont qu'un moyen particulièrement populaire de réaliser l'encapsulation. Mais nous pouvons permettre que cela glisse.

Supposons que vous ayez une API de jeu de cartes générale. Vous avez une carte de classe. Maintenant, cette classe de cartes doit déterminer la visibilité des joueurs.

GOOD HEAVENS NO. Lorsque vous jouez au Bridge, est-ce que vous demandez aux sept de cœur quand est-il temps de changer la main du mannequin d'un secret connu seulement au mannequin à être connu de tous? Bien sûr que non. Ce n'est pas du tout une préoccupation de la carte.

Une façon consiste à avoir un booléen isVisible (Player p) sur la classe Card. Une autre consiste à avoir booléen isVisible (Carte c) sur la classe Player.

Les deux sont horribles; ne faites rien de tout cela. Ni le joueur ni la carte ne sont responsables de la mise en œuvre des règles de Bridge !

Au lieu de cela, j'ai opté pour la troisième option où nous avons une classe Viewport qui, étant donné un joueur et une liste de cartes, détermine quelles cartes sont visibles.

Je n'ai jamais joué aux cartes avec une "fenêtre d'affichage" auparavant, donc je n'ai aucune idée de ce que cette classe est censée encapsuler. J'ai joué avec quelques jeux de cartes, quelques joueurs, une table et une copie de Hoyle. Lequel de ces éléments représente Viewport?

Cependant, cette approche prive les classes Card et Player d'une fonction membre possible.

Bien!

Une fois que vous faites cela pour autre chose que la visibilité des cartes, vous vous retrouvez avec des classes Card et Player qui contiennent uniquement des données car toutes les fonctionnalités sont implémentées dans d'autres classes, qui sont principalement des classes sans données, uniquement des méthodes, comme la fenêtre ci-dessus. Ceci est clairement contraire à l'idée principale de la POO.

Non; l'idée de base de OOP est que les objets résument leurs préoccupations . Dans votre système, une carte ne se soucie pas beaucoup . Pas plus qu'un joueur. C'est parce que vous modélisez le monde avec précision. Dans le monde réel, les propriétés des cartes qui sont pertinentes pour un jeu sont extrêmement simples. Nous pourrions remplacer les images sur les cartes avec les nombres de 1 à 52 sans beaucoup changer le jeu du jeu. On pourrait remplacer les quatre personnes par des mannequins étiquetés Nord, Sud, Est et Ouest sans trop changer le jeu du jeu. Les joueurs et les cartes sont les les choses les plus simples dans le monde des jeux de cartes. Les règles sont ce qui est compliqué, donc la classe qui représente les règles est où la complication devrait être.

Maintenant, si l'un de vos joueurs est une IA, son état interne pourrait être extrêmement compliqué. Mais cette IA ne détermine pas si elle peut voir une carte. Les règles déterminent que.

Voici comment je concevrais votre système.

Tout d'abord, les cartes sont étonnamment compliquées s'il y a des jeux avec plus d'un jeu. Vous devez considérer la question: les joueurs peuvent-ils distinguer deux cartes de même rang? Si le joueur un joue l'un des sept cœurs, puis que quelque chose se passe, puis que le joueur deux joue l'un des sept cœurs, le joueur trois peut-il déterminer qu'il s'agissait du même sept coeurs? Réfléchissez bien. Mais à part cette préoccupation, les cartes devraient être très simples; ce ne sont que des données.

Ensuite, quelle est la nature d'un joueur? Un joueur consomme une séquence d'actions visibles et produit une action .

L'objet règles est ce qui coordonne tout cela. Les règles produisent une séquence d'actions visibles et informent les joueurs:

  • Joueur un, les dix coeurs vous ont été remis par le joueur trois.
  • Joueur deux, une carte a été remise au joueur un par le joueur trois.

Et demande ensuite au joueur une action.

  • Joueur un, que veux-tu faire?
  • Le premier joueur dit: tripler le fromp.
  • Joueur un, c'est une action illégale car un fromp triplé produit un gambit indéfendable.
  • Joueur un, que veux-tu faire?
  • Le premier joueur dit: défaussez la reine de pique.
  • Le joueur deux, le joueur un a défaussé la reine de pique.

Etc.

Séparez vos mécanismes de vos politiques. Les politiques du jeu doivent être encapsulées dans un objet politique, pas dans les cartes. Les cartes ne sont qu'un mécanisme.

150
Eric Lippert

Vous avez raison de dire que le couplage des données et du comportement est l'idée centrale de la POO, mais il y a plus. Par exemple, encapsulation: OOP/programmation modulaire nous permet de séparer une interface publique des détails d'implémentation. En OOP cela signifie que les données ne devraient jamais être accessibles au public, et ne devraient être utilisées que via des accesseurs. Par cette définition, un objet sans aucune méthode est en effet inutile.

Une classe qui n'offre aucune méthode au-delà des accesseurs est essentiellement une structure trop compliquée. Mais ce n'est pas mauvais, car OOP vous donne la possibilité de modifier les détails internes, ce que ne fait pas une structure. Par exemple, au lieu de stocker une valeur dans un champ membre, elle pourrait être recalculée à chaque fois. Ou un algorithme de sauvegarde est modifié, et avec lui l'état qui doit être conservé.

Bien que OOP présente certains avantages évidents (en particulier par rapport à une programmation procédurale simple), il est naïf de rechercher une POO "pure". Certains problèmes ne correspondent pas bien à une approche orientée objet et sont résolus plus facilement par d'autres paradigmes. Lorsque vous rencontrez un tel problème, n'insistez pas sur une approche inférieure.

  • Envisagez de calculer la séquence de Fibonacci d'une manière orientée objet. Je ne peux pas penser à une manière sensée de le faire; une programmation structurée simple offre la meilleure solution à ce problème.

  • Votre relation isVisible appartient aux deux classes, ou à aucune, ou en fait: au contexte. Les enregistrements sans comportement sont typiques d'une approche de programmation fonctionnelle ou procédurale, qui semble être la mieux adaptée à votre problème. Il n'y a rien de mal à

    static boolean isVisible(Card c, Player p);
    

    et il n'y a rien de mal à ce que Card n'ait aucune méthode au-delà des accesseurs rank et suit.

29
amon

L'idée de base derrière OOP est que les données et le comportement (sur ces données) sont inséparables et ils sont couplés par l'idée d'un objet d'une classe. Les objets ont des données et des méthodes qui fonctionnent avec cela ( Evidemment, selon les principes de la POO, les objets qui ne sont que des données (comme les structures C) sont considérés comme un anti-modèle. (...) Cela va clairement à l'encontre de l'idée principale de la POO.

C'est une question difficile car elle est basée sur quelques prémisses défectueuses:

  1. L'idée que OOP est le seul moyen valide d'écrire du code.
  2. L'idée que OOP est un concept bien défini. C'est devenu un mot à la mode qu'il est difficile de trouver deux personnes qui peuvent se mettre d'accord sur ce qu'est OOP).
  3. L'idée que OOP concerne le regroupement des données et du comportement.
  4. L'idée que tout est/devrait être une abstraction.

Je n'aborderai pas beaucoup les points 1 à 3, car chacun pourrait engendrer sa propre réponse, et cela invite à beaucoup de discussions basées sur l'opinion. Mais je trouve que l'idée de "POO consiste à coupler données et comportement" est particulièrement troublante. Non seulement cela mène au # 4, mais cela conduit également à l'idée que tout devrait être une méthode.

Il y a une différence entre les opérations qui définissent un type et les façons dont vous pouvez utiliser ce type. Pouvoir récupérer l'élément ith est essentiel au concept d'un tableau, mais le tri n'est qu'une des nombreuses choses que je peux choisir de faire avec un. Le tri n'a pas plus besoin d'être une méthode que doit l'être "créer un nouveau tableau contenant uniquement les éléments pairs".

La POO consiste à utiliser des objets. Les objets ne sont qu'un moyen de réaliser l'abstraction . L'abstraction est un moyen d'éviter un couplage inutile dans votre code, pas une fin en soi. Si votre notion de carte est définie uniquement par la valeur de sa suite et de son rang, il est bon de l'implémenter en tant que simple tuple ou enregistrement. Il n'y a pas de détails non essentiels sur lesquels toute autre partie du code pourrait former une dépendance. Parfois, vous n'avez rien à cacher.

Vous ne feriez pas de isVisible une méthode du type Card car être visible n'est probablement pas essentiel à votre conception d'une carte (sauf si vous avez des cartes très spéciales qui peuvent devenir translucides ou opaques .. .). Doit-il s'agir d'une méthode de type Player? Eh bien, ce n'est probablement pas non plus une qualité déterminante des joueurs. Doit-il faire partie d'un type Viewport? Encore une fois, cela dépend de ce que vous définissez comme une fenêtre et si la notion de vérification de la visibilité des cartes fait partie intégrante de la définition d'une fenêtre.

Il est très possible que isVisible soit juste une fonction libre.

19
Doval

De toute évidence, selon les principes de la POO, les objets qui ne sont que des données (comme les structures C) sont considérés comme un anti-modèle.

Non, ils ne le sont pas. Les objets Plain-Old-Data sont un modèle parfaitement valide, et je m'attendrais à ce qu'ils soient dans n'importe quel programme qui traite des données qui doivent être persistées ou communiquées entre des zones distinctes de votre programme.

Alors que votre couche de données pourrait mettre en file d'attente une classe Player complète lorsqu'elle lit à partir de la table Players, elle pourrait plutôt être simplement une bibliothèque de données générale qui renvoie un POD avec les champs de la table, qu'il passe à une autre zone de votre programme qui convertit un POD joueur en votre classe concrète Player.

L'utilisation d'objets de données, typés ou non, peut ne pas avoir de sens dans votre programme, mais cela n'en fait pas un anti-modèle. S'ils ont un sens, utilisez-les, et s'ils ne le font pas, ne le faites pas.

9
DougM

Personnellement, je pense que la conception pilotée par domaine aide à clarifier ce problème. La question que je pose est la suivante: comment décrire le jeu de cartes aux êtres humains? En d'autres termes, qu'est-ce que je modélise? Si la chose que je modélise inclut véritablement le mot "viewport" et un concept qui correspond à son comportement, alors je créerais l'objet viewport et lui ferais ce qu'il devrait logiquement.

Cependant, si je n'ai pas le concept de la fenêtre d'affichage sur mon jeu, et c'est quelque chose dont je pense avoir besoin car sinon le code "se sent mal". Je pense à deux fois à l'ajouter à mon modèle de domaine.

Le modèle Word signifie que vous construisez une représentation de quelque chose. Je mets en garde contre la création d'une classe qui représente quelque chose d'abstrait au-delà de ce que vous représentez.

Je modifierai pour ajouter qu'il est possible que vous ayez besoin du concept d'une fenêtre d'affichage dans une autre partie de votre code, si vous avez besoin d'interfacer avec un écran. Mais en termes DDD, ce serait une préoccupation d'infrastructure et existerait en dehors du modèle de domaine.

8
RibaldEddie

Je ne fais généralement pas d'auto-promotion, mais le fait est que j'ai beaucoup écrit sur OOP problèmes de conception sur mon blog . Pour résumer plusieurs pages: vous ne devriez pas Ne commencez pas la conception avec des classes. En commençant par les interfaces ou les API et le code de forme à partir de là, vous avez plus de chances de fournir des abstractions significatives, d'ajuster les spécifications et d'éviter de gonfler les classes concrètes avec du code non réutilisable.

Comment cela s'applique au problème de Card-Player: La création d'une abstraction ViewPort est logique si vous pensez que Card et Player sont deux bibliothèques indépendantes (ce qui impliquerait que Player est parfois utilisé sans Card). Cependant, je suis enclin à penser qu'un Player contient Cards et devrait leur fournir un accesseur Collection<Card> getVisibleCards (). Ces deux solutions (ViewPort et la mienne) sont meilleures que de fournir isVisible comme méthode de Card ou Player, en termes de création de relations de code compréhensibles.

Une solution hors classe est beaucoup, beaucoup mieux pour le DocumentId. Il y a peu de motivation pour faire (fondamentalement, un entier) dépendre d'une bibliothèque de base de données complexe.

5
Arthur Havlicek

Je ne suis pas sûr que la question à l'étude reçoive une réponse au bon niveau. J'avais exhorté les sages du forum à réfléchir activement au cœur de la question ici.

U Mad évoque une situation où il pense que la programmation selon sa compréhension de OOP entraînerait généralement de nombreux nœuds feuilles étant des détenteurs de données tandis que son API de niveau supérieur comprend la plupart des comportement.

Je pense que le sujet est allé légèrement tangentiel pour savoir si isVisible serait défini sur Card vs Player; c'était un simple exemple illustré, quoique naïf.

Je devais pousser l'expérimenté ici pour examiner le problème actuel. Je pense qu'il y a une bonne question pour laquelle U Mad a insisté. Je comprends que vous pousseriez les règles et la logique concernée à un objet qui lui est propre; mais si je comprends bien, la question est

  1. Est-il correct d'avoir des constructions simples de détenteurs de données (classes/structures; je me fiche de ce qu'elles sont modélisées comme pour cette question) qui n'offrent pas vraiment beaucoup de fonctionnalités?
  2. Si oui, quelle est la meilleure ou la meilleure façon de les modéliser?
  3. Si non, comment incorporer ces contre-parties de données dans des classes d'API supérieures (y compris le comportement)

Mon avis:

Je pense que vous posez une question de granularité qui est difficile à comprendre dans la programmation orientée objet. Dans ma petite expérience, je n'inclurais pas une entité dans mon modèle qui n'inclut aucun comportement en soi. Si je le dois, j'ai probablement utilisé une construction une structure conçue pour contenir une telle abstraction contrairement à une classe qui a l'idée d'encapsuler des données et un comportement.

3
Harsha

Une source courante de confusion dans OOP vient du fait que de nombreux objets encapsulent deux aspects de l'état: les choses qu'ils connaissent et les choses qui les connaissent. Les discussions sur l'état des objets ignorent souvent ce dernier aspect, car dans les cadres où les références aux objets sont promiscues, il n'y a aucun moyen général de déterminer ce que les choses peuvent savoir sur un objet dont la référence a déjà été exposée au monde extérieur.

Je dirais qu'il serait probablement utile d'avoir un objet CardEntity qui encapsule ces aspects de la carte dans des composants séparés. Un composant concernerait les marquages ​​sur la carte (par exemple "Diamond King" ou "Lava Blast; les joueurs ont une chance d'esquiver AC-3, ou bien de subir des dégâts 2D6"). On pourrait se rapporter à un aspect unique de l'état tel que la position (par exemple, c'est dans le jeu, ou dans la main de Joe, ou sur la table devant Larry). Un troisième pourrait concerner peut le voir (peut-être personne, peut-être un joueur, ou peut-être plusieurs joueurs). Pour garantir que tout est synchronisé, les emplacements où une carte pourrait se trouver ne seraient pas encapsulés comme de simples champs, mais plutôt comme des objets CardSpace; pour déplacer une carte dans un espace, on lui donnerait une référence à l'objet CardSpace approprié; il se retirerait alors de l'ancien espace et se placerait dans le nouvel espace).

L'encapsulation explicite de "qui sait X" séparément de "ce que X sait" devrait aider à éviter beaucoup de confusion. Des précautions sont parfois nécessaires pour éviter les fuites de mémoire, en particulier avec de nombreuses associations (par exemple, si de nouvelles cartes peuvent voir le jour et que les anciennes cartes disparaissent, il faut veiller à ce que les cartes qui doivent être abandonnées ne soient pas perpétuellement attachées à des objets à longue durée de vie ) mais si l'existence de références à un objet forme une partie pertinente de son état, il est tout à fait approprié que l'objet lui-même encapsule explicitement ces informations (même s'il délègue à une autre classe le travail de le gérer réellement).

2
supercat

Cependant, cette approche prive les classes Card et Player d'une fonction membre possible.

Et comment est-ce mauvais/mal avisé?

Pour utiliser une analogie similaire à l'exemple de vos cartes, considérez un Car, un Driver et vous devez déterminer si le Driver peut piloter le Car.

OK, vous avez donc décidé de ne pas vouloir que votre Car sache si le Driver a la bonne clé de voiture ou non, et pour une raison inconnue, vous avez également décidé de ne pas vouloir votre Driver pour en savoir plus sur la classe Car (vous n'avez pas non plus complètement expliqué cela dans votre question d'origine). Par conséquent, vous avez une classe intermédiaire, quelque chose dans le style d'une classe Utils, qui contient la méthode avec règles métier afin de renvoyer une valeur boolean pour le question ci-dessus.

Je pense que ça va. La classe intermédiaire n'a peut-être qu'à vérifier les clés de voiture maintenant, mais peut être refactorisée pour déterminer si le conducteur a un permis de conduire valide, sous l'influence de l'alcool, ou dans un avenir dystopique, vérifier la biométrie de l'ADN. Par encapsulation, il n'y a vraiment pas de gros problèmes à faire coexister ces trois classes.

0
h.j.k.