¿Cuándo debe un método privado tomar la ruta pública para acceder a datos privados?

11

¿Cuándo debe un método privado tomar la ruta pública para acceder a datos privados? Por ejemplo, si tuviera esta clase inmutable de 'multiplicador' (un poco artificial, lo sé):

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

Hay dos formas en que podría implementar getProduct:

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

o

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

Porque la intención aquí es usar el valor de a, es decir , obtener a , usar getA()para implementar me getProduct()parece más limpio. Preferiría evitar usarlo a amenos que tuviera que modificarlo. Mi preocupación es que a menudo no veo el código escrito de esta manera, en mi experiencia a * bsería una implementación más común que getA() * getB().

¿Deberían los métodos privados usar la vía pública cuando pueden acceder a algo directamente?

0x5f3759df
fuente

Respuestas:

7

Depende del significado real de a, by getProduct.

El propósito de los captadores es poder cambiar la implementación real manteniendo la interfaz del objeto igual. Por ejemplo, si un día se getAconvierte return a + 1;, el cambio se localiza en un captador.

Los casos de escenarios reales a veces son más complicados que un campo de respaldo constante asignado a través de un constructor asociado con un getter. Por ejemplo, el valor del campo puede calcularse o cargarse desde una base de datos en la versión original del código. En la próxima versión, se puede agregar el almacenamiento en caché para optimizar el rendimiento. Si getProductcontinúa utilizando la versión calculada, no se beneficiará del almacenamiento en caché (o el mantenedor hará el mismo cambio dos veces).

Si tiene sentido getProductusarlo ay usarlo bdirectamente, úsalos. De lo contrario, use getters para evitar problemas de mantenimiento más adelante.

Ejemplo donde uno usaría 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;
}

Si bien por el momento, el captador no contiene ninguna lógica de negocios, no se excluye que la lógica en el constructor se migre al captador para evitar hacer el trabajo de la base de datos al inicializar el objeto:

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

Más tarde, se puede agregar el almacenamiento en caché (en C #, uno usaría Lazy<T>, haciendo que el código sea corto y fácil; no sé si hay un equivalente 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;
}

Ambos cambios se centraron en el captador y el campo de respaldo, y el código restante no se vio afectado. Si, en cambio, hubiera usado un campo en lugar de un captador getPriceWithRebate, también tendría que reflejar los cambios allí.

Ejemplo donde probablemente se usarían campos privados:

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

El captador es sencillo: es una representación directa de un campo constante (similar al de C # readonly) que no se espera que cambie en el futuro: lo más probable es que el captador de ID nunca se convierta en un valor calculado. Así que manténgalo simple y acceda al campo directamente.

Otro beneficio es que getIdpodría eliminarse en el futuro si parece que no se usa en el exterior (como en el fragmento de código anterior).

Arseni Mourzenko
fuente
No puedo darle un +1 porque su ejemplo para usar campos privados no es un IMHO, principalmente porque ha declarado const: Supongo que eso significa que el compilador en línea getIdllamará de todos modos y le permite hacer cambios en cualquier dirección. (De lo contrario, estoy totalmente de acuerdo con sus razones para usar getters). Y en los lenguajes que proporcionan sintaxis de propiedad, hay incluso menos razones para no usar la propiedad en lugar del campo de respaldo directamente.
Mark Hurd
1

Por lo general, usaría las variables directamente. Espera cambiar todos los miembros al cambiar la implementación de una clase. No usar las variables directamente simplemente hace que sea más difícil aislar correctamente el código que depende de ellas y hace que sea más difícil leer el miembro.

Por supuesto, esto es diferente si los captadores implementan una lógica real, en ese caso depende de si necesita utilizar su lógica o no.

DeadMG
fuente
1

Diría que sería preferible utilizar los métodos públicos, si no por cualquier otro motivo, sino para cumplir con DRY .

Sé que en su caso, tiene campos de respaldo simples para sus accesores, pero podría tener cierta lógica, por ejemplo, código de carga diferida, que debe ejecutar antes de la primera vez que usa esa variable. Por lo tanto, querrá llamar a sus accesores en lugar de hacer referencia directa a sus campos. Aunque no tenga esto en este caso, tiene sentido apegarse a una sola convención. De esa manera, si alguna vez realiza un cambio en su lógica, solo tiene que cambiarlo en un solo lugar.

rory.ap
fuente
0

Para una clase tan pequeña, la simplicidad gana. Solo usaría a * b.

Para algo mucho más complicado, consideraría usar getA () * getB () si quisiera separar claramente la interfaz "mínima" de todas las otras funciones en la API pública completa. Un excelente ejemplo sería std :: string en C ++. Tiene 103 funciones de miembro, pero solo 32 de ellas realmente necesitan acceso a miembros privados. Si tuviera una clase tan compleja, forzar a todas las funciones "no centrales" a pasar constantemente por la "API principal" podría hacer que la implementación sea mucho más fácil de probar, depurar y refactorizar.

Ixrec
fuente
1
Si tuvieras una clase así de compleja, deberías estar obligado a arreglarla, no a la curita.
DeadMG
Convenido. Probablemente debería haber elegido un ejemplo con solo 20-30 funciones.
Ixrec
1
"103 funciones" es una especie de arenque rojo. Los métodos sobrecargados deben contarse una vez, en términos de complejidad de la interfaz.
Avner Shahar-Kashtan
Estoy en completo desacuerdo. Diferentes sobrecargas pueden tener semánticas diferentes e interfaces diferentes.
DeadMG
Incluso para este "pequeño" ejemplo, getA() * getB()es mejor a medio y largo plazo.
Mark Hurd