Infelizmente muitos dos erros mais comuns são iguais aos ocorridos no Problema 1, por isso, se não leu o texto sobre os erros mais comuns no Problema 1, leia-o!
A destruição das variáveis dinâmicas deveria ser feita sempre que:
Quarto).
Reserva).
Isso evitaria possíveis problemas com cópias indevidas de ponteiros, i.e., criação de "siameses" (este tipo de problema não foi penalizado dado que a matéria correspondente foi dada muito perto da data de entrega da resolução, mas será tido em conta em trabalhos futuros).
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 instância das classes.
De seguida apresenta-se um dos exemplos de quebra desta regra que apareceu com mais frequência nas resoluções do Problema2.
O método
cuja implementação é:
public:
Quarto* devolveQuartoComIdentificador(std::string const& identificador) const;
Apesar de ter um ciclo interno bem estruturado, este método tem vários problemas:
Quarto* GestorDeQuartos::devolveQuartoComIdentificador(std::string const& identificador) const{list<Quarto*>::iterator i = lista_de_quartos.begin();while(i != lista_de_quartos.end() and (*i)->identificador() != identificador)++i;
return *i;}
além de ter os problemas citados acima, indica que a classe, (neste caso a classe
public:
list<Reserva*> devolveReservas() const;
Quarto), não está a
cumprir as suas responsabilidades e está a quebrar o encapsulamento, deixando
que uma outra classe manipule a sua lista e faça com ela operações que podem
deixar a classe num estado inconsistente.
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 as variáveis internas sem verificar quaisquer pré-condições, ou a utilização de funções membro que dão acesso directo às variáveis internas permitindo ao utilizador da classe alterar directamente estas variáveis.
Outro exemplo:
Se um GestorDeQuartos se encarrega de fazer o interface
com o utilizador (e aqui é discutível se a classe está ou não a fazer vários
papeis em simultâneo, o que neste caso não foi penalizado), os métodos que fazem
as várias operações devem ser privados. Os únicos métodos públicos devem ser o
construtor e um método para iniciar a execução mostrando o menu, no qual são
chamados os métodos privados da classe GestorDeQuartos.
A declaração de atributos que servem como variáveis auxiliares dentro dos 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 declaradas e destruídas onde necessário e ter o menor âmbito possível.
Alguns trabalhos apresentam uma estruturação incompreensível das classes,
tendo, por exemplo, listas de ponteiros para Reserva como membro da classe
Reserva, ou ponteiros para Quarto dentro do Quarto.
Aqui há claramente
uma confusão de conceitos: será que uma reserva (física) tem uma lista de
reservas que por sua vez tem uma lista de reservas que por sua vez ... ?
Imaginem um impresso que devia ser preenchido quando é feita uma reserva com
toda a informação da reserva tal como declarada no exemplo anterior. Para
contemplar a estrutura indicada seria necessário um impresso de tamanho
infinito, dado que poderia sempre haver mais reservas dentro de uma reserva.
Um
quarto (físico) não tem outro quarto associado, a não ser que se esteja a tratar
de quartos geminados com comunicação entre ambos ou qualquer coisa desse género.
No caso do Problema 2 não faz sentido que um quarto esteja associado a outro.
Do parágrafo anterior deve ficar claro que os ponteiros (ou listas de ponteiros) podem ser usados para representar relações de composição ou agregação (relações "tem um") ou relações muito mais fracas, de simples associação ("sabe da existência de", se quiserem). Claramente no âmbito deste problema não havia qualquer necessidade de as reservas saberem de outras reservas e os quartos saberem de outros quartos.
a variável
private:list<Reserva*> reservas;int numero_de_reservas;
numero_de_reservas 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 quantas
reservas tem a lista podemos chamar o método size() da lista de
reservas que nos devolve o número de itens que a lista contém.
Mais estranho ainda é uma função como a seguinte:
que, pelas razões acima citadas, é totalmente desnecessária.
int comprimentoDaLista(list<...> const& lista) {int comprimento = 0;for(list<...>::iterator i = lista.begin(); i != lista.end(); ++i)++comprimento;return comprimento;}
exit() para sair de um programa.
break, continue e return no meio de ciclos
para evitar pôr guardas adequadas nos ciclos (que tipicamente começam com
while(true) ou for(;;) ).
if(uma_condição_qualquer == true).
O
"== true" está implícito. Não se escreve código desnecessário a não
ser para melhorar a legibilidade, o que não é o caso.
if(uma_condicao_qualquer)return true;elsereturn false;
não se usam, sendo substituídas com vantagem por:
return uma_condição_qualquer;
bool verifica(); //verifica o quê?Reserva* devolveUmaReserva(list<Reseva*> const& ... ); //qual delas ? em que condições?
A utilização de instruções de asserção é indispensável para que o programador detecte possíveis erros no seu código. Todas as chamadas a este método devem ser feitas garantindo que a pré-condição se verifica, por exemplo:
private:
/**@brief Devolve o quarto cujo identificador é igual ao dado como argumento:@pre Existe um quarto cujo identificador é igual ao dado como argumento.*/
Quarto const* quartoComIdentificador(std::string const& identificador) const;//ou, se necessário (tendo em atenção que é um método privado)://Quarto* quartoComIdentificador(std::string const& identificador) const;...
Quarto const* GestorDeQuartos::quartoComIdentificador(std::string const& identificador) const{assert (existeQuartoComIdentificador(identificador));
list<Quarto*>::const_iterator i = lista_de_quartos.begin();while(i != lista_de_quartos.end() and (*i)->identificador() != identificador)++i;
return *i;}
if(existeQuartocomIdentificador(identificador)) {Quarto const* quarto = devolveQuartoComIdentificador(identificador);//... fazer qualquer coisa com o quarto ...}
Repare que, como a função é privada, todas as chamadas são feitas dentro da classe e por isso é fácil detectar erros de utilização desta função.
A verificação da condição invariante de instância da classe também é uma ferramenta útil para a correcção de erros deste tipo. Ver as Secções 7 e 8 dos erros mais comuns do Problema 1.
.H). Os
módulos que incluem esse
ficheiro não devem ser condenados a ver o espaço de nomes poluído apenas
por que a especificação de utilização é conveniente no ficheiro de
implementação (.C) correspondente ao de interface. Ver Capítulo
8.
As variáveis devem sempre ter o menor âmbito possível, estando declaradas apenas onde é estritamente necessário, por exemplo:
deveria serlist<...>::iterator i = lista.begin();for(; i != lista.end(); ++i) {...}
ou, se a linha inicial for demasiado extensa,
for(list<...>::iterator i = lista.begin(); i != lista.end(); ++i) {...}
ou ainda
for(list<...>::iterator 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.
typedef list<...>::iterator iterador;
for(iterador i = lista.begin();i != lista.end(); ++i ) {...}
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,
é mais legível e adequado (além de ser mais difícil esquecer de acrescentar o progresso) do que
for(list<...>::iterator i = lista.begin();i != lista.end();++i) {...}
Os ciclos que têm de efectuar determinada operação para um determinado item de uma lista, por exemplo, são normalmente mais legíveis quando usam instruções
list<...>::iterator i = lista.begin();while(i != lista.end()) {...++i;}
while para procurar o item em
causa e , após o ciclo, realizam a operação pretendida. Por
exemplo,
é mais legível e adequado do que
list<...>::iterator i = lista.begin();while(i != lista.end() and(*i)->ocupado() and(*i)->reservadoEntre(data_inicio, data_fim))++i;...
//Operação sobre*i.
for(list<...>::iterator i = lista.begin(); i != lista.end(); ++i)
if(not (*i)->ocupado())if (not (*i)->reservadoEntre(data_inicio, data_fim)) {...
//Operação sobre*i.break;
}