web-dev-qa-db-fra.com

Y a-t-il une raison d'utiliser des classes de "données anciennes"?

Dans le code hérité, 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 de fonctionnement sur ces données. Cela semble exclure les objets de ce type. Pour moi, ils ne sont rien plus que structs et sorte de défaire le but de OO. Je ne pense pas que ce soit nécessairement mauvais, même si cela peut être une odeur de code.

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

44
Michael K

Certainement pas mal et pas une odeur de code dans mon esprit. Les conteneurs de données sont un citoyen OO valide. Parfois, vous voulez encapsuler des informations connexes ensemble. Il est préférable 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 chaque appelant de DoStuffWithBottle. Et vous pouvez sous-classer Bouteille 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 existe également d'autres considérations. Par exemple, en C #, les classes et les structures se comportent différemment, car les structures sont un type de valeur et les classes sont des types de référence.

68
Adam Lear

Les classes de données sont valides dans certains cas. Les DTO sont un bon exemple mentionné par Anna Lear. En général cependant, vous devez les considérer comme le germe 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 les anciens programmeurs C/C++ qui n'ont jamais effectué la transition vers la programmation OO et sont un signe de programmation procédurale. S'appuyant sur des getters et des setters tout le temps (ou pire encore, sur accès direct de membres non privés) peut vous causer des ennuis avant de vous en rendre compte. Prenons un exemple de méthode externe qui nécessite des informations de Bottle.

Ici Bottle est une 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é à Bottle un certain comportement):

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

La première méthode viole le principe Tell-Don't-Ask, et en gardant Bottle muet, nous avons laissé les connaissances implicites sur les bouteilles, telles que ce qui rend un fragment (le Cap), se glisser dans logique qui est en dehors de la classe Bottle. Vous devez être sur vos gardes pour éviter ce genre de "fuite" lorsque vous comptez habituellement sur les getters.

La deuxième méthode ne demande à Bottle que ce dont elle a besoin pour faire son travail et laisse Bottle décider si elle est fragile ou plus grande qu'une taille donnée. Le résultat est un couplage beaucoup plus lâche 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 ces nombreux champs d'un objet sans écrire une logique qui devrait résider dans la classe avec ces champs. Déterminez ce qu'est cette logique, puis déplacez-la là où elle appartient.

25
Mike E

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, 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.

7
compman

Comme l'a dit @Anna, certainement pas mal. Bien sûr, vous pouvez mettre des opérations (méthodes) en classes, mais seulement si vous voulez pour. Vous n'avez pas avez pour.

Permettez-moi un petit reproche sur l'idée que vous devez mettre des opérations dans les classes, ainsi que 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 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 afin de rester cohérent. Le fait de ne pas effectuer toutes les modifications coordonnées le rend incohérent et constitue un bogue.

  2. Résolvez le problème 1 en mettant des méthodes notification, de sorte que si la partie A est modifiée, il essaie de propager les changements nécessaires aux parties B et C. C'est la principale raison pour laquelle il est recommandé d'avoir get-and -définir les méthodes d'accesseur. Étant donné que cette pratique est recommandée, elle semble excuser le problème 1, provoquant davantage de problème 1 et plus de solution 2. Cela entraîne non seulement des bogues dus à une mise en œuvre incomplète des notifications, mais aussi un problème de performances rédhibitoires des notifications incontrôlables. Ce ne sont pas des calculs infinis, simplement de très longs calculs.

Ces concepts sont enseignés comme de bonnes choses, généralement par des enseignants qui n'ont pas eu à travailler avec des applications monstres à un million de lignes criblées de ces problèmes.

Voici ce que j'essaie de faire:

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

  2. Lorsque les données doivent être non normalisées et que la redondance est inévitable, n'utilisez pas de notifications pour tenter de les garder cohérentes. Au lieu de cela, tolérez l'incohérence temporaire. Résolvez les incohérences avec des balayages périodiques dans les données par un processus qui ne fait que cela. Cela centralise la responsabilité du maintien de la cohérence, tout en évitant les problèmes de performances et d'exactitude auxquels les notifications sont sujettes. Il en résulte un code beaucoup plus petit, sans erreur et efficace.

6
Mike Dunlavey

Avec la conception de jeux, la surcharge de 1000 appels de fonctions et les écouteurs d'événements peuvent parfois valoir la peine d'avoir des classes qui ne stockent que des données et d'autres classes qui parcourent toutes les classes de données uniquement pour exécuter la logique.

3
AttackingHobo

D'accord avec Anna Lear,

Certainement pas mal et pas une odeur de code dans mon esprit. Les conteneurs de données sont un citoyen OO valide. Parfois, vous voulez encapsuler des informations connexes ensemble. Il est préférable d'avoir une méthode comme ...

Parfois, les gens oublient de lire les conventions de codage de 1999 Java qui indiquent très clairement que ce type de programmation est parfaitement bien. En fait, si vous l'évitez, votre code sentira! (Trop de getters/setters)

From Java Conventions de code 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 auriez utilisé une structure à la place d'une classe (if Java structure prise en charge), alors 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

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

3
Richard Faber

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

  • il est assez facile de créer des cas de test et de garantir la cohérence des données.
  • il contient toutes sortes de comportements qui impliquent ces informations, donc 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 flexibilité et cohérence. L'ajout d'un attribut complexe calculé sur la base d'informations simples déjà présentes dans la classe, résume par écrit une méthode simple. C'est beaucoup plus agile et productif que d'avoir à vérifier votre base de données et à vous assurer que toutes les bases de données sont corrigées avec de nouvelles modifications.

Donc, pour résumer, d'après mon expérience, ils sont généralement plus utiles qu'ennuyeux.

3
guiman

Les structures ont leur place, même en Java. Vous ne devez 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 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 rendre les champs publics et ignorer les getters/setters. Les accesseurs et les setters sont maladroits de toute façon, et Java est idiot de ne pas avoir de propriétés comme un langage utile. Puisque votre objet de type struct ne devrait pas avoir de méthodes de toute façon, les champs publics ont le plus de sens.

Cependant, si l'un ou l'autre ne s'applique pas, vous avez affaire à une vraie classe. Cela signifie que tous les champs doivent être privés. (Si vous avez absolument besoin d'un champ à une portée plus accessible, utilisez un getter/setter.)

Pour vérifier si votre structure supposée a un comportement, regardez quand les champs sont utilisés. S'il semble violer dites, 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 rendre tous ces champs définitifs. Vous pourriez envisager de rendre votre classe immuable . Si vous devez valider vos données, fournissez la validation dans les setters et les constructeurs. (Une astuce utile consiste à définir un setter privé et à modifier votre champ au sein de votre classe en utilisant uniquement ce setter.)

Votre exemple de bouteille échouerait très 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 changez la hauteur et le diamètre, serait-ce la même bouteille? Probablement pas. Celles-ci devraient être définitives. Une valeur négative est-elle correcte pour le diamètre? Votre bouteille doit-elle être plus haute que large? Le plafond peut-il être nul? Non? Comment validez-vous cela? Supposons que le client soit soit stupide soit mauvais. ( Il est impossible de faire la différence. ) Vous devez vérifier ces valeurs.

Voici à quoi pourrait ressembler votre nouvelle classe de bouteilles:

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...

}
3
Eva

À mon humble avis, il n'y a souvent pas suffisamment de classes comme celle-ci dans les 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 y a des centaines ou des milliers d'endroits dans votre base de code altérant ces données. Cela demande des ennuis et des difficultés à maintenir des invariants. Pourtant, en même temps, cela ne signifie pas que chaque classe de la base de code entière bénéficie 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 classe privée Node d'une structure de données. Il peut souvent simplifier considérablement le code en réduisant le nombre d'interactions d'objets en cours si ledit Node peut simplement être constitué de données brutes. Cela sert de mécanisme de découplage car la version alternative pourrait nécessiter un couplage bidirectionnel de, disons, Tree->Node et Node->Tree par opposition à simplement Tree->Node Data.

Un exemple plus complexe serait les systèmes à composants d'entité 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 comme celle que vous avez montrée. Cependant, leur portée/visibilité a tendance à être limitée car il n'y a généralement qu'un ou deux systèmes qui peuvent accéder à ce type particulier de composant. En conséquence, vous avez toujours tendance à trouver assez facile de maintenir des invariants dans ces systèmes et, en outre, ces systèmes ont très peu de object->object interactions, ce qui permet de comprendre très facilement ce qui se passe au niveau des yeux de l'oiseau.

Dans de tels cas, vous pouvez vous retrouver avec quelque chose de plus similaire en ce qui concerne les 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):

enter image description here

... par opposition à ceci:

enter image description here

... et l'ancien type de système a tendance à être beaucoup plus facile à entretenir et à raisonner en termes d'exactitude, malgré le fait que les dépendances se dirigent en fait 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 d'objets interagissant les uns avec les autres formant un graphique d'interactions très complexe.

0
user204677