Erros mais frequentes na resolução do Problema

Sumário

Introdução

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.

Má atribuição de responsabilidades

De acordo com o enunciado, o programa pretendido deveria possuir as três classes Veículo, Reserva e GestorDeAgência para representar respectivamente os conceitos de veículo, reserva e gestor de agência.  A classe Veículo teria como responsabilidade permitir a consulta da matrícula, marca e modelo do veiculo bem como consultar e alterar as tarifas do aluguer; a classe Reserva deveria ter como responsabilidades permitir a consulta das datas da reserva e dos dados do cliente; a classe GestorDeAgência teria como responsabilidade efectuar a interacção entre o utilizador e os veículos e as suas reservas, i.e., auxiliar a gerir os veículos e as suas reservas.

No caso seguinte,

inline void Veiculo::alteraTarifas() 
{
    std::cout << "Tarifa diaria: ";
    std::cin >> tarifa_diaria_;
    std::cout << "Tarifa de fim de semana: ";
    std::cin >> tarifa_fim_de_semana_;
}

é a classe Veiculo que está a realizar o trabalho de interagir com o utilizador que é responsabilidade da classe GestorDeAgência.  Ou seja, deveria ser

inline void Veiculo::alteraTarifaDiasUteis(double const nova_tarifa)
{
    assert(cumpreInvariante());

    tarifa_dias_uteis_ = nova_tarifa;

    assert(cumpreInvariante());
}

...

void GestorDeAgencia::alteraTarifarioDeVeiculo()
{
    assert(cumpreInvariante());
    std::string matricula;

    ...

    if(veiculoJaEstaRegistado(matricula))
    {

        double nova_tarifa_diaria;
        double nova_tarifa_fds;

        ...

        std::vector<Veiculo>::size_type i = 0;
     
        while(veiculos_registados[i].matricula() != matricula and
              i != veiculos_registados.size())
            ++i;
     
        veiculos_registados[i].alteraTarifaDiasUteis(nova_tarifa_diaria);
        veiculos_registados[i].alteraTarifaFds(nova_tarifa_fds);
    }
    else
       
...

    assert(cumpreInvariante());
}

Violações 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 classeEsta é 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 GestorDeAgência é 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 GestorDeAgência, 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 privada, naturalmente, pois diz respeito apenas à sua implementação.

As operações auxiliares, que não fazem parte da interface da classe, devem também ser privadas.

Atributos

3.1  Atributos desnecessários

Na seguinte definição

private:
  std::list<Veiculo> veículos;
  int número_de_veículos;

a variável número_de_veículos 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 invocar a operação std::list<Veiculos>::size(), que nos devolve o número de itens que a lista contém.

3.2  Atributos usados para trocar informação entre métodos

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 estritamente necessário e ter o menor âmbito possível.

Inclusões em ficheiros de interface

Num ficheiro de interface (.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.

Utilização de espaços nominativos

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 não membro auxiliares

Rotinas como pedeInteiroPara(int& valor), 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.

Ciclos

Se tivermos uma lista de Veículos, e se soubermos que cada elemento da lista possui uma matricula única, o uso de ciclos para encontrar um determinado elemento da lista deve ser feito de forma a que não seja percorrida a lista completa mesmo após ter sido encontrado o elemento desejado.  A mesma observação é válida para qualquer outro tipo de contentor.  Por exemplo, em

for(std::vector<Veiculo>::size_type i = 0; i != veiculos.size(); ++i)
    if(veiculos[i].matricula() == matricula)
        veiculos[i].alteraTarifas();

se o veículo desejado for o primeiro da lista e a lista tiver n veículos, vamos repetir esta operação n -1 vezes desnecessariamente.  Uma solução possível para este problema poderia ser:

std::vector<Veiculo>::size_type i = 0;

while(veiculos_registados[i].matricula() != matricula and
      i != veiculos_registados.size())
    ++i;

if(i != veiculos_registados.size())
    veiculos[i].alteraTarifas();

A definição da variável de controlo do ciclo fora do corpo do ciclo apenas 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) {
    ...
}

// Aqui o iterador não é necessário.

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

Recursividade em vez de ciclos

Usar recursividade para substituir ciclos simples já é, em geral, má ideia.  Pior ainda é quando se usa erradamente.  E uma verdadeira desgraça é quando se usa erradamente e... parece funcionar impecavelmente!  No seguinte exemplo pode ver-se o uso da recursividade de uma  forma incorrecta:

void GestorDeAgencia::menu()
{
   
...

    switch(opcao) {

        ...

      case 6:
        alteraTarifarioDeVeiculo();

        menu(); // Errado!

        break;

        ...

    }

    ...
}

Quando se pretende repetir uma determinada operação deve recorrer-se ao uso de ciclos e não evocar de uma forma recursiva as operações, tal como é feito neste exemplo.  Outro erro resultante do uso incorrecto da recursividade consiste em evocar métodos de uma forma encadeada, o método A evoca o método B e o método B evoca o método A, que pode por sua vez voltar a evocar o método B e continuar assim indefinidamente. 

void GestorDeAgencia::alteraTarifarioDeVeiculo()
{
   
...

    if(veiculoJaEstaRegistado(matricula))
    {
       
...

        menu();
    } else
        menu();
}

Assim, o método alteraTarifarioDeVeiculo() não devia evocar o método menu.

Booleanos e o estilo...  principiante...

Continua a haver muita gente a escrever código como

bool f(...
{
    if(condição)
        return true;
    else
        return false;
}

Escrevam antes

bool f(...)
{
    return condição;
}

pois é muito mais simples e claro!

O mesmo para:

if(condição == true)
   
...

e

if(condição == false)
   
...

que devem ser escritas antes como

if(condição)
   
...

e

if(not condição)
   
...