Quando un metodo privato dovrebbe prendere la strada pubblica per accedere ai dati privati?


11

Quando un metodo privato dovrebbe prendere la strada pubblica per accedere ai dati privati? Ad esempio, se avessi questa immutabile classe 'moltiplicatore' (un po 'inventata, lo so):

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

Esistono due modi per implementare getProduct:

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

o

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

Perché qui l'intenzione è quella di usare il valore di a, ovvero ottenere a , usando getA()per implementare getProduct()mi sembra più pulito. Preferirei evitare di usarlo a ameno che non dovessi modificarlo. La mia preoccupazione è che non vedo spesso il codice scritto in questo modo, nella mia esperienza a * bsarebbe un'implementazione più comune di getA() * getB().

I metodi privati ​​dovrebbero mai usare il modo pubblico quando possono accedere a qualcosa direttamente?

Risposte:


7

Dipende del significato reale di a, be getProduct.

Lo scopo di Getter è di essere in grado di cambiare l'implementazione effettiva mantenendo allo stesso tempo l'interfaccia dell'oggetto. Ad esempio, se un giorno getAdiventa return a + 1;, la modifica è localizzata in un getter.

I casi di scenari reali sono talvolta più complicati di un campo di supporto costante assegnato tramite un costruttore associato a un getter. Ad esempio, il valore del campo può essere calcolato o caricato da un database nella versione originale del codice. Nella versione successiva, è possibile aggiungere la memorizzazione nella cache per ottimizzare le prestazioni. Se getProductcontinua a utilizzare la versione calcolata, non trarrà vantaggio dalla memorizzazione nella cache (o il manutentore farà la stessa modifica due volte).

Se ha perfettamente senso per getProductl'uso ae bdirettamente, usali. In caso contrario, utilizzare getter per prevenire problemi di manutenzione in un secondo momento.

Esempio in cui si utilizzerebbero i getter:

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

Mentre per il momento, il getter non contiene alcuna logica aziendale, non è escluso che la logica nel costruttore verrà migrata al getter al fine di evitare di fare il lavoro di database durante l'inizializzazione dell'oggetto:

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

Successivamente, è possibile aggiungere la memorizzazione nella cache (in C #, si potrebbe usare Lazy<T>, rendendo il codice breve e semplice; non so se esiste un equivalente in 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;
}

Entrambe le modifiche erano focalizzate sul getter e sul campo di supporto, il codice rimanente non era interessato. Se, invece, avessi usato un campo anziché un getter in getPriceWithRebate, avrei dovuto riflettere anche i cambiamenti lì.

Esempio in cui si potrebbero probabilmente usare campi privati:

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

Il getter è semplice: è una rappresentazione diretta di un campo costante (simile al C # readonly) che non dovrebbe cambiare in futuro: è probabile che ID getter non diventerà mai un valore calcolato. Quindi mantienilo semplice e accedi direttamente al campo.

Un altro vantaggio è che getIdpotrebbe essere rimosso in futuro se sembra che non sia usato all'esterno (come nel precedente pezzo di codice).


Non posso darti un +1 perché il tuo esempio di utilizzo dei campi privati ​​non è un IMHO, principalmente perché hai dichiarato const: Suppongo che ciò significhi che il compilatore inserirà getIdcomunque una chiamata e ti consentirà di apportare modifiche in entrambe le direzioni. (In caso contrario, sono pienamente d'accordo con le tue ragioni per usare i getter.) E nelle lingue che forniscono la sintassi delle proprietà, ci sono anche meno motivi per non usare direttamente la proprietà piuttosto che il campo di supporto.
Mark Hurd,

1

In genere, si utilizzano direttamente le variabili. Ti aspetti di cambiare tutti i membri quando cambi l'implementazione di una classe. Non utilizzare direttamente le variabili rende semplicemente più difficile isolare correttamente il codice che dipende da esse e rende più difficile leggere il membro.

Ciò è ovviamente diverso se i getter implementano una logica reale, in tal caso dipende dal fatto che sia necessario utilizzare la loro logica o meno.


1

Direi che sarebbe preferibile utilizzare i metodi pubblici, se non per qualsiasi altro motivo, ma per conformarsi a DRY .

So che nel tuo caso, hai dei semplici campi di supporto per i tuoi accessori, ma potresti avere una certa logica, ad esempio un codice di caricamento lento, che devi eseguire prima di usare quella variabile per la prima volta. Pertanto, si desidera chiamare i propri accessori anziché fare direttamente riferimento ai campi. Anche se non hai questo in questo caso, ha senso attenersi a un'unica convenzione. In questo modo, se mai dovessi apportare una modifica alla tua logica, dovrai solo cambiarla in un unico posto.


0

Per una classe questa piccola semplicità vince. Vorrei solo usare a * b.

Per qualcosa di molto più complicato, prenderei fortemente in considerazione l'utilizzo di getA () * getB () se volessi separare chiaramente l'interfaccia "minima" da tutte le altre funzioni nell'API pubblica completa. Un esempio eccellente sarebbe std :: string in C ++. Ha 103 funzioni membro, ma solo 32 di esse hanno davvero bisogno di accedere a membri privati. Se avessi una classe così complessa, forzare tutte le funzioni "non core" a passare costantemente attraverso le "API core" potrebbe rendere l'implementazione molto più semplice da testare, eseguire il debug e refactor.


1
Se avevi una classe così complessa, dovresti essere costretto a ripararla, non a un aiuto di banda.
DeadMG

Concordato. Probabilmente avrei dovuto scegliere un esempio con solo 20-30 funzioni.
Ixrec,

1
"103 funzioni" è un po 'un'aringa rossa. I metodi sovraccaricati devono essere conteggiati una volta, in termini di complessità dell'interfaccia.
Avner Shahar-Kashtan,

Non sono completamente d'accordo. I sovraccarichi diversi possono avere semantica e interfacce diverse.
DeadMG

Anche per questo "piccolo" esempio, getA() * getB()è meglio a medio e lungo termine.
Mark Hurd,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.