Quand une méthode privée doit-elle emprunter la voie publique pour accéder aux données privées?


11

Quand une méthode privée doit-elle emprunter la voie publique pour accéder aux données privées? Par exemple, si j'avais cette classe de multiplicateur immuable (un peu artificielle, je sais):

class Multiplier {
public:
    Multiplier(int a, int b) : a(a), b(b) { }
    int getA() const { return a; }
    int getB() const { return b; }
    int getProduct() const { /* ??? */ }
private:
    int a, b;
};

Il y a deux façons de mettre en œuvre getProduct:

    int getProduct() const { return a * b; }

ou

    int getProduct() const { return getA() * getB(); }

Parce que l'intention ici est d'utiliser la valeur de a, c'est-à-dire d' obtenir a , d'utiliser getA()pour implémenter getProduct()me semble plus propre. Je préférerais éviter d'utiliser asauf si je devais le modifier. Mon souci est que je ne vois pas souvent du code écrit de cette façon, selon mon expérience, ce a * bserait une implémentation plus courante que getA() * getB().

Les méthodes privées devraient-elles jamais utiliser la voie publique lorsqu'elles peuvent accéder directement à quelque chose?

Réponses:


7

Cela dépend de la signification réelle de a, bet getProduct.

Le but des getters est de pouvoir changer l'implémentation réelle tout en gardant l'interface de l'objet la même. Par exemple, si un jour, getAdevient return a + 1;, le changement est localisé sur un getter.

Les cas de scénarios réels sont parfois plus compliqués qu'un champ de support constant attribué par un constructeur associé à un getter. Par exemple, la valeur du champ peut être calculée ou chargée à partir d'une base de données dans la version d'origine du code. Dans la prochaine version, la mise en cache peut être ajoutée pour optimiser les performances. Si getProductcontinue d'utiliser la version calculée, il ne bénéficiera pas de la mise en cache (ou le responsable effectuera la même modification deux fois).

S'il est parfaitement logique getProductd'utiliser aet bdirectement, utilisez-les. Sinon, utilisez des getters pour éviter des problèmes de maintenance ultérieurement.

Exemple où l'on utiliserait des getters:

class Product {
public:
    Product(ProductId id) : {
        price = Money.fromCents(
            data.findProductById(id).price,
            environment.currentCurrency
        )
    }

    Money getPrice() {
        return price;
    }

    Money getPriceWithRebate() {
        return getPrice().applyRebate(rebate); // ← Using a getter instead of a field.
    }
private:
    Money price;
}

Alors que pour le moment, le getter ne contient aucune logique métier, il n'est pas exclu que la logique du constructeur soit migrée vers le getter afin d'éviter de faire un travail de base de données lors de l'initialisation de l'objet:

class Product {
public:
    Product(ProductId id) : id(id) { }

    Money getPrice() {
        return Money.fromCents(
            data.findProductById(id).price,
            environment.currentCurrency
        )
    }

    Money getPriceWithRebate() {
        return getPrice().applyRebate(rebate);
    }
private:
    const ProductId id;
}

Plus tard, la mise en cache peut être ajoutée (en C #, on utiliserait Lazy<T>, ce qui rend le code court et facile; je ne sais pas s'il y a un équivalent en C ++):

class Product {
public:
    Product(ProductId id) : id(id) { }

    Money getPrice() {
        if (priceCache == NULL) {
            priceCache = Money.fromCents(
                data.findProductById(id).price,
                environment.currentCurrency
            )

        return priceCache;
    }

    Money getPriceWithRebate() {
        return getPrice().applyRebate(rebate);
    }
private:
    const ProductId id;
    Money priceCache;
}

Les deux modifications étaient axées sur le getter et le champ de support, le code restant n'étant pas affecté. Si, à la place, j'avais utilisé un champ au lieu d'un getter getPriceWithRebate, je devrais également y refléter les changements.

Exemple où l'on utiliserait probablement des champs privés:

class Product {
public:
    Product(ProductId id) : id(id) { }
    ProductId getId() const { return id; }
    Money getPrice() {
        return Money.fromCents(
            data.findProductById(id).price, // ← Accessing `id` directly.
            environment.currentCurrency
        )
    }
private:
    const ProductId id;
}

Le getter est simple: il s'agit d'une représentation directe d'un readonlychamp constant (similaire à C # ) qui ne devrait pas changer à l'avenir: il y a de fortes chances que l'ID getter ne devienne jamais une valeur calculée. Alors restez simple et accédez directement au terrain.

Un autre avantage est que le getIdfichier pourrait être supprimé à l'avenir s'il apparaît qu'il n'est pas utilisé à l'extérieur (comme dans le code précédent).


Je ne peux pas vous donner un +1 parce que votre exemple d'utilisation de champs privés n'est pas un IMHO, principalement parce que vous avez déclaré le const: je suppose que cela signifie que le compilateur insérera un getIdappel de toute façon et cela vous permet de faire des changements dans les deux sens. (Sinon, je suis entièrement d'accord avec vos raisons d' utiliser des getters.) Et dans les langues qui fournissent la syntaxe des propriétés, il y a encore moins de raisons de ne pas utiliser la propriété plutôt que le champ de support directement.
Mark Hurd

1

En règle générale, vous utiliseriez directement les variables. Vous vous attendez à changer tous les membres lorsque vous modifiez l'implémentation d'une classe. Ne pas utiliser directement les variables rend simplement plus difficile l'isolement correct du code qui en dépend et rend la lecture du membre plus difficile.

Ceci est bien sûr différent si les getters implémentent une vraie logique, dans ce cas cela dépend si vous devez utiliser leur logique ou non.


1

Je dirais que l'utilisation des méthodes publiques serait préférable, sinon pour toute autre raison, mais pour se conformer à DRY .

Je sais que dans votre cas, vous avez des champs de support simples pour vos accesseurs, mais vous pouvez avoir une certaine logique, par exemple du code de chargement paresseux, que vous devez exécuter avant la première fois que vous utilisez cette variable. Ainsi, vous voudriez appeler vos accesseurs au lieu de référencer directement vos champs. Même si vous n'avez pas cela dans ce cas, il est logique de s'en tenir à une seule convention. De cette façon, si jamais vous changez votre logique, vous n'avez qu'à la changer en un seul endroit.


0

Pour une classe, cette petite simplicité gagne. Je voudrais simplement utiliser un * b.

Pour quelque chose de beaucoup plus compliqué, j'envisagerais fortement d'utiliser getA () * getB () si je voulais séparer clairement l'interface "minimale" de toutes les autres fonctions de l'API publique complète. Un excellent exemple serait std :: string en C ++. Il a 103 fonctions membres, mais seulement 32 d'entre eux ont vraiment besoin d' accéder à des membres privés. Si vous aviez une classe aussi complexe, forcer toutes les fonctions "non essentielles" à passer systématiquement par "l'API principale" pourrait rendre l'implémentation beaucoup plus facile à tester, déboguer et refactoriser.


1
Si vous aviez une classe aussi complexe, vous devriez être obligé de la réparer, pas de la panser.
DeadMG

D'accord. J'aurais probablement dû choisir un exemple avec seulement 20-30 fonctions.
Ixrec

1
"103 fonctions" est un peu un hareng rouge. Les méthodes surchargées doivent être comptées une fois, en termes de complexité d'interface.
Avner Shahar-Kashtan

Je suis totalement en désaccord. Différentes surcharges peuvent avoir différentes sémantiques et différentes interfaces.
DeadMG

Même pour ce "petit" exemple, getA() * getB()c'est mieux à moyen et long terme.
Mark Hurd du
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.