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

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

Atributos

3.1  Atributos desnecessários

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.

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

3.3  Ser vs. conter

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.

3.4  Contentores como atributos

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.

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

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