Modularização

Atualizado pela última vez em outubro de 2021.

A modularização é um conceito muito importante na programação que deve continuar sendo usado depois dos enunciados pararem de exigir isso.

É importante saber criar programas feitos de pequenos pedaços extremamente simples e independentes que podem ser modificados ou corrigidos sem que seja necessário ficar prestando atenção ao programa inteiro. Para isso, o planejamento de parâmetros é fundamental para criar subprogramas pequenos e independentes. Esteja atento às questões: Quais dados esse subprograma precisa receber para resolver o subproblema? Que respostas ele precisa passar adiante? Esses serão os parâmetros de entrada e os parâmetros de saída. Eventualmente pode existir uma informação que é ao mesmo tempo entrada e saída, porque ela precisa ser atualizada ou melhorada, mas isso é menos comum. Procure se ater inicialmente à definir as entradas e as saídas.

Por exemplo, para calcular a área de um retângulo, as entradas são a altura e a largura, enquanto a saída é a área. Se o seu subprograma recebe parâmetros que não altura nem largura, algo está mal feito e o projeto deve ser refeito.

Em IAlg existem duas complicações para essa determinação: 1) a necessidade de mais de uma saída; porque a linguagem C++ prevê um único parâmetro de saída (baseado no conceito matemático de função) e 2) a necessidade de controles adicionais numa recursão. Esses dois casos serão abordados em detalhes a seguir.

Parâmetros versus operações de leitura e escrita

Quando um enunciado se refere a receber e retornar, está falando de passagem de parâmetros. Exemplo de função que recebe um inteiro e retorna um texto:

string funcao(int numero);

Ler e escrever são coisas bem diferentes de receber e retornar, são operações de interface. Exemplo de função que um inteiro e escreve um texto:

void funcao() { int numero; cin >> numero; ... cout << "texto" << endl; }

Subprogramas não devem misturar interface e processamento, ou seja, ou o subprograma serve para ler informações, ou serve para escrever informações ou serve para processar informações. Essas coisas não devem ser misturadas a não ser que o enunciado exija.

Ruim (mistura interface com processamento):

void Par(int numero) { if (numero % 2 == 0) cout << "par" << endl; else cout << "impar" << endl; }

Bom (toda leitura e escrita no subprograma principal):

bool Par(int numero) { return numero % 2 == 0; } int main() { int n; cin >> n; if (Par(n)) cout << "par" << endl; else cout << "impar" << endl; }

Independência

Subprogramas devem ser tão independentes quanto possível. Não se deve fazer um subprograma que só funciona quando usado de determinada forma. Por exemplo, não se deve fazer um subprograma que encontra o maior item de uma coleção, mas que não funciona se a coleção tem um único item, depois testar se tem um único item antes de chamar a função (pior ainda se for depois de chamar). O teste antes de chamar só deve ser usado para separar os casos de erro, ou seja, aqueles casos em que não é possível produzir uma resposta. Quando um subprograma tem requisitos para funcionar (por exemplo: só faz sentido calcular isso se for um valor não negativo), é bom escrever esses requisitos em comentários no início do subprograma.

Alguns outros exemplos de planejamento ruim que afeta a independência de um subprograma: a) só funciona se passarem determinado valor em determinado parâmetro; b) uso de variáveis externas para transportar informação para dentro ou para fora do subprograma; c) uso de passagem por referência para valores de entrada.

Tipos de passagem de parâmetros

Quando for necessário retornar mais de um valor, é usual usar parâmetros comuns para retornar resultados ao invés de receber valores. Para isso usamos passagem por referência. Como em C++ não exite uma sintaxe para parâmetros de saída, a passagem por referência, que define um parâmetro de entrada e saída, é usada para parâmetros de saída.

A passagem por referência até pode ser usada para valores de entrada, se o programador quer evitar uma cópia do parâmetro. Isso pode ser importante quando o parâmetro ocupa um espaço grande de memória. Nesse caso, deve-se usar a passagem por referência constante, ou seja o parâmetro passado por referência deve ser declarado como constante e não como variável (veja o exemplo da segunda função Menor, abaixo). Um exemplo de informação grande são os vetores, ficar copiando vetores gasta tempo e espaço. Lembre-se que strings também são vetores. Os vetores de C não são copiados e portanto não há nenhuma utilidade de usar passagem por referência com eles.

O uso da passagem por referência para retornar valores diminui a legibilidade dos programas e por isso recomendo que ela deixe de ser usada após o aprendizado de ponteiros.

O uso de parâmetros comuns para retornar dados traz um problema de ordem de avaliação. Na linguagem C++, uma expressão com vários operandos pode ter esses operandos calculados em qualquer ordem. Se o programador escreve algo que depende de uma certa ordem de avaliação, então foi criado comportamento indefinido. Por exemplo: suponha que você precisa de uma função que retorna um int e um float:

int Funcao(char entrada, float& saida);

Se você usar a função e o retorno float da função na mesma expressão, cria comportamento indefinido porque não há nenhuma garantia que a função vai ser executada antes da determinação do valor que deveria ser modificado pela função. Exemplo:

cout << Funcao(letra, resposta) << ' ' << resposta << endl;

Parâmetros inúteis

Cuidado para não deixar parâmetros que não são usados no subprograma. No caso de parâmetros de saída, eles devem ser pensando em termos de quais valores se espera de uma resposta, mesmo que nem sempre todas as saídas sejam usadas (independência). Retornar o mesmo valor em dois lugares diferentes é indício de parâmetro inútil. Somente informações necessárias devem ser parâmetros. Criar parâmetros para informações auxiliares é errado. A criação de outros subprogramas pode eliminar a suposta necessidade de parâmetros auxiliares.

Exemplo: Um subprograma que calcula o reverso de um número natural precisa receber só esse número e precisa retornar o reverso, ou seja:

unsigned Reverso(unsigned n);

Pode ser usado o tipo int se você acha complicado usar unsigned mas qualquer parâmetro extra ou uso de passagem por referência é um projeto ruim. Se você está pensando em passar uma outra variável para funcionar como acumulador nas suas contas, tente usar uma variável local para isso. Por exemplo: se você não consegue calcular o reverso sem calcular antes a quantidade de algarismos do número, então não exija que isso seja passado como parâmetro; calcule dentro da função usando variáveis locais.

Exemplo: um subprograma que encontra o menor elemento num vetor precisa receber o vetor e retornar a posição do menor. Se o vetor for do tipo vetor de C então será necessário um parâmetro auxiliar que é o tamanho do vetor. Se for um vector, não é preciso nenhum parâmetro auxiliar, mas é desejável passar o vetor por referência constante para evitar cópia, ou seja:

unsigned Menor(int vetor[], unsigned tamanho); unsigned Menor(const vector<int>& vetor);

Parâmetros na recursão

Os mesmos conceitos gerais continuam valendo se seu subprograma é recursivo. Porém pode ser necessário parâmetros auxiliares para controlar a recursão. Inicialmente é importante gastar um tempo para pensar se esses parâmetros auxiliares são realmente necessários. Em IAlg eles geralmente não são e quando aparecem são um indício de que você está complicando desnecessariamente seu programa. Se você não conseguir eliminar esses parâmetros auxiliares, separe seu subprograma em dois: um principal que não tem parâmetros auxiliares e um auxiliar que tem. Coloque comentário no subprograma auxiliar informando que ela só deveria ser usado pelo outro. Em IAlg não ensinamos formas de obrigar que isso aconteça.

A busca binária recursiva é um exemplo de caso em que não é possível se livrar dos parâmetros auxiliares pois a busca vai simplificando o vetor em termos de tamanho, mas não é possível simplificar sempre na direção do início do vetor. É necessário um índice de início e um índice de fim. Entretanto, quando alguém vai procurar alguma coisa em um vetor, é de se esperar que queira procurar em todo o vetor e por isso, não é desejável obrigar quem for usar a função a determinar o início. Usando o tipo vetor de C, é de se esperar que os parâmetros de entrada sejam: 1) o vetor, 2) o tamanho do vetor e 3) o item procurado. Os parâmetros de saída seriam: 1) a posição do item e 2) uma indicação sobre se o item foi encontrado. Portanto os parâmetros seriam:

unsigned Busca(int vetor[], unsigned tamanho, bool& encontrado);

Mas com esses parâmetros não temos informação suficiente para delimitar uma porção do vetor na busca recursiva, então fazemos uma função auxiliar, onde o inicio e o fim do espaço usado no vetor são parâmetros:

unsigned BuscaAux(int vetor[], unsigned inicio, unsigned fim, bool& encontrado);

E a função principal chama a função auxiliar determinando essas variáveis de controle:

unsigned Busca(int vetor[], unsigned tamanho, bool& encontrado) { return BuscaAux(vetor, 0, tamanho-1, encontrado); }

Quem vai fazer a busca não precisa então se preocupar em produzir valores auxiliares:

bool encontrado; unsigned pos = Busca(vetor, tamanho, encontrado); if (encontrado) cout << "Item encontrado na posicao " << pos << ".\n"; else cout << "Item nao encontrado!\n";

Esta página é mantida por Bruno Schneider para a disciplina de IAlg