Wann sollte eine private Methode den öffentlichen Weg einschlagen, um auf private Daten zuzugreifen?

11

Wann sollte eine private Methode den öffentlichen Weg einschlagen, um auf private Daten zuzugreifen? Wenn ich zum Beispiel diese unveränderliche 'Multiplikator'-Klasse hätte (ein bisschen erfunden, ich weiß):

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;
};

Es gibt zwei Möglichkeiten, die ich implementieren könnte getProduct:

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

oder

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

Weil die Absicht hier ist, den Wert von zu verwenden a, dh zu bekommen a , um getA()zu implementieren, getProduct()scheint mir sauberer. Ich würde es vorziehen, die Verwendung zu vermeiden, awenn ich sie nicht ändern müsste . Ich mache mir Sorgen, dass ich Code nicht oft so geschrieben sehe, meiner Erfahrung nach a * bwäre dies eine häufigere Implementierung als getA() * getB().

Sollten private Methoden jemals den öffentlichen Weg benutzen, wenn sie direkt auf etwas zugreifen können?

0x5f3759df
quelle

Antworten:

7

Es hängt von der tatsächlichen Bedeutung a, bund getProduct.

Der Zweck von Getter besteht darin, die tatsächliche Implementierung ändern zu können, während die Schnittstelle des Objekts gleich bleibt. Wenn beispielsweise eines Tages wird, getAwird return a + 1;die Änderung in einem Getter lokalisiert.

Reale Szenariofälle sind manchmal komplizierter als ein konstantes Hintergrundfeld, das über einen Konstruktor zugewiesen wird, der einem Getter zugeordnet ist. Beispielsweise kann der Wert des Feldes in der Originalversion des Codes berechnet oder aus einer Datenbank geladen werden. In der nächsten Version kann Caching hinzugefügt werden, um die Leistung zu optimieren. Wenn getProductdie berechnete Version weiterhin verwendet wird, profitiert sie nicht vom Caching (oder der Betreuer nimmt dieselbe Änderung zweimal vor).

Wenn es für getProductdie Verwendung aund bdirekt sinnvoll ist, verwenden Sie sie. Verwenden Sie andernfalls Getter, um spätere Wartungsprobleme zu vermeiden.

Beispiel, wo man Getter verwenden würde:

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;
}

Während der Getter derzeit keine Geschäftslogik enthält, wird nicht ausgeschlossen, dass die Logik im Konstruktor auf den Getter migriert wird, um Datenbankarbeiten beim Initialisieren des Objekts zu vermeiden:

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;
}

Später kann Caching hinzugefügt werden (in C # würde man verwenden Lazy<T>, um den Code kurz und einfach zu machen; ich weiß nicht, ob es in C ++ ein Äquivalent gibt):

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;
}

Beide Änderungen konzentrierten sich auf den Getter und das Hintergrundfeld, wobei der verbleibende Code nicht betroffen war. Wenn ich stattdessen ein Feld anstelle eines Getters verwendet hätte getPriceWithRebate, müsste ich auch die Änderungen dort widerspiegeln.

Beispiel, wo man wahrscheinlich private Felder verwenden würde:

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;
}

Der Getter ist unkompliziert: Es handelt sich um eine direkte Darstellung eines konstanten readonlyFeldes (ähnlich dem von C # ), von dem nicht erwartet wird, dass es sich in Zukunft ändert: Es besteht die Möglichkeit, dass der ID-Getter niemals zu einem berechneten Wert wird. Halten Sie es einfach und greifen Sie direkt auf das Feld zu.

Ein weiterer Vorteil ist, dass das getIdmöglicherweise in Zukunft entfernt wird, wenn es den Anschein hat, dass es nicht im Freien verwendet wird (wie im vorherigen Code).

Arseni Mourzenko
quelle
Ich kann Ihnen keine +1 geben, da Ihr Beispiel für die Verwendung privater Felder meiner Meinung nach kein einziges ist, hauptsächlich, weil Sie Folgendes deklariert haben const: Ich gehe davon aus, dass der Compiler einen getIdAufruf trotzdem einbindet und Sie Änderungen in beide Richtungen vornehmen können. (Ansonsten stimme ich Ihren Gründen für die Verwendung von Gettern voll und ganz zu .) Und in Sprachen, die Eigenschaftssyntax bieten, gibt es noch weniger Gründe, die Eigenschaft nicht direkt als das Hintergrundfeld zu verwenden.
Mark Hurd
1

Normalerweise verwenden Sie die Variablen direkt. Sie erwarten, dass Sie alle Mitglieder ändern, wenn Sie die Implementierung einer Klasse ändern. Wenn Sie die Variablen nicht direkt verwenden, wird es einfacher, den von ihnen abhängigen Code korrekt zu isolieren, und das Lesen des Elements wird schwieriger.

Dies ist natürlich anders, wenn die Getter echte Logik implementieren. In diesem Fall hängt es davon ab, ob Sie ihre Logik verwenden müssen oder nicht.

DeadMG
quelle
1

Ich würde sagen, dass die Verwendung der öffentlichen Methoden vorzuziehen wäre, wenn nicht aus einem anderen Grund, als um DRY zu entsprechen .

Ich weiß, dass Sie in Ihrem Fall einfache Sicherungsfelder für Ihre Accessoren haben, aber Sie könnten eine bestimmte Logik haben, z. B. einen Code zum verzögerten Laden, den Sie ausführen müssen, bevor Sie diese Variable zum ersten Mal verwenden. Daher möchten Sie Ihre Accessoren anrufen, anstatt direkt auf Ihre Felder zu verweisen. Auch wenn Sie dies in diesem Fall nicht haben, ist es sinnvoll, sich an eine einzige Konvention zu halten. Auf diese Weise müssen Sie Ihre Logik nur an einer Stelle ändern, wenn Sie sie jemals ändern.

rory.ap
quelle
0

Für eine Klasse gewinnt diese kleine Einfachheit. Ich würde nur a * b verwenden.

Für etwas weitaus Komplizierteres würde ich die Verwendung von getA () * getB () in Betracht ziehen, wenn ich die "minimale" Schnittstelle klar von allen anderen Funktionen in der vollständigen öffentlichen API trennen möchte. Ein hervorragendes Beispiel wäre std :: string in C ++. Es hat 103 Mitgliederfunktionen, aber nur 32 von ihnen benötigen wirklich Zugriff auf private Mitglieder. Wenn Sie eine so komplexe Klasse hätten, könnte das Erzwingen, dass alle "Nicht-Kern" -Funktionen konsistent die "Kern-API" durchlaufen, das Implementieren, Debuggen und Refaktorisieren der Implementierung erheblich vereinfachen.

Ixrec
quelle
1
Wenn Sie eine so komplexe Klasse hatten, sollten Sie gezwungen sein, sie zu reparieren, nicht mit Pflaster.
DeadMG
Einverstanden. Ich hätte wahrscheinlich ein Beispiel mit nur 20-30 Funktionen wählen sollen.
Ixrec
1
"103 Funktionen" ist ein bisschen wie ein roter Hering. Überladene Methoden sollten bei der Komplexität der Schnittstelle einmal gezählt werden.
Avner Shahar-Kashtan
Ich bin völlig anderer Meinung. Unterschiedliche Überladungen können unterschiedliche Semantik und unterschiedliche Schnittstellen haben.
DeadMG
Auch für dieses "kleine" Beispiel getA() * getB()ist es mittel- und langfristig besser.
Mark Hurd