Quando um método privado deve seguir a rota pública para acessar dados privados?


11

Quando um método privado deve seguir a rota pública para acessar dados privados? Por exemplo, se eu tivesse essa classe 'multiplicadora' imutável (um pouco artificial, eu sei):

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

Existem duas maneiras de implementar getProduct:

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

ou

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

Porque a intenção aqui é usar o valor de a, ou seja , obter a , usar getA()para implementar getProduct()parece mais limpo para mim. Eu preferiria evitar o uso, a amenos que tivesse que modificá-lo. Minha preocupação é que muitas vezes não vejo código escrito dessa maneira, na minha experiência a * bseria uma implementação mais comum do que getA() * getB().

Os métodos privados devem sempre usar a maneira pública quando podem acessar algo diretamente?

Respostas:


7

Depende do significado real de a, be getProduct.

O objetivo dos getters é poder alterar a implementação real, mantendo a interface do objeto igual. Por exemplo, se um dia getAse tornar return a + 1;, a alteração será localizada em um getter.

Casos de cenários reais às vezes são mais complicados do que um campo de apoio constante atribuído por meio de um construtor associado a um getter. Por exemplo, o valor do campo pode ser calculado ou carregado em um banco de dados na versão original do código. Na próxima versão, o armazenamento em cache pode ser adicionado para otimizar o desempenho. Se getProductcontinuar usando a versão computada, ela não se beneficiará do cache (ou o mantenedor fará a mesma alteração duas vezes).

Se faz sentido getProductusar ae bdiretamente, use-os. Caso contrário, use getters para evitar problemas de manutenção posteriormente.

Exemplo onde alguém usaria 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;
}

Embora, no momento, o getter não contenha nenhuma lógica de negócios, não é de excluir que a lógica no construtor será migrada para o getter, a fim de evitar o trabalho do banco de dados ao inicializar o 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;
}

Posteriormente, o cache pode ser adicionado (em C #, seria usado Lazy<T>, tornando o código curto e fácil; não sei se existe um equivalente em 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;
}

Ambas as alterações foram focadas no getter e no campo de suporte, o código restante não sendo afetado. Se, em vez disso, eu tivesse usado um campo em vez de um getter getPriceWithRebate, também teria que refletir as alterações lá.

Exemplo em que alguém provavelmente usaria 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;
}

O getter é direto: é uma representação direta de um campo constante (semelhante ao de C # readonly) que não deve mudar no futuro: é provável que o getter de ID nunca se torne um valor calculado. Portanto, mantenha-o simples e acesse o campo diretamente.

Outro benefício é que ele getIdpoderá ser removido no futuro se parecer que não é usado fora (como no trecho de código anterior).


Não posso dar um +1 porque seu exemplo para usar campos particulares não é um IMHO, principalmente porque você declarou const: Suponho que isso significa que o compilador fará uma getIdchamada em linha de qualquer maneira e permite que você faça alterações em qualquer direção. (Caso contrário, concordo plenamente com seus motivos para usar getters.) E nos idiomas que fornecem sintaxe de propriedade, há ainda menos motivos para não usar a propriedade em vez do campo de apoio diretamente.
Mark Hurd

1

Normalmente, você usaria as variáveis ​​diretamente. Você espera alterar todos os membros ao alterar a implementação de uma classe. Não usar as variáveis ​​diretamente simplesmente torna mais difícil isolar corretamente o código que depende delas e torna mais difícil a leitura do membro.

É claro que isso é diferente se os getters implementarem lógica real; nesse caso, depende se você precisa utilizar a lógica deles ou não.


1

Eu diria que o uso de métodos públicos seria preferível, se não por qualquer outro motivo, mas para estar em conformidade com o DRY .

Sei que, no seu caso, você tem campos de apoio simples para seus acessadores, mas pode ter uma certa lógica, por exemplo, código de carregamento lento, que você precisa executar antes da primeira vez que usar essa variável. Portanto, convém chamar seus acessadores em vez de referenciar diretamente seus campos. Mesmo que você não tenha isso nesse caso, faz sentido manter uma única convenção. Dessa forma, se você mudar sua lógica, precisará alterá-la em um só lugar.


0

Para uma classe tão pequena, a simplicidade vence. Eu apenas usaria a * b.

Para algo muito mais complicado, eu consideraria fortemente usar getA () * getB () se quisesse separar claramente a interface "mínima" de todas as outras funções na API pública completa. Um excelente exemplo seria std :: string em C ++. Possui 103 funções de membro, mas apenas 32 delas realmente precisam de acesso a membros privados. Se você tivesse uma classe tão complexa, forçar todas as funções "não essenciais" a passar consistentemente pela "API principal" poderia facilitar muito a implementação de teste, depuração e refatoração.


1
Se você teve uma classe tão complexa, deve ser forçado a corrigi-la, não a curar.
precisa saber é

Acordado. Eu provavelmente deveria ter escolhido um exemplo com apenas 20 a 30 funções.
Ixrec

1
"103 funções" é um pouco de um arenque vermelho. Os métodos sobrecarregados devem ser contados uma vez, em termos de complexidade da interface.
Avner Shahar-Kashtan

Eu discordo completamente. Sobrecargas diferentes podem ter semântica e interfaces diferentes.
precisa saber é

Mesmo para este exemplo "pequeno", getA() * getB()é melhor a médio e longo prazo.
Mark Hurd
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.