web-dev-qa-db-fra.com

Est-ce une mauvaise idée d'utiliser des getters / setters et / ou des propriétés?

Je suis perplexe devant les commentaires sous cette réponse: https://softwareengineering.stackexchange.com/a/358851/212639

Un utilisateur s'y oppose contre l'utilisation de getters/setters et de propriétés. Il soutient que la plupart du temps, leur utilisation est le signe d'une mauvaise conception. Ses commentaires gagnent des votes positifs.

Eh bien, je suis un programmeur débutant, mais pour être honnête, je trouve le concept de propriétés très attrayant. Pourquoi un tableau ne devrait-il pas permettre le redimensionnement en écrivant dans sa propriété size ou length? Pourquoi ne devrions-nous pas obtenir tous les éléments dans un arbre binaire en lisant sa propriété size? Pourquoi un modèle de voiture ne devrait-il pas exposer sa vitesse maximale en tant que propriété? Si nous faisons un RPG, pourquoi un monstre ne devrait-il pas voir son attaque comme un getter? Et pourquoi ne devrait-il pas exposer son HP actuel comme getter? Ou si nous créons un logiciel de conception domestique, pourquoi ne devrions-nous pas autoriser la recoloration d'un mur en écrivant dans son color setter?

Tout cela semble si naturel, si évident pour moi que je n'ai jamais pu conclure que l'utilisation de propriétés ou de getters/setters pourrait être un signe d'avertissement.

L'utilisation de propriétés et/ou de setters de getters est-elle généralement une mauvaise idée et si oui, pourquoi?

12
gaazkam

Le simple fait d'exposer des champs - que ce soit en tant que champs publics, propriétés ou méthodes d'accesseur - peut être un indicateur d'une modélisation d'objet insuffisante. Le résultat est que nous demandons un objet pour diverses données et prenons des décisions sur ces données. Ces décisions seront donc prises en dehors de l'objet. Si cela se produit à plusieurs reprises, peut-être que cette décision devrait être la responsabilité de l'objet, et devrait être fournie par une méthode de cet objet: Nous devrions dire l'objet quoi faire, et il devrait comprendre comment le faire par lui-même.

Bien sûr, ce n'est pas toujours approprié. Si vous en faites trop, vos objets se retrouvent avec une collection sauvage de nombreuses petites méthodes indépendantes qui ne sont utilisées qu'une seule fois. Il est peut-être préférable de séparer votre comportement de vos données et de choisir une approche plus procédurale. Cela s'appelle parfois un "modèle de domaine anémique" . Vos objets ont alors très peu de comportement, mais exposent leur état à travers un certain mécanisme.

Si vous exposez tout type de propriétés, vous devez être cohérent, tant dans la dénomination que dans la technique utilisée. La façon de procéder dépend principalement de la langue. Soit exposez tous en tant que champs publics, soit tous via des propriétés, soit tous via des méthodes d'accesseur. Par exemple. ne combinez pas un champ rectangle.x avec une méthode rectange.y(), ou une méthode user.name() avec un getter user.getEmail().

Un problème avec les propriétés et en particulier avec les propriétés ou setters inscriptibles est que vous risquez d'affaiblir l'encapsulation de votre objet. L'encapsulation est importante car vous pouvez raisonner sur l'exactitude de votre objet de manière isolée. Cette modularisation vous permet de comprendre plus facilement des systèmes complexes. Mais si nous accédons aux champs d'un objet au lieu d'émettre des commandes de haut niveau, nous devenons de plus en plus couplés à cette classe. Si des valeurs arbitraires peuvent être écrites dans un champ depuis l'extérieur d'un objet, il devient très difficile de maintenir un état cohérent pour l'objet. Il est de la responsabilité de l'objet de rester cohérent, ce qui signifie valider les données entrantes. Par exemple. un objet tableau ne doit pas avoir sa longueur définie sur une valeur négative. Ce type de validation est impossible pour un domaine public et facile à oublier pour un setter autogénéré. C'est plus évident avec une opération de haut niveau comme array.resize().

Termes de recherche suggérés pour plus d'informations:

22
amon

Ce n'est pas une conception intrinsèquement mauvaise, mais comme toute fonctionnalité puissante, elle peut être utilisée à mauvais escient.

Le point des propriétés (méthodes getter et setter implicites) est qu'elles fournissent une abstraction syntaxique puissante permettant à du code de consommation de les traiter logiquement comme des champs tout en gardant le contrôle de l'objet qui les définit.

Bien que cela soit souvent pensé en termes d'encapsulation, cela a souvent plus à voir avec l'état de contrôle A et B avec la commodité syntaxique pour l'appelant.

Pour obtenir un certain contexte, nous devons d'abord considérer le langage de programmation lui-même. Alors que de nombreux langages ont des "propriétés", la terminologie et les capacités varient considérablement.

Le langage de programmation C # a une notion très sophistiquée de propriétés à la fois sémantique et syntaxique. Il permet au getter ou au setter d'être facultatif, et il leur permet de manière critique d'avoir différents niveaux de visibilité. Cela fait une grande différence si vous souhaitez exposer une API à grain fin.

Considérons un assembly qui définit un ensemble de structures de données qui sont par nature des graphes cycliques. Voici un exemple simple:

public sealed class Document
{
    public Document(IEnumerable<Word> words)
    {
        this.words = words.ToList();
        foreach (var Word in this.words)
        {
            Word.Document = this;
        }
    }

    private readonly IReadOnlyList<Word> words;
}

public sealed class Word
{
    public Document Document
    {
        get => this.document;
        internal set
        {
            this.document = value;
        }
    }

    private Document document;
}

Idéalement, Word.Document ne serait pas du tout modifiable, mais je ne peux pas l'exprimer dans la langue. J'aurais pu créer un IDictionary<Word, Document> qui a mappé Words à Documents mais finalement il y aura une mutabilité quelque part

Le but ici est de créer un graphe cyclique de telle sorte que toute fonction à laquelle un mot est attribué puisse interroger le Word quant à son contenant Document au moyen du Word.Document propriété (getter). Nous voulons également empêcher la mutation par les consommateurs après la création de Document. Le seul but du setter est d'établir le lien. Il ne doit être écrit qu'une seule fois. C'est difficile à faire car nous devons d'abord créer les instances Word. En utilisant la capacité de C # à attribuer différents niveaux d'accessibilité au getter et au setter de la propriété même, nous sommes en mesure d'encapsuler la mutabilité de la Word.Document propriété au sein de l'assembly conteneur.

Le code consommant ces classes à partir d'un autre assembly verra la propriété en lecture seule (comme ayant uniquement un get et non un set).

Cet exemple m'amène à ma chose préférée sur les propriétés. Ils ne peuvent avoir qu'un getter. Que vous souhaitiez simplement renvoyer une valeur calculée ou que vous souhaitiez exposer uniquement la capacité à lire mais pas à écrire, l'utilisation des propriétés est en fait intuitive et simple pour l'implémentation et le code consommateur.

Prenons l'exemple trivial de

class Rectangle
{
   public Rectangle(double width, double height) => (Width, Height) = (width, height);

   public double Area { get => Width * Height; }

   public double Width { get; private set; }

   public double Height { get; private set; }
}

Maintenant, comme je l'ai dit précédemment, certaines langues ont des concepts différents de ce qu'est une propriété. JavaScript en est un exemple. Les possibilités sont complexes. Il existe une multitude de façons dont une propriété peut être définie par un objet et ce sont des façons très différentes de contrôler la visibilité.

Cependant, dans le cas trivial, comme en C #, JavaScript permet de définir des propriétés de telle sorte qu'elles n'exposent qu'un getter. C'est merveilleux pour contrôler la mutation.

Considérer:

function createRectangle(width, height) {
   return {
     get width() {
       return width;
     },
     get height() {
       return height;
     }
   };
}

Alors, quand faut-il éviter les propriétés? En général, évitez setters qui ont une logique. Même une simple validation est souvent mieux effectuée ailleurs.

Pour en revenir à C #, il est souvent tentant d'écrire du code tel que

public sealed class Person
{
    public Address Address
    {
        get => this.address;
        set
        {
            if (value is null)
            {
                throw new ArgumentException($"{nameof(Address)} must have a value");
            }
            this.address = value;
        }
    }

    private Address address;
}

public sealed class Address { }

L'exception levée est probablement une surprise pour les consommateurs. Après tout, Person.Address est déclaré mutable et null est une valeur parfaitement valide pour une valeur de type Address. Pour aggraver les choses, un ArgumentException est levé. Le consommateur peut ne pas savoir qu'il appelle une fonction et le type de l'exception ajoute donc encore plus au comportement surprenant. Sans doute, un type d'exception différent, tel que InvalidOperationException serait plus approprié. Cependant, cela met en évidence où les setters peuvent devenir laids. Le consommateur pense les choses en termes différents. Il y a des moments où ce type de conception est utile.

Au lieu de cela, cependant, il serait préférable de faire de Address un argument constructeur obligatoire, ou de créer une méthode dédiée pour le définir si nous devons exposer l'accès en écriture et exposer une propriété avec seulement un getter.

J'ajouterai plus à cette réponse.

3
Aluan Haddad

Ajouter des getters et des setters par réflexe (c'est-à-dire, sans réfléchir, à ne pas confondre avec le faire de manière réfléchie selon la question que vous liez) est un signe de problèmes. Tout d'abord, si vous avez des getters et setters passthrough pour les champs, pourquoi ne pas simplement rendre les champs publics? Par exemple:

class Foo
{
    private int _i;
    public int getData() { return _i; }
    public void setData(int i) { _i = i; };
}

Ce que vous avez fait ci-dessus, c'est simplement rendre difficile l'utilisation des données et difficile à lire en utilisant du code. Plutôt que de simplement faire foo.i += 1, Vous forcez l'utilisateur à faire foo.setData(foo.getData() + 1), ce qui est juste obtus. Et pour quel bénéfice? Soi-disant, vous faites des getters et des setters pour l'encapsulation et/ou le contrôle des données, mais si vous avez un getter public ET un setter passthrough, vous n'avez pas besoin de l'un ou de l'autre de toute façon. Certaines personnes soutiennent que vous voudrez peut-être ajouter le contrôle des données plus tard, mais à quelle fréquence cela se fait-il en pratique? Si vous avez des getters et setters publics, vous travaillez déjà à un niveau d'abstraction où vous ne le faites tout simplement pas. Et au cas où vous le feriez, vous pouvez simplement le réparer à ce moment-là.

Les getters et setters publics sont un signe de programmation culte, où vous ne faites que suivre les règles par cœur parce que vous pensez qu'elles améliorent votre code, mais vous n'avez pas réellement pensé si oui ou non. Si votre base de code est remplie de getters et de setters, demandez-vous pourquoi vous ne travaillez tout simplement pas avec des POD, peut-être est-ce simplement une meilleure façon de travailler avec des données pour votre domaine?

La critique à la question d'origine n'est pas que les getters ou setters sont une mauvaise idée, c'est que le simple fait d'ajouter des getters et setters publics pour tous les domaines ne fait rien de bon.

3
Sebastian Zander

Pour comprendre le problème avec les getters et les setters, je pense qu'il est important de prendre du recul et de parler d'abstraction et du concept d'interface. Je ne parle pas du mot-clé de programmation interface comme en Java, je veux dire interface au sens plus général. Par exemple, si vous avez un four à micro-ondes, il possède une sorte d'interface utilisateur qui vous permet de le contrôler. La conception d'interfaces est l'un des aspects les plus fondamentaux de la conception de systèmes. Elle est directement liée à l'évolution de votre système.

Commençons par un exemple négatif. Supposons que je souhaite stocker des informations sur mon inventaire. Je crée donc une classe: Inventory. Tout d'abord, mes exigences disent que je dois savoir combien d'articles différents il y a ou un certain type dans l'inventaire afin que ces informations puissent être utilisées dans d'autres parties du programme. OK codons:

public class Inventory {
    public final String sku;
    public int numberOfItems;
}

Super, maintenant je peux créer des objets d'inventaire qui peuvent être partagés dans le programme. Mais bientôt je rencontre un problème. Lorsque des articles sont vendus ou ajoutés à l'inventaire, je dois mettre à jour ces objets. Il pourrait y avoir beaucoup de personnes différentes qui achètent ou vendent ces choses en même temps. Lorsque deux parties du programme essaient de mettre à jour le numéro en même temps, nous commençons à obtenir des erreurs. Par exemple, disons que nous avons 50 articles et 100 articles ont été introduits dans l'entrepôt, mais en même temps, nous en avons vendu 25. Donc, deux parties lisent la valeur, puis la modifient et la réécrivent. Si le code de vente définit la dernière valeur, nous avons maintenant 25 articles dans l'objet, alors qu'en réalité, nous en avons 125.

Nous ajoutons donc un wrapper et synchronisons:

public class Inventory {
    public final String sku;
    private int numberOfItems;

    public int getNumberOfItems() {
        synchronized (this) {
            return numberOfItems;
        }
    }

    synchronized public void setNumberOfItems(int number) {
        synchronized (this) {
            this.numberOfItems = number;
        }
    }
}

Génial. Maintenant, deux threads ne peuvent pas lire et écrire en même temps. Mais nous n'avons pas vraiment résolu le problème. Les deux parties peuvent toujours lire à partir du getter (une à la fois) puis réécrire les nouvelles valeurs erronées (une à la fois). Nous n'avons pas vraiment réussi.

Voici une meilleure approche:

public class Inventory {
    private final String sku;
    private int numberOfItems;

    public int getSku() {
      return sku;
    }

    public int getNumberOfItems() {
        synchronized (this) {
            return numberOfItems;
        }
    }

    public void addItems(int number) {
        synchronized (this) {
            this.numberOfItems += number;
        }
    }
}

Nous avons maintenant quelque chose qui résout réellement le problème. Il garantit que les modifications de l'inventaire sont effectuées une par une.

Ceci est un exemple très simple et il ne vise pas à montrer comment écrire du bon code en 2017. Le point ici est que pour avoir une conception robuste, vous devez vous concentrer sur la façon dont vos objets/classes définiront leurs contrôles dans d'autres parties. de la demande. La définition de l'état interne n'a que très peu à voir avec cela. Dans l'exemple ci-dessus, disons que l'entreprise se développe et est maintenant distribuée. Notre inventaire sera stocké quelque part sur un serveur distant. Il n'y aura même pas de champ. Afin de conserver l'interface getter/setter, vous devez créer d'autres classes d'assistance qui gèrent vos beans et transfèrent les données de part et d'autre. C'est ainsi que fonctionnent les programmes procéduraux (par exemple COBOL). Les données sont tournées autour et il existe divers programmes qui manipulent ces données. Ces types de conceptions sont très fragiles et coûteux à modifier car vous avez de nombreux morceaux de code disparates qui doivent être modifiés de concert. Chaque partie peut faire des choses qui cassent d'autres parties du système.

Notez que dans la conception améliorée, il y a encore des getters. Les getters sont certainement moins problématiques que les setters. Bien que je n'aime pas personnellement le préfixe get sur les choses, il y a des moments où il est préférable de se conformer aux attentes. Mais, vous pouvez toujours avoir des ennuis avec les getters. Si vous utilisez des objets comme des sacs de données pour la livraison aux parties du code avec logique, c'est essentiellement le même problème.

J'espère que ça aide. Commentaires bienvenus.

0
JimmyJames