PROJETOS E DECLARAÇÕES
116 C++ E FICAZ : 55 MANEIRAS DE APRIMORAR SEUS PROGRAMAS E PROJETOS
dos por meio de uma função, pode substituir o membro de dados poste- riormente com uma computação, e ninguém que está usando sua classe ficará ciente disso.
Por exemplo, suponhamos que você esteja escrevendo um aplicativo em que um equipamento automatizado está monitorando a velocidade dos carros que estão passando. À medida que os carros passam, sua velocidade é com- putada e o valor é adicionado em uma coleção de todos os dados de veloci- dade coletados até agora.
class SpeedDataCollection { ...
public:
void addValue(int speed); // adiciona um novo valor de dados double averageSoFar() const; // retorna a velocidade média ...
};
Agora, considere a implementação da função membro averageSoFar (mé- dia até agora). Uma maneira de implementá-la é ter um membro de dados na classe que é uma média corrente de todos os dados de velocidades cole- tados até o momento. Em qualquer momento que averageSoFar for cha- mada, ela só retorna o valor desse membro de dados. Uma abordagem dife- rente é fazer averageSoFar computar seu valor cada vez que for chamada, algo que você pode fazer examinando todos os valores de dados na coleção. A primeira abordagem (manter uma média corrente) aumenta cada obje- to SpeedDataCollection (coleção de dados de velocidade), porque você precisa alocar espaço para os membros de dados que mantêm a média corrente, o total acumulado e o número de pontos de dados. Entretanto, averageSoFar pode ser implementada de maneira muito eficiente: é ape- nas uma função internalizada (veja o Item 30) que retorna o valor da mé- dia corrente. Em contrapartida, computar a média sempre que for requi- sitada faz averageSoFar ser executada mais lentamente, mas cada objeto SpeedDataCollection será menor.
Quem poderá dizer qual é a melhor? Em uma máquina em que a memória é restrita (por exemplo, um dispositivo embarcado ao lado da via), e em aplica- ções nas quais as médias não se fazem necessárias com frequência, computar a média todas as vezes é, provavelmente, uma solução melhor. Em uma apli- cação em que as médias costumam ser necessárias, a velocidade é a essência, e a memória não é um problema, manter uma média corrente geralmente será preferível. O ponto importante é que, ao acessar a média por uma função membro (encapsulando-a), você pode trocar entre essas implementações di- ferentes (bem como por qualquer outra que você imaginar), e os clientes, no pior dos casos, só terão que ser compilados novamente. (Você pode eliminar até mesmo essa inconveniência seguindo as técnicas descritas no Item 31.) Ocultar os membros de dados atrás de interfaces funcionais pode ofere- cer todos os tipos de flexibilidade de implementação. Por exemplo, desse
modo fica mais fácil notificar outros objetos quando os membros de dados são lidos ou gravados, verificar invariantes de classe e pré e pós-condições de funções, realizar sincronização em ambientes com múltiplas linhas de execução, etc. Os programadores que chegam a C++ vindos de linguagens como Delphi e C# reconhecerão essas capacidades como o equivalente às “propriedades” nessas outras linguagens, embora seja preciso digitar um conjunto extra de parênteses.
A questão sobre o encapsulamento é mais importante do que pode pare- cer inicialmente. Se você ocultar os membros de dados de seus clientes (ou seja, encapsulá-los), pode garantir que as invariantes de classe sejam sempre mantidas, porque apenas as funções membro podem afetá-las. Além disso, você se reserva o direito de modificar suas decisões de implementação pos- teriormente. Se você não ocultar essas decisões, logo descobrirá que, mesmo que seja o proprietário do código-fonte de uma classe, sua habilidade de mo- dificar algo público é extremamente restrita, porque muito código cliente será quebrado. Público significa não encapsulado e, na prática, significa imutável, em especial para classes que são amplamente usadas. Ainda assim, as clas- ses amplamente usadas têm uma imensa necessidade de encapsulamento, pois são as que mais podem se beneficiar da habilidade de substituir uma implementação por outra melhor.
O argumento contra os membros de dados protegidos é similar. Na verdade, é idêntico, apesar de não parecer à primeira vista. O raciocínio sobre a consis- tência sintática e sobre o controle de acesso de granularidade fina é claramente tão aplicável aos dados protegidos quanto aos dados públicos. Mas e o encap- sulamento? Os membros de dados protegidos não são mais encapsulados do que os públicos? Na prática, a resposta é, surpreendentemente, não.
O Item 23 explica que o encapsulamento de algo é inversamente proporcio- nal à quantidade de código que pode ser quebrado se algo for modificado. Assim, o grau de encapsulamento de um membro de dados é inversamente proporcional à quantidade de código que pode ser quebrado se os mem- bros de dados mudarem, por exemplo, se um membro de dados for re- movido da classe (possivelmente em favor de uma computação, como em averageSoFar, acima).
Suponhamos que tivéssemos um membro de dados público e o eliminás- semos. Quanto do código seria quebrado? Todo o código cliente, que é ge- ralmente uma quantidade desconhecidamente grande. Então, os membros de dados públicos são completamente não encapsulados. Mas suponhamos que tivéssemos um membro de dados protegido e o eliminássemos. Quanto código seria quebrado agora? Todas as classes derivadas que o usam, o que é, mais uma vez, uma quantidade desconhecidamente grande. Os membros de dados protegidos são tão não encapsulados quanto os públicos, porque, em ambos os casos, se os membros de dados são modificados, uma quanti- dade desconhecidamente grande de código cliente estará quebrada. Isso não faz sentido, mas, como dirão os implementadores experientes de bibliotecas, é verdade mesmo assim. Uma vez que você declarou um membro de dados