Problema 2

Erros Mais Frequentes

No texto que se segue são apresentados os erros mais comuns encontrados no Problema 2 e algumas regras que um bom programador deve cumprir.
 Como em tudo, há ocasiões em que o melhor é fugir às regras, mas só um programador experimentado as sabe identificar.  No caso presente (do Problema 2) havia soluções adequadas para o problema sem quebrar qualquer uma destas regras e portanto o seu incumprimento não nos pareceu justificável.  Com o tempo e a experiência tornar-se-á mais óbvio para todos os alunos desta cadeira quais são os casos excepcionais que justificam a quebra destas regras.  Para já, como em todos os tipos de aprendizagem, há que aprender e utilizar as regras básicas até a experiência vos começar a indicar claramente onde e porquê se encontram circunstâncias fora do comum que obrigam a soluções de excepção.

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!

1  Construção e destruição de variáveis dinâmicas

Uma das políticas mais simples e eficazes de construção e destruição de variáveis dinâmicas "quem constrói, destrói".  Esta política era facilmente aplicável a este trabalho e não se justifica a opção por políticas mais complicadas.

A destruição das variáveis dinâmicas deveria ser feita sempre que:

Todas as classes que possuem variáveis dinâmicas deveriam definir:
  1. Um destrutor.
  2. Um construtores por cópia.
  3. Um operador de atribuição por cópia.

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).

2  Quebras do princípio do encapsulamento

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 instância.  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 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

public:
 
Quarto* devolveQuartoComIdentificador(std::string const& identificador) const;

cuja implementação é:

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

Apesar de ter um ciclo interno bem estruturado, este método tem vários problemas:
  1. É público.
  2. É público e devolve um ponteiro para uma variável membro não constante (quebra o encapsulamento dos dados, sendo equivalente a declarar variáveis publicas).
  3. Devolve um ponteiro para uma variável membro não constante e indica que a sua execução não altera a classe.
  4. Se o quarto não existir estoira (na melhor das hipóteses) ou devolve lixo, o que fará o programa estoirar mais tarde num ponto qualquer do programa depois deste ciclo.  I.e., falta uma asserção verificando a pré-condição, que neste caso é que existe um quarto com o identificador dado.
É claro que existem outros tipos de quebras do encapsulamento, como a utilização injustificada de "amizades", a declaração de atributos publicos e a maior quebra de encapsulamento possível: a declaração de variáveis (não constantes) globais.  Este último tipo de solução, foi, e será sempre, fortemente penalizado.

3 Membros e responsabilidades das classes

O método

public:
 
list<Reserva*> devolveReservas() const;

além de ter os problemas citados acima, indica que a classe, (neste caso a classe 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.

4  Atributos desnecessários

Na seguinte definição

private:
  list<Reserva*> reservas;
  int numero_de_reservas;

a variável 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:

int comprimentoDaLista(list<...> const& lista) {
    int comprimento = 0;
    for(list<...>::iterator i = lista.begin(); i != lista.end(); ++i)
            ++comprimento;
    return comprimento;
}

que, pelas razões acima citadas, é totalmente desnecessária.

5  Estilo e elegância na programação

Há algumas coisas que um programador com um mínimo de experiência (como se pretende que os alunos desta cadeira tenham daqui a poucas semanas) não faz a não ser quando há justificações extremamente fortes:

6  Utilização de instruções de asserção e verificação de invariantes

Em ciclos como o do inspector abaixo  (agora correctamente declarada e definida):

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

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:

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.

7  Utilização de espaços nominativos

Não deve ser especificada a utilização de um espaço nominativo num ficheiro de interface (.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.

8  Ciclos

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

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 while para procurar o item em causa e , após o ciclo, realizam a operação pretendida.  Por exemplo,

list<...>::iterator i = lista.begin();
while(i != lista.end() and
      (*i)->ocupado() and (*i)->reservadoEntre(data_inicio, data_fim))
    ++i;
... // Operação sobre *i.

é mais legível e adequado do que

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