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.
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());
}
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 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.
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.
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.
.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 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.
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;
}
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.
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)...