Y a-t-il une raison d'utiliser des classes de «vieilles données ordinaires»?

43

Dans le code existant, je vois parfois des classes qui ne sont que des wrappers pour les données. quelque chose comme:

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

Ma compréhension de OO est que les classes sont des structures pour les données et les méthodes d'exploitation sur ces données. Cela semble exclure les objets de ce type. Pour moi, ils ne sont rien de plus qu'une structssorte de défaite pour le but de OO. Je ne pense pas que ce soit nécessairement mauvais, bien que cela puisse être une odeur de code.

Existe-t-il un cas où de tels objets seraient nécessaires? Si cela est utilisé souvent, est-ce que cela rend la conception suspecte?

Michael K
la source
1
Cela ne répond pas tout à fait à votre question mais semble néanmoins pertinent: stackoverflow.com/questions/36701/struct-like-objects-in-java
Adam Lear
2
Je pense que le terme que vous recherchez est POD (Plain Old Data).
Gaurav
9
Ceci est un exemple typique de programmation structurée. Pas nécessairement mauvais, mais pas orienté objet.
Konrad Rudolph
1
cela ne devrait-il pas être sur le débordement de pile?
Muad'Dib
7
@ Muad'Dib: techniquement, il s'agit de programmeurs. Votre compilateur ne se soucie pas si vous utilisez des vieilles structures de données simples. Votre processeur en profite probablement (dans le genre "J'aime l'odeur des données fraîches du cache"). Ce sont des gens qui se raccrochent à la question "est-ce que cela rend ma méthodologie moins pure?" des questions.
Shog9

Réponses:

67

Certainement pas le mal et pas une odeur de code dans mon esprit. Les conteneurs de données sont des citoyens OO valides. Parfois, vous souhaitez encapsuler des informations connexes. C'est beaucoup mieux d'avoir une méthode comme

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

que

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

L'utilisation d'une classe vous permet également d'ajouter un paramètre supplémentaire à Bottle sans modifier tous les appelants de DoStuffWithBottle. Et vous pouvez sous-classer Bottle et augmenter encore la lisibilité et l'organisation de votre code, si nécessaire.

Il existe également des objets de données simples qui peuvent être renvoyés à la suite d'une requête de base de données, par exemple. Je crois que le terme pour eux dans ce cas est "objet de transfert de données".

Dans certaines langues, il y a aussi d'autres considérations. Par exemple, en C #, les classes et les structures se comportent différemment, puisque les structures sont un type de valeur et que les classes sont des types de référence.

Adam Lear
la source
25
Nah. DoStuffWithdevrait être une méthode de la Bottleclasse en POO (et devrait très probablement être immuable, aussi). Ce que vous avez écrit ci-dessus n'est pas un bon modèle dans un langage OO (sauf si vous vous connectez avec une API héritée). Il s'agit toutefois d'une conception valide dans un environnement non OO.
Konrad Rudolph
11
@ Javier: Java n'est donc pas la meilleure solution non plus. L'accent mis par Java sur OO est accablant, le langage n'est fondamentalement bon en rien d'autre.
Konrad Rudolph
11
@ JohnL: J'étais bien sûr en train de simplifier. Mais en général, les objets dans OO encapsulent l' état et non les données. C'est une distinction fine mais importante. Le but de la programmation orientée objet est précisément de ne pas disposer de beaucoup de données. Il envoie des messages entre les états qui créent un nouvel état. Je ne vois pas comment vous pouvez envoyer des messages à un objet sans méthode ou à partir de celui-ci. (Et c'est la définition originale de la POO , donc je ne considère qu'il est imparfait.)
Konrad Rudolph
13
@ Konrad Rudolph: C'est pourquoi j'ai explicitement fait le commentaire à l'intérieur de la méthode. Je conviens que les méthodes qui affectent des instances de Bottle devraient être classées dans cette classe. Mais si un autre objet doit modifier son état en fonction des informations contenues dans Bottle, alors je pense que ma conception sera assez valide.
Adam Lear
10
@ Konrad, je ne suis pas d'accord pour dire que doStuffWithBottle devrait aller dans la classe des bouteilles. Pourquoi une bouteille devrait-elle savoir se débrouiller seule? doStuffWithBottle indique que quelque chose d' autre fera quelque chose avec une bouteille. Si la bouteille avait cela dedans, ce serait un couplage étroit. Cependant, si la classe Bottle avait une méthode isFull (), cela serait tout à fait approprié.
Nemi
25

Les classes de données sont valables dans certains cas. Les DTO sont un bon exemple cité par Anna Lear. En général cependant, vous devriez les considérer comme la graine d'une classe dont les méthodes n'ont pas encore germé. Et si vous en rencontrez beaucoup dans l'ancien code, traitez-les comme une forte odeur de code. Ils sont souvent utilisés par de vieux programmeurs C / C ++ qui n’ont jamais effectué la transition vers la programmation OO et sont un signe de programmation procédurale. Vous pouvez avoir des ennuis avant même de vous fier à des getters et des setters (ou pire encore, à un accès direct à des membres non privés). Prenons un exemple de méthode externe nécessitant des informations Bottle.

Voici Bottleune classe de données):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Ici nous avons donné Bottleun comportement):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

La première méthode enfreint le principe "Ne pas demander", et en gardant l' Bottleidiot, nous avons laissé des connaissances implicites sur les bouteilles, telles que ce qui fait qu'un fragment (le Cap), glisse dans une logique en dehors de la Bottleclasse. Vous devez être sur vos gardes pour prévenir ce type de «fuite» lorsque vous comptez habituellement sur les getters.

La seconde méthode Bottlene demande que ce dont elle a besoin pour faire son travail et laisse Bottledécider si elle est fragile ou plus grande qu'une taille donnée. Le résultat est un couplage beaucoup plus souple entre la méthode et la mise en œuvre de Bottle. Un effet secondaire agréable est que la méthode est plus propre et plus expressive.

Vous utiliserez rarement autant de champs d'un objet sans écrire une logique qui devrait résider dans la classe contenant ces champs. Déterminez quelle est cette logique et déplacez-la là où elle se trouve.

Mike E
la source
1
Je ne peux pas croire que cette réponse n’a pas de voix (enfin, vous en avez une maintenant). Cela pourrait être un exemple simple, mais lorsque OO est suffisamment abusé, vous obtenez des classes de service qui se transforment en cauchemars contenant des tonnes de fonctionnalités qui auraient dû être encapsulées dans des classes.
Alb.
"par d’anciens programmeurs C / C ++ qui n’ont jamais effectué la transition (sic) vers OO"? Les programmeurs C ++ sont généralement de bons OO, car c'est un langage OO, c'est-à-dire l'intérêt d'utiliser C ++ au lieu de C.
nappyfalcon
7

Si c'est le genre de chose dont vous avez besoin, c'est bien, mais s'il vous plaît, s'il vous plaît, s'il vous plaît, faites-le comme

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

au lieu de quelque chose comme

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

S'il vous plaît.

compman
la source
14
Le seul problème avec cela est qu'il n'y a pas de validation. Toute logique quant à ce qu'une bouteille valide est doit être dans la classe Bottle. Cependant, en utilisant votre implémentation proposée, je peux avoir une bouteille de hauteur et de diamètre négatifs. Il est impossible d'imposer des règles commerciales à l'objet sans valider chaque fois que l'objet est utilisé. En utilisant la deuxième méthode, je peux m'assurer que si j'ai un objet Bottle, il était, est et sera toujours un objet Bottle valide conformément à mon contrat.
Thomas Owens
6
C'est un domaine où .NET a un léger avantage sur les propriétés, car vous pouvez ajouter un accesseur de propriété avec une logique de validation, avec la même syntaxe que si vous accédez à un champ. Vous pouvez également définir une propriété où les classes peuvent obtenir la valeur de la propriété, mais pas la définir.
JohnL
3
@ user9521 Si vous êtes sûr que votre code ne causera pas d'erreur fatale avec des "mauvaises" valeurs, choisissez votre méthode. Cependant, si vous avez besoin d'une validation supplémentaire, ou de la possibilité d'utiliser un chargement différé, ou d'autres contrôles lorsque les données sont lues ou écrites, utilisez des getters et des setters explicites. Personnellement, j'ai tendance à garder mes variables confidentielles et à utiliser des accesseurs et des setters pour plus de cohérence. De cette façon, toutes mes variables sont traitées de la même manière, indépendamment de la validation et / ou d’autres techniques "avancées".
Jonathan
2
L'avantage d'utiliser le constructeur est qu'il est beaucoup plus facile de rendre la classe immuable. Ceci est essentiel si vous souhaitez écrire du code multi-thread.
Fortyrunner
7
Je rendrais les champs finaux chaque fois que possible. IMHO J'aurais préféré que les champs soient finaux par défaut et qu'ils aient un mot clé pour les champs mutables. p.ex. var
Peter Lawrey
6

Comme @Anna a dit, certainement pas le mal. Bien sûr, vous pouvez mettre des opérations (méthodes) dans des classes, mais seulement si vous le souhaitez . Tu n'es pas obligé .

Permettez-moi un petit reproche à propos de l’idée qu’il faut placer des opérations dans des classes, ainsi que de l’idée que les classes sont des abstractions. En pratique, cela encourage les programmeurs à

  1. Créez plus de classes que nécessaire (structure de données redondante). Lorsqu'une structure de données contient plus de composants que le minimum nécessaire, elle n'est pas normalisée et contient donc des états incohérents. En d’autres termes, lorsqu’il est modifié, il doit être modifié à plus d’un endroit pour rester cohérent. L'échec à effectuer toutes les modifications coordonnées le rend incohérent et constitue un bogue.

  2. Résolvez le problème 1 en définissant des méthodes de notification afin que, si la partie A est modifiée, elle tente de propager les modifications nécessaires aux parties B et C. Il est donc essentiel de recourir à des méthodes d’accesseur d’obtention et de définition. Comme il s'agit d'une pratique recommandée, il semble que le problème 1 soit excusé, ce qui entraîne davantage de problèmes 1 et plus de solution 2. Cela entraîne non seulement des bogues dus à une mise en œuvre incomplète des notifications, mais également à un problème de performances qui entrave les notifications. Ce ne sont pas des calculs infinis, mais des calculs très longs.

Ces concepts sont enseignés comme de bonnes choses, généralement par des enseignants qui n’ont pas eu à travailler avec des millions d’applications monstres monstres confrontées à ces problèmes.

Voici ce que j'essaie de faire:

  1. Conservez les données aussi normalisées que possible. Ainsi, lorsqu'une modification est apportée aux données, elle est effectuée avec le moins de points de code possible, afin de minimiser la probabilité d'entrer un état incohérent.

  2. Lorsque les données ne doivent pas être normalisées et que la redondance est inévitable, n'utilisez pas de notifications pour tenter de les maintenir cohérentes. Plutôt tolérer une incohérence temporaire. Résolvez les incohérences avec le balayage périodique des données par un processus ne faisant que cela. Cela centralise la responsabilité du maintien de la cohérence, tout en évitant les problèmes de performances et de correction auxquels les notifications sont sujettes. Il en résulte un code beaucoup plus petit, sans erreur et efficace.

Mike Dunlavey
la source
3

Ce type de classes est très utile lorsque vous utilisez des applications de taille moyenne / volumineuse, pour certaines raisons:

  • il est assez facile de créer des cas de test et de s'assurer de la cohérence des données.
  • il contient tous les types de comportements qui impliquent cette information, de sorte que le temps de suivi des bogues de données est réduit
  • Leur utilisation devrait garder les arguments de méthode légers.
  • Lorsque vous utilisez des ORM, ces classes offrent souplesse et cohérence. L'ajout d'un attribut complexe calculé sur la base d'informations simples déjà présentes dans la classe a pour résultat d'écrire une méthode simple. Cela est bien plus agile et productif que de devoir vérifier votre base de données et de s’assurer que toutes les bases de données sont corrigées de nouvelles modifications.

Donc, pour résumer, selon mon expérience, ils sont généralement plus utiles que gênants.

Guiman
la source
3

Avec la conception du jeu, les milliers d’appels de fonction et les écouteurs d’événements peuvent parfois valoir la peine d’avoir des classes qui stockent uniquement des données et d’autres classes qui parcourent toutes les classes de données uniquement pour exécuter la logique.

Attaquer
la source
3

D'accord avec Anna Lear,

Certainement pas le mal et pas une odeur de code dans mon esprit. Les conteneurs de données sont des citoyens OO valides. Parfois, vous souhaitez encapsuler des informations connexes. C'est beaucoup mieux d'avoir une méthode comme ...

Parfois, les gens oublient de lire les conventions de codage Java de 1999 qui expliquent clairement que ce type de programmation convient parfaitement. En fait, si vous l'évitez, votre code va sentir! (trop de getters / setters)

D'après les conventions de code Java 1999: Un exemple de variables d'instance publique appropriées est le cas où la classe est essentiellement une structure de données, sans comportement. En d'autres termes, si vous avez utilisé une structure au lieu d'une classe (si la structure est supportée par Java), il est approprié de rendre publiques les variables d'instance de la classe. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

Utilisés correctement, les POD (structures de données anciennes) sont meilleurs que les POJO, tout comme les POJO sont souvent meilleurs que les EJB.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures

Richard Faber
la source
3

Les structures ont leur place, même en Java. Vous ne devriez les utiliser que si les deux choses suivantes sont vraies:

  • Vous avez juste besoin d'agréger des données qui n'ont aucun comportement, par exemple pour les passer en paramètre.
  • Peu importe le type de valeurs que les données agrégées ont

Si tel est le cas, vous devez alors rendre les champs publics et ignorer les getters / setters. De toute façon, les accesseurs et les setters sont maladroits, et Java est idiot de ne pas avoir de propriétés comme un langage utile. Comme votre objet de type struct ne devrait de toute façon pas utiliser de méthode, les champs publics ont le plus de sens.

Toutefois, si l’un ou l’autre de ces critères ne s’applique pas, vous avez affaire à une véritable classe. Cela signifie que tous les champs doivent être privés. (Si vous avez absolument besoin d'un champ dont la portée est plus accessible, utilisez un getter / setter.)

Pour vérifier si votre supposée-structure a un comportement, regardez quand les champs sont utilisés. Si cela semble violer tell, ne demandez pas , alors vous devez déplacer ce comportement dans votre classe.

Si certaines de vos données ne doivent pas changer, vous devez alors rendre tous ces champs définitifs. Vous pourriez envisager de rendre votre classe immuable . Si vous devez valider vos données, fournissez une validation dans les setters et les constructeurs. (Une astuce utile consiste à définir un poseur privé et à modifier votre champ au sein de votre classe en utilisant uniquement ce sélecteur.)

Votre exemple de bouteille échouerait probablement aux deux tests. Vous pourriez avoir un code (artificiel) qui ressemble à ceci:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

Au lieu de cela, il devrait être

double volume = bottle.calculateVolumeAsCylinder();

Si vous changiez la hauteur et le diamètre, s'agirait-il de la même bouteille? Probablement pas. Celles-ci devraient être finales. Une valeur négative est-elle acceptable pour le diamètre? Votre bouteille doit-elle être plus grande que large? Le cap peut-il être nul? Non? Comment validez-vous cela? Supposons que le client soit stupide ou méchant. ( Il est impossible de faire la différence. ) Vous devez vérifier ces valeurs.

Voici à quoi pourrait ressembler votre nouvelle classe Bottle:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
Eva
la source
0

IMHO il n'y a souvent pas assez de classes comme celle-ci dans des systèmes fortement orientés objet. Je dois soigneusement qualifier cela.

Bien sûr, si les champs de données ont une portée et une visibilité étendues, cela peut être extrêmement indésirable s’il existe des centaines, voire des milliers, d’endroits dans votre base de code manipulant de telles données. C'est demander des ennuis et des difficultés à maintenir des invariants. Cependant, cela ne signifie pas pour autant que chaque classe de la base de code profite de la dissimulation d'informations.

Mais il existe de nombreux cas où ces champs de données auront une portée très étroite. Un exemple très simple est une Nodeclasse privée d'une structure de données. Cela peut souvent simplifier énormément le code en réduisant le nombre d'interactions entre objets, si celles-ci Nodepouvaient simplement consister en données brutes. Cela sert de mécanisme de découplage car la version alternative pourrait nécessiter un couplage bidirectionnel, par exemple, Tree->Nodeet Node->Treepar opposition à simple Tree->Node Data.

Un exemple plus complexe serait celui des systèmes à composants d'entités, tels qu'ils sont souvent utilisés dans les moteurs de jeux. Dans ces cas, les composants ne sont souvent que des données brutes et des classes similaires à celle que vous avez montrée. Cependant, leur portée / visibilité a tendance à être limitée car il n’ya généralement qu’un ou deux systèmes pouvant accéder à ce type de composant particulier. En conséquence, il est toujours assez facile de maintenir des invariants dans ces systèmes et, de plus, ces systèmes ont très peu d’ object->objectinteractions, ce qui facilite la compréhension de ce qui se passe au niveau de l’oiseau.

Dans de tels cas, vous pouvez vous retrouver avec quelque chose de plus semblable aux interactions (ce diagramme indique les interactions, pas le couplage, car un diagramme de couplage peut inclure des interfaces abstraites pour la deuxième image ci-dessous):

entrez la description de l'image ici

... par opposition à ceci:

entrez la description de l'image ici

... et le premier type de système tend à être beaucoup plus facile à maintenir et à raisonner en termes d’exactitude, en dépit du fait que les dépendances vont effectivement vers les données. Vous obtenez beaucoup moins de couplage principalement parce que beaucoup de choses peuvent être transformées en données brutes au lieu que les objets interagissent les uns avec les autres pour former un graphique très complexe d'interactions.


la source
-1

Il existe même de nombreuses créatures étranges dans le monde de la programmation orientée objet. J'ai déjà créé une classe, abstraite, qui ne contient rien, juste pour être un parent commun de deux autres classes ... c'est la classe Port:

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

Donc SceneMember est la classe de base, il fait tout le travail qui doit apparaître sur la scène: ajouter, supprimer, obtenir un ID, etc. Le message est la connexion entre les ports, il a sa propre vie. InputPort et OutputPort contiennent leurs propres fonctions spécifiques.

Le port est vide. Il sert simplement à regrouper InputPort et OutputPort pour, par exemple, une liste de ports. Il est vide car SceneMember contient tous les éléments permettant d'agir en tant que membre, et InputPort et OutputPort contiennent les tâches spécifiées par le port. InputPort et OutputPort sont tellement différents qu’ils n’ont pas de fonctions communes (à l’exception de SceneMember). Donc, le port est vide. Mais c'est légal.

C'est peut-être un anti - modèle , mais j'aime bien.

(C'est comme le mot "compagnon", qui est utilisé à la fois pour "femme" et "mari". Vous n'utilisez jamais le mot "partenaire" pour désigner une personne concrète, car vous connaissez son sexe, et cela n'a pas d'importance. si il / elle est marié ou non, vous utilisez donc "quelqu'un" à la place, mais il existe toujours et est utilisé dans de rares situations abstraites, par exemple un texte de loi.)

ern0
la source
1
Pourquoi vos ports doivent-ils étendre SceneMember? Pourquoi ne pas créer les classes de ports à utiliser sur SceneMembers?
Michael K
1
Alors, pourquoi ne pas utiliser le modèle d'interface de marqueur standard? Fondamentalement identique à votre classe de base abstraite vide, mais c'est un idiome beaucoup plus commun.
TMN
1
@ Michael: Seulement pour des raisons théoriques, j'ai réservé Port pour une utilisation future, mais ce futur n'est pas encore arrivé et peut-être ne le sera-t-il jamais. Je n'avais pas réalisé qu'ils n'avaient aucun commun, contrairement à leurs noms. Je paierai une indemnité pour tous ceux qui ont subi une perte de cette classe vide ...
ern0
1
@TMN: Les types SceneMember (dérivés) ont la méthode getMemberType (). Il existe plusieurs cas où Scene est analysé pour un sous-ensemble d'objets SceneMember.
ern0
4
Cela ne répond pas à la question.
Eva