Sobre Projeto e Desenvolvimento de
Famílias de Programas
Resumo
Famílias de programas são definidas
(analogamente a famílias de hardware) como conjuntos de programas nos quais há
tantas propriedades em comum que é vantajoso estudar essas propriedades antes
de analisar cada membro. Considerando que, se ao se desenvolver um conjunto de
programas parecidos dentro de um período de tempo, deve-se considerar o
conjunto como um todo enquanto desenvolvemos os primeiros três enfoques, é
discutido. Um enfoque tradicional de desenvolvimento, chamado
"desenvolvimento seqüencial" é comparado com o "refinamento
gradativo" e "especificação do encapsulamento de módulos de
informação". Uma comparação mais detalhada dos dois métodos é feita. Por
meio de vários exemplos é demonstrado que os dois métodos são baseados nos
mesmos conceitos mas levam a vantagens complementares.
Termos Índices
Encapsulamento de módulos de
informação, especificação de módulos, famílias de programas, metodologia de
projeto de software, engenharia de software, refinamento na forma de passos.
Introdução
Nós levaremos em consideração um
conjunto de programas para constituir uma família, quando isso valer a pena
para estudar programas de um conjunto, primeiro estudando as propriedades
comuns do conjunto e então determinando as propriedades específicas de cada um
dos membros da família. Uma típica família de programas é o conjunto de versões
de um sistema operacional distribuído por um fabricante. Quando há muita
diferença significativa entre as versões, é ideal aprender-se as propriedades
comuns de todas as versões antes de estudar detalhes de qualquer uma. Famílias
de programas são análogas a famílias de hardware de vários fabricantes. Embora
os vários modelos em uma família de hardware podem não ter um componente
individual em comum, quase todo mundo lê o manual dos "princípios de
operação" antes de estudar as características especiais de um modelo
específico. Métodos tradicionais de programação objetivam o desenvolvimento de
um único programa. Nesse artigo, nos propomos a examinar explicitamente o
processo de desenvolvimento de uma família de programas e comparar várias
técnicas de programação convenientes ao projeto de conjuntos de programas
semelhantes.
Motivação para o interesse em famílias
de programas
Mudanças nas necessidades das
aplicações, variações na configurações de hardware, e a sempre presente
oportunidade de aperfeiçoar-se um programa que irá inevitavelmente existir em
muitas versões. As diferenças entre essas versões são inevitáveis e úteis. Além
disso, a experiência mostra que nós não podemos sempre projetar todos
algoritmos antes da implementação do sistema. Estes algoritmos são
invariavelmente melhorados experimentalmente depois do sistema estar completo.
Esta necessidade de existência de várias versões experimentais de um sistema é
ainda outro motivo para o interesse em programas "multiversão".
E é bom saber que a produção e
manutenção de programas multiversão é um caro problema para os distribuidores
de software. Com freqüência, manuais e grupos de manutenção separados são
necessários. A conversão de um programa de uma versão para outra é uma não
trivial (e portanto cara) tarefa.
Este artigo trata dois relativamente
novos métodos de programação que são explicitamente destinados ao desenvolvimento
de famílias de programas. Nos motivamos pela suposição de que se um
projetista/programador presta consciente atenção para a família bem após uma
seqüência de programas individuais, o custo total de desenvolvimento e
manutenção dos programas será reduzido. O objetivo desse artigo é comparar os
métodos, provendo alguma idéia sobre as vantagens e desvantagens de cada um.
Método clássico de produção de famílias
de programas
O método clássico de desenvolvimento de
programas é melhor definido como uma "complementação seqüencial". Um
membro em particular é completamente desenvolvido até o estágio de produção.
O(s) próximo(s) membro(s) da família é(são) desenvolvido(s) através da
modificação desses programas que estão na fase de produção. Uma representação
esquemática deste processo é mostrada pela figura 1. Nesta figura um nó é
representado como um círculo, se ele é uma representação intermediária no
caminho para o desenvolvimento de um programa, mas não um programa em produção
por si só. Um X representa um completo (usável) membro da família. Uma seta de
um nó até outro indica que um programa (ou a representação intermediária de um
programa) associado com o primeiro nó foi modificado para produzir este
associado com o segundo.
Cada seta deste gráfico representa uma
decisão de projeto. Em muitos casos cada decisão reduz o conjunto de programas
possíveis sob consideração. Contudo, quando se começa a partir de um programa
em fase de produção, geralmente se vai através de um passo inverso, nos quais
os possíveis programas são novamente incrementados (p.e., alguns detalhes não
foram decididos). Os nós 5 e 6 são instâncias disso.
Quando uma família de programas é
produzida de acordo com o modelo acima, um membro da família pode ser
considerado como um ancestral de outros membros. É bem usual para os
descendentes de um dado programa compartilhar algumas das características de
seus ancestrais que não são apropriadas ao propósito de outros descendentes.
Levando a próxima versão à finalização, decisões corretas foram feitas que
poderiam não terem sido feitas se o programa descendente fosse desenvolvido
independentemente. Estas decisões ficam no programa descendente somente em
função de que sua remoção pode acarretar uma grande quantidade de
reprogramação. Como resultado, as próximas versões do programa têm deficiências
de performance, por elas serem derivadas de programas modificados anteriormente
projetados para funcionar em um diferente ambiente ou com uma diferente carga.
Novas técnicas
A figura 2 mostra o conceito básico
comum de novos métodos. Usando estes métodos nunca se modifica um programa
completo para se obter um novo membro da família; sempre se começa com um dos
estágios intermediários e se continua deste ponto com decisões de projeto,
ignorando as decisões feitas depois deste ponto no desenvolvimento de versões
anteriores. Onde no método clássico pode-se dizer que uma versão do programa é
ancestral de outra, aqui nós encontramos aquela das duas versões que possuem um
ancestral comum (3).
As várias versões não precisam ser
desenvolvidas seqüencialmente. Se o desenvolvimento de um ramo da árvore não
usa informação de outro ramo, as duas sub-famílias devem ser desenvolvidas em
paralelo. A segunda nota importante é que nesses métodos a ordem em que as decisões
são feitas possui mais significado que no método clássico. Rechamadas que todas
decisões fizeram abaixo de um ponto de um ramo são compartilhadas por todos os
membros das famílias abaixo daquele ponto. Em nossa motivação no conceito de
família demos ênfase à importância dos membros da família possuírem muitas
características em comum. Através da decisão, tanto quanto possível, antes do
ponto de divisão, nós aumentamos a semelhança dos sistemas. Como sabemos que
determinadas diferenças devem existir entre os programas, o objetivo de novos
métodos de projeto é possibilitar as decisões, que podem ser compartilhadas por
uma família inteira, para que estas famílias sejam feitas antes daquelas
decisões, que provém a diferenciação dos membros da família. Como a figura 2
ilustra, é muito significante falar sobre sub-famílias que compartilham muitas
decisões que são utilizadas por uma família inteira.
Se a raiz de uma árvore representa uma
situação antes que qualquer decisão tenha sido tomada, então dois programas,
que possuem apenas a raiz como um ancestral comum, não possuem nada em comum.
Nós devemos notar que a representação
deste processo por uma árvore é muito simplificado. Determinadas decisões de
projeto podem ser feitas sem consideração de outras (os processos de decisão
podem ser visualizados como operadores comutativos). É possível usar decisões
de projeto em uma quantidade grande de ramos. Por exemplo, um grande número de
diferentes sistemas operacionais pode fazer uso do mesmo algoritmo de prevenção
de travamento, ainda que não tivesse uma das decisões feitas em um ancestral
comum.
Representando os estágios
intermediários:
No método clássico de produção de
famílias de programas, os estágios intermediários não foram bem definidos e os
projetos incompletos não foram precisamente representados. Esta é a causa e o
resultado do fato daquela comunicação entre versões estar anteriormente na
forma de programas completos. Se qualquer um dos dois métodos discutidos aqui
são para trabalhar eficientemente, é necessário que nós tenhamos representações
precisas dos estágios intermediários (especialmente aquelas que podem ser
usadas como pontos de decisão). Ambos métodos enfatizam precisão nas descrições
de programas projetados parcialmente. Eles diferem na forma como os projetos
parciais são representados. Nós devemos notar que esta não é a versão final do
programa, que é nosso verdadeiro produto (raramente usa-se o programa sem
modificação); nos novos métodos isto é bem desenvolvido mas permanece uma
representação incompleta que é oferecida como uma contribuição ao trabalho
alheio.
Programando através do refinamento
gradativos
O método de "refinamento
gradativo" foi formalmente introduzido por Dijkstra e desde então foi
discutido por uma variedade de colaboradores. Na literatura, uma maior ênfase
foi dada à produção de programas corretos, mas umas das conseqüências foi a de
que o método encoraja a produção de famílias de programas. Um dos exemplos foi
o desenvolvimento de um programa para a geração de números primos, no qual os
seguintes programas ainda podem usar dois algoritmos completamente diferentes
para a geração de números primos. Esse programa incompleto definiu uma família
de programas que inclui os dois últimos significantes diferentes membros.
No "refinamento gradativo "
os estágios intermediários são representados por programas, que são completos
exceto para a implementação de certos tipos de operadores e operandos. Os
programas foram escritos como se os operadores e operandos estivessem embutidos
na linguagem. A implementação desses operadores na linguagem atual é postergado
para estágios posteriores. Enquanto a (implícita ou explícita) definição dos
operadores é suficientemente abstrata para permitir uma variedade de
implementações, as versões posteriores do programa definem uma família na qual
há um membro para cada possibilidade de implementação de um operador e operando
não implementado. Por exemplo, um programa que foi escrito com a declaração do
tipo de dados pilha e operadores push e pop. Somente nas versões posteriores a
representação da pilha e procedimentos para executar push e pop são
introduzidos. Nós ilustramos as técnicas do refinamento gradativo com dois
exemplos que serão usados em uma comparação posterior.
Exemplo 1) Programa de números primos de
Dijkstra: Dijkstra descreveu o desenvolvimento de um programa para a impressão
de números primos. O primeiro passo é o seguinte:
begin variable table p;
fill table p with first thousand prime
numbers;
print table p;
end
Nesse programa Dijkstra assumiu um tipo
de operando "table" e dois operadores. A representação da tabela, o
método de cálculo dos números primos e o formato de apresentação serão
decididos depois. De fato, as únicas decisões obrigatórias (características
comuns de toda família de programas) é que todos os números primos serão
desenvolvidos antes que qualquer um seja representado, e que nós sempre iremos
querer os primeiro mil números primos. Dijkstra então debate entre a
implementação da tabela ou elaboração do método "fill table". Eventualmente
ele decide que a tabela deva ser implementada, e todos membros da família que
resta irão compartilhar a mesma implementação de tabela. Um ramo da família com
uma implementação de uma tabela alternativa é mencionada mas não desenvolvida.
Os próximos membros da família serão desenvolvidos considerando diversos
métodos possíveis de computação de números primos.
Exemplo 2) Programa de índice KWIC do
Wulf: Wulf apresenta o método de refinamento gradativo de um programa de
produção de índice KWIC, como segue:
Passo 1: PRINTKWIC
Nós podemos pensar nisso como sendo uma
instrução em uma linguagem (ou máquina), no qual a noção de geração de um
índice KWIC é primitiva. Partindo-se do princípio que esta operação não é
primitiva na maioria das linguagens práticas vamos proceder definindo:
Passo 2: PRINTKWIC: generate and save
all interesting circular shifts
alphabetize the saved lines
print alphabetized lines
Novamente nós podemos pensar em cada
uma dessas linhas como sendo uma instrução em uma linguagem apropriada; e
novamente, partindo-se do princípio que ela não é primitiva em muitas
linguagens existentes, nós temos que defini-las; por exemplo:
Passo 3 a: generate and save all
interesting circular shifts
for each line in the input do
begin
generate and save all interesting
shifts of "this line"
end
etc.
Para comparações posteriores,
relacionaremos as decisões que devem ser compartilhadas pelos membros
remanescentes da família:
1) Todas mudanças serão armazenadas;
2) Todas mudanças circulares serão
geradas e armazenadas antes da ordenação alfabética começar;
3) A ordenação será concluída antes que
a apresentação comece;
4) todas as mudanças de uma linha serão
feitas antes de qualquer mudança em outra linha.
5) As mudanças que não interessarem
serão eliminadas logo que forem geradas.
Nos melhores exemplos de programação
utilizando o refinamento gradativo, as definições dos operadores foram
informais. Todos os exemplos publicados foram projetados como exemplos
tutoriais, e os operadores foram mantidos de forma "clássica" então
aquelas compreensões intuitivas dos mesmos são suficientes para o correto
entendimento do desenvolvimento do programa. A única excessão conhecida do
autor é [7]. A definição formal dos operadores pode ser incluída pela aplicação
da inserção do predicado inicialmente introduzida por Floyd com o propósito de
verificação do programa. Como Dijkstra sugeriu, podemos pensar nos operadores
como "transformadores de predicado" (regras que descrevem como um
predicado que descreve o estado das variáveis depois da aplicação do operador
poder ser transformado em um predicado descrevendo o estado das variáveis do
programa antes do operador ser executado).
Técnica da Especificação de Módulos
Outra técnica para o projeto de
famílias de programas foi descrito em [9], [10]. Este método é distinguido do
método de refinamento gradativo, no qual as representações intermediárias não
são programas completos. Em vez disso, eles são "especificações" do
externamente visível comportamento coletivo de grupos de programas chamados
módulos. Estas representações intermediárias não são escritas em uma linguagem
de programação, e nunca tornam-se parte do sistema final.
Para ilustrar este método nós iremos
comparar o desenvolvimento do programa KWIC descrito em [9], [10] com o desenvolvimento
através do refinamento gradativo discutido anteriormente nesse artigo.
No método de "Especificação de
Módulos" as decisões de projeto que não podem ser propriedades da família
são identificados e um módulo (um grupo de programas) é projetado para ocultar
cada decisão de projeto. Para o nosso exemplo, as seguintes decisões de projeto
foram identificadas:
1) a representação interna dos dados a
serem processados;
2) a representação das trocas
circulares daquelas linhas e o momento em que as trocas seriam computadas;
3) o método de ordenação, que poderia
ser usado, e o momento em que a ordenação poderia ser descartada;
4) os formatos de entrada;
5) os formatos de saída;
6) a representação interna de palavras
individuais (uma parte da decisão 1).
Para ocultar a representação dos dados
na memória, um módulo permite que seus usuários simplesmente escrevam
CHAR(line, word, c) em ordem para acessar determinado caracter. Os dados foram
guardados neste módulo através da chamada SETCHAR(line, word, c). Outras funções
neste módulo poderiam informar o número de linhas, o número de palavras em
determinada linha, e o número de caracteres em uma palavra. Através do uso
deste grupo de programas, o resto do programa poderia ser escrito de uma forma
completamente independente da representação atual.
Um módulo bastante similar em aparência
como o descrito acima, ocultou a representação das mudanças circulares, o
momento em que eles foram computados, mesmo que eles nunca tivessem sido
armazenados. (Alguns membros da família de programas reduziram a necessidade de
armazenamento através do processamento do caracter em um dado ponto da lista de
mudanças sempre que se pediu). Todas estas implementações compartilham a mesma
interface externa.
Outros programas ocultam o momento e o
método de ordenação alfabética. Este (programa 2) módulo disponibiliza uma
função ITH (i) que daria o índice no segundo módulo para a linha i-th na
sequência alfabética.
As decisões listadas acima são aquelas
que não foram feitas, i.e., postergadas. As decisões não feitas são mais
difíceis de identificar. O projeto colocou restrições na forma que as partes do
programa podem se referir a outras e têm, desta maneira, reduzido o espaço de
possíveis programas.
A descrição acima é uma revisão breve
voltada para aqueles que já possuem alguma familiariedade com os dois métodos.
Aqueles que são iniciantes nesse assunto devem consultar os artigos originais
antes de ler mais.
Comparação baseada no exemplo KWIC
Para entender as diferenças nas
técnicas o leitor deve observar a lista de decisões que define a família de
programas KWIC da qual o desenvolvimento iniciou-se com Wulf. Todas as decisões
que são compartilhadas pelos membros da família Wulf estão ocultas em módulos
individuais pelo segundo método e diferenciam os membros da família. Aquelas
decisões sobre sequenciamento de eventos estão especificadas anteriormente no
desenvolvimento Wulf mas foram postergadas no segundo método.
Afim de que não se pense que no segundo
método não foram tomadas decisões sobre implementação, relacionaremos abaixo
algumas das propriedades comuns de programas produzidos utilizando o segundo
método.
1) Todos programas terão acesso à
string de caracteres durante o processamento do índice KWIC.
2) Palavras comuns como THE, AND, etc.,
não serão eliminadas até o estágio de saída (se existir).
3) O módulo de saída pegará sua
informação caracter por caracter.
O leitor atento terá notado que estas
decisões não são necessariamente boas. Não menos importante, as decisões devem
ser tomadas enquanto é possível trabalhar-se nos módulos para começar e
progredir na conclusão sem interações adicionais entre os programadores. Neste
método, o objetivo do trabalho inicial é não tomar decisões sobre o programa
mas fazer o possível para postergar (e consequentemente facilitar) as decisões.
Posteriores será muito mais fácil e rápido obter o resultado.
No método de refinamento gradativo nós
temos um progresso rápido ao fim do processo em uma família comum (pequenas
variações na família). Com módulos nós temos preparado o caminho para o
desenvolvimento de famílias relativamente irmãs.
******************************************************************
{Dorival}
Observações comparativas baseadas no
programa de números primos de Dijkstra
Agora nós vamos pegar a segunda visão
de desenvolvimento do programa de números primos de Dijkstra.
Em seu desenvolvimento, Dijkstra é
movido a fazer uma decisão prematura sobre a implementação da tabela de forma a
ir mais longe. Todos os membros da família desenvolvida subseqüentemente
compartilham aquela implementação. Ele pode decidir voltar atrás e reconsiderar
aquela decisão, ele teria que reconsiderar todas as decisões feitas após aquele
ponto. O método de especificação de módulo deveria dar margem a ele postergar a
implementação da tabela para um estágio posterior (i.e. esconder a decisão) e
desse modo terminar com êxito uma família ampla.
Observações comparativas baseadas em um
problema de sistema operacional.
Consideremos o problema de alocação de
espaço em um sistema operacional. Podemos assumir que possuímos uma lista de
áreas livres e dados que devem ser armazenados. Escrevendo um programa que irá
encontrar um espaço livre e alocar o espaço para o programa que esteja
precisando dele, é trivial. Infelizmente, existem muitos programas semelhantes,
e nós não podemos ter certeza qual deles nós precisamos. Os programas podem
diferenciar em pelo menos dois caminhos importantes, política e implementação
do mecanismo. Por política nós significamos simplesmente a regra da escolha do
lugar, se há uma grande quantidade de lugares utilizáveis; pela implementação
do mecanismo, nós significamos questões semelhantes a, como deveremos mostrar a
lista dos lugares livres, quais operação nós devemos executar para adicionar um
espaço livre a lista, como remover um espaço livre ? Deverá a lista ser
colocada em uma ordem especial ? Qual é o procedimento de busca ? etc..
As decisões discutidas acima são
importantes naqueles casos em que eles podem ter um maior impacto na
performance do sistema. De outra forma, nós não podemos escolher a melhor
solução; não há melhor solução!
No lado da política existem numerosos
debates entre políticas tais como "primeiro local" – alocar o
primeiro espaço utilizável na lista, "melhor local" – encontrar o
menor lugar que irá encaixar, "privilegia-se áreas no final do
espaço", "melhor local modificado" – procura por um pedaço que
encaixa bem mas não deixa um pequeno fragmento inutilizável, etc. É claro que a
maioria que tenha pesquisado o problema como a "melhor" política
depende da natureza da demanda, i.e., a distribuição da áreas requisitadas, o
quantidade de tempo desejado para que uma área será retornada, e tantas outras.
Escolher uma implementação é sempre
mais complicado porque ela depende em parte da política escolhida. Manter a
lista ordenada pelo tamanho dos fragmentos é válido se nós procuramos buscar um
"melhor local" entretanto não serve para a política que tende a
colocar menos coisas na área quanto possível.
O desenvolvimento do seguinte
"programa estruturado" de semelhante algoritmo ilustra a construção
de um programa abstrato que possui as propriedades de todos os quais nós
estamos interessados e não prejudica nossa escolha.
Estágio 1:
melhor escolha := null;
while todos espaços não forem considerados do
Begin
encontre o próximo item da lista de
espaços livre (candidato)
melhor escolha := melhor de (melhor
escolha, candidato)
end
se melhor escolha = null then
ação para erros
alocar (melhor escolha);
remover (melhor escolha)
Seguindo-se estritamente os princípios
de bem-escrever programas estruturados nós devemos verificar que o que está
acima é correto ou descrever abaixo as condições sob as quais nós podemos obter
certeza que é correto:
Diretivas de correção:
1) "melhor escolha" é a
variável capaz de indicar um espaço livre; null é um possível valor
indicando que não há espaço.
2) "todos espaços não forem
considerados" é um predicado que irá ser verdadeiro enquanto é possível
que um melhor espaço possa ser encontrado mas será falso quando todos os itens
possíveis forem considerados.
3) "candidato" é uma variável
do mesmo tipo que "melhor escolha".
4) "encontre o próximo item da
lista de espaços livre" irá atribuir ao seu parâmetro um valor indicando
um dos itens na lista de espaços livres. Se existem n itens na lista, n
chamadas do procedimento irão retornar cada um dos n itens por vez.
5) Nenhum item será excluído ou
acrescentados a lista durante a execução do programa.
6) "melhor de" é um
procedimento que recebe duas variáveis do mesmo tipo que "melhor
escolha" e retorna (como um valor do mesmo tipo) o melhor dos dois espaços
de acordo com algum critério inespecífico. Se nenhum dos locais forem
selecionáveis, o valor retornado é null, que nunca será selecionável.
7) "ação para erros" é o que
o programa supostamente deve fazer se nenhum local selecionável possa ser
encontrado.
8) "remover" é um
procedimento que remove os espaços indicados pelo seu parâmetro da lista de
espaço livres. Uma busca posterior não encontrará este espaço.
9) "alocar" é um procedimento
que entrega o espaço indicado pelo seu parâmetro para o programa que o
requisite.
10) Uma vez que nós tenhamos começado a
executar este programa, nenhuma outra execução dele irá começar até que ele
tenha terminado (exclusão mútua).
11) O único outro programa que irá
modificar as estruturas de dados envolvidas, é o que pode adicionar espaços
livres na lista. Exclusão mútua deve também ser necessária aqui.
Decisões de projetos no estágio 1
Apesar de que este primeiro programa
aparentemente seja inócuo, ele representa muitas decisões reais de projeto que
são melhor compreendidas considerando programas que não compartilham as
propriedades do programa abstrato acima.
1) Decidimos produzir um programa no
qual ninguém está autorizado a adicionar espaços livres enquanto se procura por
um espaço livre.
2) Nós não construímos um programa em
que duas buscas possam ser realizadas simultaneamente.
3) Estamos considerando apenas
programas em que os candidatos não são excluídos da lista de espaço livre
enquanto ele está sendo considerado.
4) Nos escolhemos não usar um programa
no qual um teste da possível alocação é feita antes de buscar na lista.
******************************************************************
{Peron}
Alguns programas razoáveis teriam que verificar por uma lista vazia ou mesmo uma verificação do tamanho do maior espaço disponível após o loop, de modo que não fosse gasto tempo procurando por uma condição de ajuste quando nenhum ajuste era possível. Em nosso programa, uma atribuição a “melhor escolha”, na avaliação da condição de término, “melhor escolha=null” mais uma avaliação ocorrerá toda vez o programa for chamado.
Os códigos omitidos da família de programas que compartilham o programa abstrato do estágio 1 não são uma omissão significativa. Se fossem, nós não escolheríamos para eliminar em estágio inicial de nosso projeto. Nós apenas discutimos, de modo que o leitor vera que ter escrito o programa do estagio 1 não foi um exercício vazio.
Nós agora consideraremos uma sub-familia de programas definidos no estágio 1. Nessa sub-familia nós decidimos representar a lista com um array bi-dimendicional , na qual cada linha representara um item na lista de espaço livre. Nós assumimos que o 1º espaço livre é colocado na linha 1 e o último na linha N, sendo que o espaço livre entre 1 e N representa o espaço livre valido. Nós não faremos nenhuma suposição sobre a informação mantida em cada linha para manter o espaço livre nem a ordem ou as linhas do array. Isso permite que escrevamos o seguinte:
Programa....
Nós reservamos as variáveis “melhor escolha”e “candidato” inteiros, para implementar o teste para “nem todos os espaços considerados”, por causa de nossas suposições. Nossas suposições não nos permite elaborar operações nas linhas do array ou implementar nossa política de decisão “melhor de”. Nós não podemos executar “remover” porque não sabemos se estamos alocando todo o espaço encontrado ou alocando somente a parte que precisamos e livrando-se do resto na lista de espaço livre.
******************************************************************
{Alisson}
Como as especificações dos módulos definem uma família
Membros de uma família de programas definidos por um conjunto de especificações de módulo podem variar de três principais formas:
Qual método utilizar
Agora deve estar claro que os dois métodos não são nem equivalentes nem contraditórios. Melhor dizendo eles são complementares. Eles são ambos baseados na mesma idéia básica ( veja nota histórica como segue): 1) representação precisa do estágio intermediário em um desenho de programa, e 2) Postergação de certas decisões, enquanto continua fazendo progresso em direção ao programa completo.
O refinamento gradativo ( como praticado na literatura) encoraja a tomada de decisão sobre o seqüênciamento mais cedo, porque as representações intermediárias são todas programas. A postergação das decisões até o "run time" requer a introdução de métodos[14]. O método de especificação de módulos não é usualmente a forma mais conveniente para expressar a seqüência de decisões. Na seqüência do nosso projeto do índice KWIC descrevemos por uma anotação "estruturado" "programa principal", foi uma das muitas formas em que os módulos podem ser usados para produzir um índice KWIC. Foi escrito por último.
O refinamento gradativo foi um avanço significante que não adicionou ao esforço de desenhar o primeiro membro completo da família. Mantendo a complexidade sobre controle isto ajuda a reduzir o esforço total. Em contrapartida, as especificações do módulo representam uma parcela muito significante de esforço. A experiência tem mostrado que o esforço envolvido na escrita de um conjunto de especificações pode ser maior que o esforço que se possa ter para escrever um programa completo . O método permite a produção de uma ampla família e a finalização de várias partes do sistema independentemente, mas com um custo significativo. Este custo é usualmente pago para aplicar o método somente quando se quer a implementação eventual de uma grande gama de possíveis membros da família em contrapartida o método refinamento gradativo é sempre proveitoso.
Relação da questão famílias de programas aos geradores de programas.
Um passo comum tomado pela indústria de suporte a programas multiversão é a construção de sistemas de geração de programas. Estes programas nos ajudam em grande parte na descrição das configurações de hardware e de software necessários aos usuários. O que se faz no gerador é uma descrição de uma grande família de programas e o gerador materializa um membro específico para ser carregado em um determinado hardware.
Os métodos descritos neste artigo não têm a intenção de substituir os sistemas geradores. Desde que estes métodos sejam aplicados no estágio de projeto , o uso de sistemas geradores são muito úteis quando um membro específico da família tem que ser produzido. O refinamento gradativo e o método de especificações de módulos podem simplificar o trabalho para ser feito por um sistema gerador.
Sistemas geradores podem ser completamente desnecessários se você escolheu construir um programa que em tempo de execução deveria simular qualquer membro da família. Como um programa poderia ser relativamente ineficiente. Removendo muito desta variabilidade ao mesmo tempo que o programa é gerado, o aumento da capacidade de produção se faz possível.
Todavia uma família de programas inclue pequenos membros onde certas variáveis são fixas e grandes membros nos quais estes fatores podem variar. Por exemplo, uma família de sistemas operacionais podem incluir alguns pequenos membros onde o numero de processos são fixos e outros membros onde criação e deleção dinâmicas (de processos) são possíveis. Os programas desenvolvidos para grandes membros da família podem ser usados como parte de um "gerador", que produz um membro menor.
Conclusões.
outro meio de comparar os dois métodos é respondendo a estas FAQs:
1) Quando devemos ensinar programação estruturada ou refinamento gradativo aos nossos estudantes?
2) Quando devemos ensinar sobre módulos e especificações?
A primeira pergunta, eu posso responder com outra pergunta: " Quando devemos ensinar programação desestruturada?" A segunda pergunta de qualquer forma, requer uma resposta franca: : Só deve ser falado de especificações de módulo àqueles estudantes que aprenderam a programar bem e já se decidiram a seguir adiante na construção de pacotes [16].
Uma das dificuldades de aplicar os recentes conceitos programação estruturada é que não há critérios de como se deva avaliar a estrutura de um sistema em uma base objetiva.
Os aspirantes da profissão devem ir aos "mestres" e pedir uma avaliação. O "mestre" poderá dizer o que é bom ou não para o sistema.
O conceito de programação em famílias nos dá uma maneira de analisar a estrutura do programa mais objetivamente. Para qualquer descrição precisa de uma família de programas ( tal como um refinamento parcial como um conjunto de especificações ou uma combinação de ambos) uma maneira é se perguntar quais programas devem ser excluídos e quais programas devem permanecer.
Pode-se considerar o desenvolvimento do programa como bom, se as decisões anteriores tiverem excluído programas que não interessavam, que eram indesejados ou eram desnecessários. As decisões que removem programas desejáveis deveriam ser também postergadas para um determinado estagio ou confinadas em um subconjunto de código bem definido.