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:

  1. métodos de implementação usados nos módulos. Qualquer combinação de conjuntos de programas que coincidam em suas especificações de módulo são membros de uma família de programas. Subfamílias podem ser definidas também dividindo-se cada um dos módulos principais em submódulos de formas alternativas ou usando o método de programação estruturada para descrever uma família de implementações para o módulo.
  2. Variações nos parâmetros externos. As especificações do módulo podem ser escritas em termos de parâmetros, desta forma, resultando em uma família de especificações. Programas podem diferir nos valores dos seus parâmetros e ainda ser considerados membros de uma família de programas.
  3. Uso de subconjuntos. Em muitos casos uma aplicação irá requerer apenas uma fração das funções providas pelo sistema. Podemos considerar que programas consistem de um subconjunto de programas descritos por um conjunto de especificações de módulos para ser membro de uma família. Isto é especialmente importante no desenvolvimento de famílias de sistemas operacionais, onde algumas instalações vão requerer apenas uma porção do sistema provido por outro. O conjunto de possíveis subconjuntos é definido pelas relações "uses" entre programas distintos.

 

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.