Este documento apresenta os erros mais comuns cometidos nas resoluções do Problema. Os erros são apontados através de troços de código extraídos dos programas realizados pelos alunos. Esses troços não são identificados. Pretende-se ilustrar os erros cometidos para que possam ser corrigidos em trabalhos posteriores.
Lote
, Existências
e GestorDeExistências
para
representar respectivamente os conceitos de lote, existências e gestor de existências. A classe
Lote
teria como responsabilidade permitir a consulta do nome do produto, quantidade
e valor unitário de um produto de um lote e retirar uma dada quantidade de produto de um lote; a classe Existências
deveria ter como
responsabilidades as várias operações que se podem realizar
sobre as existências nos armazéns de uma empresa e classe GestorDeExistências
teria como responsabilidade efectuar a interacção entre o utilizador e as existências,
i.e., auxiliar a gerir as existências.No caso seguinte,
void Existências::valorExistências() const
...
{
cout << endl << "O valor das existências é " << valor_existências << ".\n\n";
}
é a classe Existências
que está a realizar o trabalho de interagir com o utilizador que é responsabilidade da classe
GestorDeExistências
. Ou seja, deveria ser
double Existências::valorDasExistências() const
{
assert(cumpreInvariante());
double valor_das_existências = 0.0;
for(list<Lote>::const_iterator i = lotes.begin(); i != lotes.end(); ++i)
valor_das_existências += lotes[i].quantidade() * lotes[i].preçoUnitário();
return valor_das_existências;
}...
void GestorDeExistências::mostraValorDasExistências() const
{
cout << "O valor das existências é " << existências.valorDasExistências() << '.'
<< endl;
}
Sendo este o exemplo típico deste tipo de erro, o seguinte troço de código ilustra o mesmo noutra perspectiva:
double const GestorDeExistencias::valorDasExistencias() const
{
vector<Lote> lotes = inventario.listagemDeProdutos();
double valor_das_existencias = 0;
for (vector<Lote>::size_type i = 0; i != lotes.size(); ++i)
valor_das_existencias += lotes[i].quantidade() * lotes[i].valor();
return valor_das_existencias;
}
Repare-se que se invoca através de inventario
(do tipo Existencias
)
uma operação que devolve um vector
de Lote
,
usado pelo GestorDeExistencias
para calcular o valor das
existências. Como já indicado, deveria ser a classe
Existências
a responsável por calcular o valor das existências.
O principal objectivo da aplicação do princípio do encapsulamento às classes é permitir a alteração dos seus atributos apenas através de métodos da classe, que garantem a sua coerência impedindo que fiquem em estados inválidos, i.e., que garantem o cumprimento da condição invariante de classe. Esta é uma das regras fundamentais da programação orientada para objectos. Nada melhor para verificar que isso é cumprido do que usar asserções.
O ideal é usar asserções extensivamente, i.e., para verificar as pré-condições e condições objectivo de todas as rotinas e métodos e para verificar o cumprimento das condições invariantes de classe.
Um dos sinais típicos de que uma classe não está a cumprir as suas responsabilidades e a manter o encapsulamento, é uma quantidade excessiva de métodos públicos que permitem fazer quase tudo com os seus atributos sem verificar quaisquer pré-condições ou condições invariantes de classe, ou a utilização de operações que dão acesso directo aos atributos, permitindo ao utilizador da classe alterá-los sem qualquer problema.
A classe GestorDeExistências
é responsável pela interface com
o utilizador. As suas únicas operações públicas devem ser o construtor e uma
operação para proceder à gestão, em cujo método são invocadas operações
auxiliares da classe
GestorDeExistências
que existem apenas por uma questão de
modularização do código, e não por serem úteis ao programador consumidor da
classe, e que como tal devem ser privadas.
A operação que indica se a condição invariante de uma classe é verdadeira deve ser privado, naturalmente, pois diz respeito apenas à sua implementação.
As operações auxiliares, que não fazem parte da interface da classe, devem ser privadas.
Na seguinte definição
private:
std::list<Lote> lotes;
int número_de_lotes;
a variável número_de_lotes
está a mais. Não só não é necessária
(e ocupa espaço) como pode dar origem a erros (como por exemplo fazer um
push_back()
na lista e esquecer-se de incrementar a variável, o que torna
imediatamente os atributos inconsistentes). Sempre que quisermos saber quantos
lotes tem a lista podemos chamar a operação std::list<Lote>::size()
,
que nos devolve o número de itens que a lista contém.
A definição de atributos que servem como variáveis auxiliares para os métodos da classe também não é boa ideia. Uma classe representa um conceito. Apenas os atributos relevantes para representar esse conceito devem ser membros da classe. Variáveis auxiliares devem ser definidas e destruídas onde necessário e ter o menor âmbito possível.
A classe Existências
representa as existências nos armazéns de
uma empresa. Logo, deve guardar uma lista dos lotes que se encontram em armazém.
Ou seja, tem um atributo que é um contentor (por exemplo, uma lista) de lotes.
A classe Existências
não é um contentor (lista) de lotes,
contém um contentor (lista) de lotes. Conter e ser são conceitos
muito diferentes.
Quando uma classe tem entre os seus atributos um contentor,
#include <list>
class Existências {
public:
...
private:
std::list<Lote> lotes;
};
Existencias::Existencias()
{
std::list<Lote> existências;
assert(cumpreInvariante());
}
este não necessita de ser inicializado explicitamente na lista de
inicializadores do construtor. Com
efeito, a lista é automaticamente construída vazia (pelo seu construtor por
omissão). Neste caso, em que o único atributo é uma lista de lotes, nem o
construtor era necessário, pois o próprio C++ fornece um construtor por omissão.
No código acima a instrução assinalada como errada não passa de uma
tentativa ingénua de inicializar a lista das existências: na realidade o que a
instrução faz é construir uma variável local existências
que
é destruída mal termina a execução do construtor.
.H
) devem-se incluir todos os
ficheiros de interface correspondentes a ferramentas (classes, modelos,
rotinas, etc.) usados nesse ficheiro de interface (alternativamente,
para reduzir as dependências entre ficheiros no projecto, podem-se declarar
explicitamente as classes e rotinas utilizados). Não se devem retirar
inclusões (ou declarações) de um ficheiro de interface apenas porque se
sabe admite que essas inclusões (ou declarações) são feitas noutros
ficheiros de interface que se incluem antes do ficheiro de interface em causa. Também não se devem colocar inclusões
nos ficheiros de interface só porque elas serão necessárias nos ficheiros que
se calcula virem a incluir o ficheiro de interface em causa. Exemplo:A.H
: class A {
};
B.H
: #include "A.H"
class B {
A a;
};
C.H
: #include "B.H"
#include "A.H" //
mesmo sabendo que B.H
inclui já
A.H
.
class C {
B b;
A a;
};
D.H
: #include "B.H"
#include "C.H" //
má ideia!
B função();
D.C
: #include D.H
#include "C.H" //
aqui sim!
static C outraFunção()
{
... }
Porquê? Porque se em B
se deixar de usar directamente A
(e portanto eliminar a inclusão de A.H
), tudo continua a funcionar
na perfeição. Porque os consumidores futuros de D.H
podem não
necessitar de C.H
, e por isso incluí-lo em D.H
só
serve para aumentar as interdependências entre módulo físicos sem qualquer ganho
prático.
Não se devem usar directivas de utilização (using namespace
...)
nos ficheiros
de interface (.H
). Os módulos que incluem esse ficheiro não devem
ser condenados a ver o seu espaço de nomes poluído apenas por que a directiva de
utilização é conveniente no ficheiro de implementação (.C
)
correspondente ao de interface. Ver Capítulo
9.
Rotinas como int inteiroPedido()
, void limpaCanal()
ou std::string versãoMinúsculaDe(std::string const& cadeia)
são
claramente rotinas utilitárias que não devem ser membro de qualquer classe.
A definição da variável de controlo do ciclo fora do corpo do ciclo para evitar ter uma linha grande demais é uma má solução para o problema da legibilidade.
As variáveis devem sempre ter o menor âmbito possível, estando declaradas apenas onde é estritamente necessário, por exemplo:
list<
...>::iterator i = lista.begin();
for(; i != lista.end(); ++i) {
...
}
deveria ser
for(list<
...>::iterator i = lista.begin(); i != lista.end(); ++i) {
...
}
ou, se a linha inicial for demasiado extensa,
for(list<
...>::iterator i = lista.begin();
i != lista.end();
++i) {
...
}
ou ainda
typedef list<
...>::iterator Iterador;
for(Iterador i = lista.begin(); i != lista.end(); ++i ) {
...
}
sem perda de legibilidade e sem estender o âmbito do iterador mais do que o estritamente necessário.
Os ciclos que devem iterar um número fixo de vezes, ou do principio ao fim de
uma lista, tipicamente escrevem-se de um modo mais legível e adequado usando
instruções for
, por exemplo,
for(list<
...>::iterator i = lista.begin();
i != lista.end();
++i) {
...
}
é mais legível e adequado (além de ser mais difícil esquecer de acrescentar o progresso) do que
list<
...>::iterator i = lista.begin();
while(i != lista.end()) {
...
++i;
}