web-dev-qa-db-fra.com

Problème de style de codage: devrions-nous avoir des fonctions qui prennent un paramètre, le modifient, puis RETOURNENT ce paramètre?

J'ai un petit débat avec mon ami pour savoir si ces deux pratiques ne sont que les deux faces d'une même médaille, ou si l'une est vraiment meilleure.

Nous avons une fonction qui prend un paramètre, en remplit un membre, puis le renvoie:

Item predictPrice(Item item)

Je crois que comme cela fonctionne sur l'objet même qui est passé, il est inutile de retourner l'article. En fait, si quelque chose, du point de vue de l'appelant, il confond les choses car vous pourriez vous attendre à ce qu'il retourne un élément nouvea qu'il ne fait pas.

Il prétend que cela ne fait aucune différence, et cela n'aurait même pas d'importance s'il crée un nouvel objet et le renvoie. Je suis fortement en désaccord, pour les raisons suivantes:

  • Si vous avez plusieurs références à l'élément passé (ou pointeurs ou autre), il alloue un nouvel objet et le renvoie est d'une importance matérielle car ces références seront incorrectes.

  • Dans les langages non gérés en mémoire, la fonction allouant une nouvelle instance revendique la propriété de la mémoire, et donc nous devrons implémenter une méthode de nettoyage qui est appelée à un moment donné.

  • L'allocation sur le tas est potentiellement coûteuse, et il est donc important de savoir si la fonction appelée le fait.

Par conséquent, je pense qu'il est très important de pouvoir voir via une signature de méthode si elle modifie un objet ou en alloue un nouveau. Par conséquent, je pense que la fonction ne faisant que modifier l'objet passé, la signature devrait être:

void predictPrice(Item item)

Dans chaque base de code (certes les bases de code C et C++, pas Java qui est le langage dans lequel nous travaillons) avec lequel j'ai travaillé, le style ci-dessus a été essentiellement respecté et respecté par beaucoup plus programmeurs expérimentés. Il prétend que comme ma taille d'échantillon de bases de code et de collègues est petite sur toutes les bases de code et collègues possibles, et donc mon expérience n'est pas un véritable indicateur de savoir si l'un est supérieur.

Alors, des pensées?

20
Squimmy

C'est vraiment une question d'opinion, mais pour ce que ça vaut, je trouve trompeur de retourner l'article modifié si l'article est modifié en place. Séparément, si predictPrice va modifier l'élément, il devrait avoir un nom qui indique qu'il va le faire (comme setPredictPrice ou quelque chose du genre).

Je préfère (dans l'ordre)

  1. Ce predictPrice était un méthode de Item (modulo une bonne raison pour qu'il ne soit pas, ce qui pourrait bien être dans votre conception globale) qui soit

    • Renvoyé le prix prévu, ou

    • A eu un nom comme setPredictedPrice à la place

  2. Cela predictPricen'a pas modifié Item, mais a renvoyé le prix prévu

  3. Cette predictPrice était une méthode void appelée setPredictedPrice

  4. Ce predictPrice a renvoyé this (pour le chaînage de méthodes vers d'autres méthodes sur n'importe quelle instance dont il fait partie) (et a été appelé setPredictedPrice)

26
T.J. Crowder

Dans un paradigme de conception orienté objet, les choses ne devraient pas modifier des objets en dehors de l'objet lui-même. Toute modification de l'état d'un objet doit être effectuée via des méthodes sur l'objet.

Ainsi, void predictPrice(Item item) en tant que fonction membre d'une autre classe est incorrect . Cela aurait pu être acceptable à l'époque de C, mais pour Java et C++, les modifications d'un objet impliquent un couplage plus profond avec l'objet qui entraînerait probablement d'autres problèmes de conception sur la route (lorsque vous refactorisez la classe et modifiez ses champs, maintenant que 'PredictPrice' doit être changé en dans un autre fichier.

Le retour d'un nouvel objet n'a pas d'effets secondaires associés, le paramètre transmis n'est pas modifié. Vous (la méthode PredictPrice) ne savez pas où ce paramètre est utilisé. Item est-il une clé d'un hachage quelque part? avez-vous changé son code de hachage en faisant cela? Est-ce que quelqu'un d'autre s'y accroche s'attend à ce qu'il ne change pas?

Ces problèmes de conception suggèrent fortement que vous ne devriez pas modifier des objets (je plaiderais pour l'immuabilité dans de nombreux cas), et si vous le faites, les modifications de l'état devraient être contenues et contrôlées par l'objet lui-même plutôt que quelque chose sinon en dehors de la classe.


Regardons ce qui se passe si l'on tripote les champs de quelque chose dans un hachage. Prenons un peu de code:

import Java.util.*;

public class Main {
    public static void main (String[] args) {
        Set set = new HashSet();
        Data d = new Data(1,"foo");
        set.add(d);
        set.add(new Data(2,"bar"));
        System.out.println(set.contains(d));
        d.field1 = 2;
        System.out.println(set.contains(d));
    }

    public static class Data {
        int field1;
        String field2;

        Data(int f1, String f2) {
            field1 = f1;
            field2 = f2;
        }

        public int hashCode() {
            return field2.hashCode() + field1;
        }

        public boolean equals(Object o) {
            if(!(o instanceof Data)) return false;
            Data od = (Data)o;
            return od.field1 == this.field1 && od.field2.equals(this.field2);

        }
    }
}

idéone

Et je dois admettre que ce n'est pas le plus grand code (accéder directement aux champs), mais il sert son objectif de démontrer un problème avec les données mutables utilisées comme clé d'un HashMap, ou dans ce cas, simplement mises dans un HashSet.

La sortie de ce code est:

true
false

Ce qui s'est passé, c'est que le hashCode utilisé lors de son insertion est l'endroit où l'objet se trouve dans le hachage. La modification des valeurs utilisées pour calculer le hashCode ne recalcule pas le hachage lui-même. C'est un danger de mettre tout objet mutable comme clé d'un hachage.

Ainsi, pour revenir à l'aspect original de la question, la méthode appelée ne "sait" pas comment l'objet qu'elle obtient en tant que paramètre est utilisé. Fournir le "permet de muter l'objet" comme la seule façon de le faire signifie qu'il existe un certain nombre de bogues subtils qui peuvent s'introduire. Comme perdre des valeurs dans un hachage ... à moins que vous en ajoutiez plus et que le hachage soit retravaillé - croyez-moi, c'est un méchant bug à traquer (j'ai perdu la valeur jusqu'à ce que j'ajoute 20 éléments supplémentaires à la hashMap, puis soudain, il réapparaît).

  • À moins qu'il n'y ait très bonne raison de modifier l'objet, renvoyer un nouvel objet est la chose la plus sûre à faire.
  • Lorsqu'il y a une bonne raison de modifier l'objet, cette modification doit être effectuée par l'objet lui-même (appel de méthodes) plutôt que par une fonction externe qui peut tordre ses champs.
    • Cela permet de refaçonner l'objet avec un moindre coût de maintenance.
    • Cela permet à l'objet de s'assurer que les valeurs qui calculent son code de hachage ne changent pas (ou ne font pas partie de son calcul de code de hachage)

Connexes: Écraser et renvoyer la valeur de l'argument utilisé comme conditionnel d'une instruction if, à l'intérieur de la même instruction if

11
user40980

Je suis toujours la pratique de ne pas muter l'objet de quelqu'un d'autre. C'est-à-dire, uniquement des objets mutants appartenant à la classe/struct/whathaveyou dans laquelle vous vous trouvez.

Étant donné la classe de données suivante:

class Item {
    public double price = 0;
}

C'est acceptable:

class Foo {
    private Item bar = new Item();

    public void predictPrice() {
        bar.price = bar.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
foo.predictPrice();
System.out.println(foo.bar.price);
// Since I called predictPrice on Foo, which takes and returns nothing, it's
// apparent something might've changed

Et cela est très clair:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public double predictPrice(Item item) {
        return item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
foo.bar.price = baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// Since I explicitly set foo.bar.price to the return value of predictPrice, it's
// obvious foo.bar.price might've changed

Mais c'est beaucoup trop boueux et mystérieux à mes goûts:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public void predictPrice(Item item) {
        item.price = item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// In my head, it doesn't appear that to foo, bar, or price changed. It seems to
// me that, if anything, I've placed a predicted price into baz that's based on
// the value of foo.bar.price
2
Ben Leggiero

La syntaxe est différente entre les différentes langues et il est inutile de montrer un morceau de code qui n'est pas spécifique à une langue car cela signifie des choses complètement différentes dans différentes langues.

En Java, Item est un type de référence - c'est le type de pointeurs vers des objets qui sont des instances de Item. En C++, ce type est écrit comme Item * (La syntaxe pour la même chose est différente entre les langues). Donc Item predictPrice(Item item) in Java est équivalent à Item *predictPrice(Item *item) en C++. Dans les deux cas, c'est une fonction (ou méthode) qui prend un pointeur vers un objet et renvoie un pointeur sur un objet.

En C++, Item (sans rien d'autre) est un type d'objet (en supposant que Item est le nom d'une classe). Une valeur de ce type "est" un objet et Item predictPrice(Item item) déclare une fonction qui prend un objet et retourne un objet. Puisque & N'est pas utilisé, il est transmis et renvoyé par valeur. Cela signifie que l'objet est copié lorsqu'il est transmis et copié lorsqu'il est renvoyé. Il n'y a pas d'équivalent dans Java parce que Java n'a pas de type d'objet).

Alors c'est quoi? Beaucoup de choses que vous posez dans la question (par exemple, "je crois que cela fonctionne sur le même objet qui est passé ...") dépendent de la compréhension exacte de ce qui est passé et retourné ici.

1
user102008

La convention à laquelle j'ai vu adhérer les bases de code est de préférer un style clair dans la syntaxe. Si la fonction change de valeur, préférez passer par le pointeur

void predictPrice(Item * const item);

Ce style se traduit par:

  • Appelant cela, vous voyez:

    predictPrice (& my_item);

et vous savez qu'il sera modifié par votre respect du style.

  • Sinon, préférez passer par const ref ou value lorsque vous ne voulez pas que la fonction modifie l'instance.
1
user27886

Bien que je n'aie pas une préférence marquée, je voterais que le retour de l'objet entraîne une meilleure flexibilité pour une API.

Notez qu'il n'a de sens que lorsque la méthode a la liberté de renvoyer un autre objet. Si votre javadoc indique @returns the same value of parameter a alors c'est inutile. Mais si votre javadoc dit @return an instance that holds the same data than parameter a, puis renvoyer la même instance ou une autre est un détail d'implémentation.

Par exemple, dans mon projet actuel (Java EE), j'ai une couche métier; pour stocker dans la base de données (et attribuer un ID automatique) un employé, disons, un employé j'ai quelque chose comme

 public Employee createEmployee(Employee employeeData) {
   this.entityManager.persist(employeeData);
   return this.employeeData;
 }

Bien sûr, aucune différence avec cette implémentation car JPA renvoie le même objet après avoir attribué l'ID. Maintenant, si je suis passé à JDBC

 public Employee createEmployee(Employee employeeData) {
   PreparedStatement ps = this.connection.prepareStatement("INSERT INTO EMPLOYEE(name, surname, data) VALUES(?, ?, ?)");
   ... // set parameters
   ps.execute();
   int id = getIdFromGeneratedKeys(ps);
   ??????
 }

Maintenant à ????? J'ai trois options.

  1. Définissez un setter pour Id, et je retournerai le même objet. Laid, car setId sera disponible partout.

  2. Effectuez un travail de réflexion intense et définissez l'ID dans l'objet Employee. Sale, mais au moins il est limité à l'enregistrement createEmployee.

  3. Fournissez un constructeur qui copie le Employee d'origine et accepte également le id à définir.

  4. Récupérez l'objet à partir de la base de données (peut-être nécessaire si certaines valeurs de champ sont calculées dans la base de données ou si vous souhaitez remplir une relation immobilière).

Maintenant, pour 1. et 2. vous retournerez peut-être la même instance, mais pour 3. et 4. vous retournerez de nouvelles instances. Si d'après le principe vous avez établi dans votre API que la "vraie" valeur sera celle retournée par la méthode, vous avez plus de liberté.

Le seul véritable argument contre lequel je peux penser est que, en utilisant les implémentations 1. ou 2., les personnes utilisant votre API peuvent ignorer l'attribution du résultat et leur code se briserait si vous passez aux implémentations 3. ou 4.).

Quoi qu'il en soit, comme vous pouvez le voir, ma préférence est basée sur d'autres préférences personnelles (comme interdire un setter pour les ID), alors peut-être que cela ne s'appliquera pas à tout le monde.

0
SJuan76

Lors du codage en Java, il faut essayer de faire correspondre l'utilisation de chaque objet de type mutable à l'un des deux modèles:

  1. Exactement une entité est considérée comme le "propriétaire" de l'objet mutable et considère l'état de cet objet comme faisant partie du sien. D'autres entités peuvent détenir des références, mais doivent considérer la référence comme identifiant un objet qui appartient à quelqu'un d'autre.

  2. Bien que l'objet soit mutable, personne qui détient une référence à celui-ci n'est autorisé à le muter, ce qui rend l'instance effectivement immuable (notez qu'en raison des bizarreries du modèle de mémoire de Java, il n'y a pas de bon moyen de rendre un objet effectivement immuable doté d'un thread- sécurité sémantique immuable sans ajouter un niveau supplémentaire d'indirection à chaque accès). Toute entité qui encapsule l'état à l'aide d'une référence à un tel objet et souhaite modifier l'état encapsulé doit créer un nouvel objet qui contient l'état approprié.

Je n'aime pas que les méthodes mutent l'objet sous-jacent et renvoient une référence, car elles donnent l'apparence de s'adapter au deuxième motif ci-dessus. Il y a quelques cas où l'approche peut être utile, mais le code qui va muter les objets devrait look comme il va le faire; un code comme myThing = myThing.xyz(q); ressemble beaucoup moins à une mutation de l'objet identifié par myThing qu'à un simple myThing.xyz(q);.

0
supercat