Programação estruturada - Structured programming

A programação estruturada é um paradigma de programação que visa melhorar a clareza, a qualidade e o tempo de desenvolvimento de um programa de computador, fazendo uso extensivo das construções de fluxo de controle estruturado de seleção ( if / then / else ) e repetição ( while e for ), estruturas de bloco e sub - rotinas .

Surgiu no final dos anos 1950 com o aparecimento das linguagens de programação ALGOL 58 e ALGOL 60 , com a última incluindo suporte para estruturas de blocos. Fatores que contribuem para sua popularidade e ampla aceitação, primeiro na academia e depois entre os profissionais, incluem a descoberta do que agora é conhecido como o teorema do programa estruturado em 1966 e a publicação da influente carta aberta " Go To Statement Considered Harmful " em 1968 pelo cientista da computação holandês Edsger W. Dijkstra , que cunhou o termo "programação estruturada".

A programação estruturada é mais freqüentemente usada com desvios que permitem programas mais claros em alguns casos particulares, como quando o tratamento de exceções deve ser executado.

Elementos

Estruturas de controle

Seguindo o teorema do programa estruturado , todos os programas são vistos como compostos de estruturas de controle :

  • "Seqüência"; instruções ordenadas ou sub-rotinas executadas em sequência.
  • "Seleção"; uma ou várias instruções são executadas dependendo do estado do programa. Isso geralmente é expresso com palavras - chave como if..then..else..endif. A declaração condicional deve ter pelo menos uma condição verdadeira e cada condição deve ter um ponto de saída no máximo.
  • "Iteração"; uma instrução ou bloco é executado até que o programa atinja um certo estado ou operações tenham sido aplicadas a cada elemento de uma coleção. Isso geralmente é expressa com palavras-chave como while, repeat, forou do..until. Freqüentemente, é recomendado que cada loop tenha apenas um ponto de entrada (e na programação estrutural original, também apenas um ponto de saída, e algumas linguagens impõem isso).
  • "Recursão"; uma instrução é executada chamando a si mesma repetidamente até que as condições de encerramento sejam atendidas. Embora semelhantes na prática aos loops iterativos, os loops recursivos podem ser mais eficientes do ponto de vista computacional e são implementados de forma diferente como uma pilha em cascata.
Representação gráfica dos três padrões básicos - sequência, seleção e repetição - usando diagramas NS (azul) e fluxogramas (verde).

Sub-rotinas

Sub-rotinas ; unidades que podem ser chamadas, como procedimentos, funções, métodos ou subprogramas, são usadas para permitir que uma sequência seja referida por uma única instrução.

Blocos

Os blocos são usados ​​para permitir que grupos de instruções sejam tratados como se fossem uma única instrução. Linguagens estruturadas em bloco têm uma sintaxe para estruturas encerradas de alguma forma formal, como uma instrução if entre colchetes if..ficomo em ALGOL 68 , ou uma seção de código entre colchetes BEGIN..END, como em PL / I e Pascal , recuo de espaço em branco como em Python - ou as chaves {...}de C e de muitas linguagens posteriores .

Linguagens de programação estruturadas

É possível fazer programação estruturada em qualquer linguagem de programação, embora seja preferível usar algo como uma linguagem de programação procedural . Algumas das linguagens inicialmente usadas para programação estruturada incluem: ALGOL , Pascal , PL / I e Ada , mas a maioria das novas linguagens de programação procedural desde então incluíram recursos para encorajar a programação estruturada e, às vezes, deixaram de fora recursos - notavelmente GOTO - em um esforço para tornar a programação não estruturada mais difícil. A programação estruturada (às vezes conhecida como programação modular) impõe uma estrutura lógica no programa que está sendo escrito para torná-lo mais eficiente e fácil de entender e modificar.

História

Fundamentação teórica

O teorema do programa estruturado fornece a base teórica da programação estruturada. Ele afirma que três maneiras de combinar programas - sequenciamento, seleção e iteração - são suficientes para expressar qualquer função computável . Essa observação não se originou com o movimento de programação estruturada; essas estruturas são suficientes para descrever o ciclo de instrução de uma unidade central de processamento , bem como a operação de uma máquina de Turing . Portanto, um processador está sempre executando um "programa estruturado" neste sentido, mesmo que as instruções que ele lê da memória não façam parte de um programa estruturado. No entanto, os autores geralmente atribuem o resultado a um artigo de 1966 de Böhm e Jacopini, possivelmente porque o próprio Dijkstra citou esse artigo. O teorema do programa estruturado não aborda como escrever e analisar um programa estruturado de forma útil. Essas questões foram abordadas durante o final dos anos 1960 e início dos anos 1970, com contribuições importantes de Dijkstra , Robert W. Floyd , Tony Hoare , Ole-Johan Dahl e David Gries .

Debate

PJ Plauger , um dos primeiros a adotar a programação estruturada, descreveu sua reação ao teorema do programa estruturado:

Nós, convertidos, agitamos essa notícia interessante sob o nariz dos programadores de linguagem assembly não reconstruídos, que continuavam apresentando fragmentos tortuosos de lógica e dizendo: 'Aposto que não dá para estruturar isso.' Nem a prova de Böhm e Jacopini, nem nossos sucessos repetidos em escrever código estruturado os trouxeram um dia antes do que estavam prontos para se convencer.

Donald Knuth aceitou o princípio de que os programas devem ser escritos com a comprovação em mente, mas ele discordou (e ainda discorda) em abolir a declaração GOTO. Em seu artigo de 1974, "Structured Programming with Goto Statements", ele deu exemplos em que acreditava que um salto direto leva a um código mais claro e eficiente sem sacrificar a comprovação. Knuth propôs uma restrição estrutural mais flexível: deve ser possível desenhar o fluxograma de um programa com todas as ramificações anteriores à esquerda, todas as ramificações posteriores à direita e nenhuma ramificação se cruzando. Muitos daqueles versados ​​em compiladores e teoria dos grafos têm defendido permitir apenas gráficos de fluxo redutíveis .

Os teóricos da programação estruturada ganharam um grande aliado na década de 1970, depois que o pesquisador da IBM Harlan Mills aplicou sua interpretação da teoria da programação estruturada ao desenvolvimento de um sistema de indexação para o arquivo de pesquisa do The New York Times . O projeto foi um grande sucesso de engenharia, e os gerentes de outras empresas o citaram em apoio à adoção da programação estruturada, embora Dijkstra tenha criticado as maneiras pelas quais a interpretação de Mills difere do trabalho publicado.

Em 1987 ainda era possível levantar a questão da programação estruturada em um periódico de ciência da computação. Frank Rubin o fez naquele ano com uma carta aberta intitulada "'GOTO Considered Harmful' Considered Harmful". Numerosas objeções se seguiram, incluindo uma resposta de Dijkstra que criticou duramente Rubin e as concessões que outros escritores fizeram ao responder a ele.

Resultado

No final do século 20, quase todos os cientistas da computação estavam convencidos de que é útil aprender e aplicar os conceitos de programação estruturada. Linguagens de programação de alto nível que originalmente não tinham estruturas de programação, como FORTRAN , COBOL e BASIC , agora as têm.

Desvios comuns

Embora goto tenha agora sido amplamente substituído pelas construções estruturadas de seleção (if / then / else) e repetição (while e for), poucas linguagens são puramente estruturadas. O desvio mais comum, encontrado em muitas linguagens, é o uso de uma instrução de retorno para saída antecipada de uma sub-rotina. Isso resulta em vários pontos de saída, em vez do único ponto de saída exigido pela programação estruturada. Existem outras construções para lidar com casos que são estranhos na programação puramente estruturada.

Saída antecipada

O desvio mais comum da programação estruturada é a saída antecipada de uma função ou loop. No nível das funções, esta é uma returndeclaração. No nível dos loops, esta é uma breakinstrução (encerrar o loop) ou continueinstrução (encerrar a iteração atual, prosseguir com a próxima iteração). Na programação estruturada, eles podem ser replicados adicionando ramificações ou testes adicionais, mas para retornos de código aninhado, isso pode adicionar uma complexidade significativa. C é um exemplo inicial e proeminente dessas construções. Algumas linguagens mais novas também têm "interrupções rotuladas", que permitem quebrar mais do que apenas o loop mais interno. As exceções também permitem a saída antecipada, mas têm consequências adicionais e, portanto, são tratadas a seguir.

Múltiplas saídas podem surgir por uma variedade de razões, na maioria das vezes porque a sub-rotina não tem mais trabalho a fazer (se retornar um valor, ela concluiu o cálculo) ou encontrou circunstâncias "excepcionais" que a impedem de continuar, portanto, precisa manipulação de exceção.

O problema mais comum na saída antecipada é que as instruções de limpeza ou finais não são executadas - por exemplo, a memória alocada não é desalocada ou os arquivos abertos não são fechados, causando vazamentos de memória ou de recursos . Isso deve ser feito em cada site de retorno, que é frágil e pode facilmente resultar em bugs. Por exemplo, em um desenvolvimento posterior, uma instrução de retorno pode ser negligenciada por um desenvolvedor e uma ação que deve ser executada no final de uma sub-rotina (por exemplo, uma instrução de rastreamento ) pode não ser executada em todos os casos. Linguagens sem uma instrução de retorno, como Pascal padrão e Seed7 , não têm esse problema.

A maioria das linguagens modernas fornece suporte em nível de linguagem para evitar tais vazamentos; veja a discussão detalhada em gerenciamento de recursos . Mais comumente, isso é feito por meio de proteção de desenrolamento, que garante que determinado código seja executado quando a execução sair de um bloco; esta é uma alternativa estruturada para ter um bloco de limpeza e um goto. Geralmente, isso é conhecido try...finally,e considerado parte do tratamento de exceções . No caso de várias returninstruções, a introdução try...finally,sem exceções pode parecer estranha. Existem várias técnicas para encapsular o gerenciamento de recursos. Uma abordagem alternativa, encontrada principalmente em C ++, é Resource Acquisition Is Initialization , que usa o desenrolamento normal da pilha (desalocação de variável) na saída da função para chamar destruidores em variáveis ​​locais para desalocar recursos.

Kent Beck , Martin Fowler e co-autores argumentaram em seus livros de refatoração que condicionais aninhados podem ser mais difíceis de entender do que um certo tipo de estrutura mais plana usando saídas múltiplas predicadas por cláusulas de guarda . Seu livro de 2009 afirma categoricamente que "um ponto de saída não é realmente uma regra útil. Clareza é o princípio-chave: se o método é mais claro com um ponto de saída, use um ponto de saída; do contrário, não". Eles oferecem uma solução de livro de receitas para transformar uma função que consiste apenas em condicionais aninhadas em uma sequência de instruções de retorno (ou lançamento) protegidas, seguidas por um único bloco desprotegido, que se destina a conter o código para o caso comum, enquanto as instruções protegidas são supostamente para lidar com os menos comuns (ou com erros). Herb Sutter e Andrei Alexandrescu também argumentam em seu livro de dicas de C ++ de 2004 que o ponto de saída única é um requisito obsoleto.

Em seu livro de 2004, David Watt escreve que "fluxos de controle de múltiplas saídas de entrada única são freqüentemente desejáveis". Usando a noção de sequenciador da estrutura de Tennent , Watt descreve uniformemente as construções de fluxo de controle encontradas nas linguagens de programação contemporâneas e tenta explicar por que certos tipos de sequenciadores são preferíveis a outros no contexto de fluxos de controle de múltiplas saídas. Watt escreve que os gotos irrestritos (sequenciadores de salto) são ruins porque o destino do salto não é autoexplicativo para o leitor de um programa até que o leitor encontre e examine o rótulo ou endereço real que é o alvo do salto. Em contraste, Watt argumenta que a intenção conceitual de um sequenciador de retorno é clara em seu próprio contexto, sem ter que examinar seu destino. Watt escreve que uma classe de sequenciadores conhecidos como sequenciadores de escape , definidos como um "sequenciador que termina a execução de um comando ou procedimento envolvente", engloba as quebras de loops (incluindo quebras de vários níveis) e instruções de retorno. Watt também observa que, embora os sequenciadores de salto (gotos) tenham sido um tanto restritos em linguagens como C, onde o alvo deve ser um bloco interno ou um bloco externo abrangente, essa restrição por si só não é suficiente para tornar a intenção de gotos em C self -descrevendo e assim eles ainda podem produzir " código espaguete ". Watt também examina como os sequenciadores de exceção diferem dos sequenciadores de escape e salto; isso é explicado na próxima seção deste artigo.

Em contraste com o que foi dito acima, Bertrand Meyer escreveu em seu livro de 2009 que instruções como breake continue"são apenas as velhas gotoem pele de cordeiro" e desaconselhou fortemente seu uso.

Manipulação de exceção

Com base no erro de codificação do desastre do Ariane 501 , o desenvolvedor de software Jim Bonang argumenta que quaisquer exceções lançadas de uma função violam o paradigma de saída única e propõe que todas as exceções entre procedimentos devem ser proibidas. Bonang propõe que todos os C ++ em conformidade de saída única devem ser escritos ao longo das linhas de:

bool MyCheck1() throw() {
  bool success = false;
  try {
    // Do something that may throw exceptions.
    if (!MyCheck2()) {
      throw SomeInternalException();
    }
    // Other code similar to the above.
    success = true;
  } catch (...) {
    // All exceptions caught and logged.
  }
  return success;
}

Peter Ritchie também observa que, em princípio, mesmo um único throwdireito antes de returnem uma função constitui uma violação do princípio de saída única, mas argumenta que as regras de Dijkstra foram escritas em um tempo antes de o tratamento de exceções se tornar um paradigma em linguagens de programação, então ele propõe permitir qualquer número de pontos de lançamento além de um único ponto de retorno. Ele observa que as soluções que envolvem exceções com o objetivo de criar uma saída única têm maior profundidade de aninhamento e, portanto, são mais difíceis de compreender, e até acusa aqueles que propõem aplicar tais soluções a linguagens de programação que suportam exceções de envolvimento no pensamento de culto à carga .

David Watt também analisa o tratamento de exceções na estrutura de sequenciadores (apresentado neste artigo na seção anterior sobre saídas iniciais). Watt observa que uma situação anormal (geralmente exemplificada com estouros aritméticos ou falhas de entrada / saída como arquivo não encontrado) é um tipo de erro que "é detectado em alguma unidade de programa de baixo nível, mas [para o qual] um manipulador está mais naturalmente localizado em uma unidade de programa de alto nível". Por exemplo, um programa pode conter várias chamadas para ler arquivos, mas a ação a ser executada quando um arquivo não é encontrado depende do significado (propósito) do arquivo em questão para o programa e, portanto, uma rotina de tratamento para esta situação anormal não pode ser localizado no código do sistema de baixo nível. Watts observa ainda que a introdução de teste de sinalizadores de status no chamador, como programação estruturada de saída única ou mesmo sequenciadores de retorno (multissaia), resulta em uma situação em que "o código do aplicativo tende a ficar confuso por testes de sinalizadores de status" e que "o programador pode esquecer ou preguiçosamente omitir o teste de um sinalizador de status. Na verdade, situações anormais representadas por sinalizadores de status são ignoradas por padrão!" Ele observa que, em contraste com o teste de sinalizadores de status, as exceções têm o comportamento padrão oposto , fazendo com que o programa seja encerrado, a menos que o programador lide explicitamente com a exceção de alguma forma, possivelmente adicionando código para ignorá-la intencionalmente. Com base nesses argumentos, Watt conclui que sequenciadores de salto ou sequenciadores de escape (discutidos na seção anterior) não são tão adequados quanto um sequenciador de exceção dedicado com a semântica discutida acima.

O livro de Louden e Lambert enfatiza que o tratamento de exceções difere de construções de programação estruturada como whileloops porque a transferência de controle "é configurada em um ponto diferente no programa daquele onde a transferência real ocorre. No ponto onde a transferência realmente ocorre , pode não haver indicação sintática de que o controle será de fato transferido. " O professor de ciência da computação Arvind Kumar Bansal também observa que em linguagens que implementam tratamento de exceções, mesmo estruturas de controle como for, que têm a propriedade de saída única na ausência de exceções, não a possuem mais na presença de exceções, porque uma exceção pode causar prematuramente um saída em qualquer parte da estrutura de controle; por exemplo, se init()lançar uma exceção for (init(); check(); increm()), o ponto de saída usual após check () não será alcançado. Citando vários estudos anteriores de outros (1999-2004) e seus próprios resultados, Westley Weimer e George Necula escreveram que um problema significativo com exceções é que elas "criam caminhos de fluxo de controle ocultos que são difíceis para os programadores raciocinarem sobre".

A necessidade de limitar o código a pontos de saída única aparece em alguns ambientes de programação contemporâneos voltados para a computação paralela, como o OpenMP . As várias construções paralelas do OpenMP, como parallel do, não permitem saídas antecipadas de dentro para fora da construção paralela; essa restrição inclui todos os tipos de saídas, desde as breakexceções C ++, mas todas elas são permitidas dentro da construção paralela se o alvo de salto também estiver dentro dela.

Múltipla entrada

Mais raramente, os subprogramas permitem várias entradas. Isso é mais comumente apenas reentrada em uma co - rotina (ou gerador / semicorotina), onde um subprograma produz controle (e possivelmente um valor), mas pode então ser retomado de onde parou. Existem vários usos comuns dessa programação, principalmente para fluxos (particularmente entrada / saída), máquinas de estado e simultaneidade. Do ponto de vista da execução do código, o rendimento de uma co-rotina é mais próximo da programação estruturada do que o retorno de uma sub-rotina, já que o subprograma não terminou de fato e continuará quando chamado novamente - não é uma saída antecipada. No entanto, co-rotinas significam que vários subprogramas têm estado de execução - em vez de uma única pilha de chamadas de sub-rotinas - e, portanto, introduzem uma forma diferente de complexidade.

É muito raro que os subprogramas permitam a entrada em uma posição arbitrária no subprograma, pois, neste caso, o estado do programa (como valores de variáveis) não é inicializado ou é ambíguo, e isso é muito semelhante a um goto.

Máquinas de estado

Alguns programas, particularmente analisadores e protocolos de comunicação , têm vários estados que se sucedem de uma maneira que não é facilmente reduzida às estruturas básicas, e alguns programadores implementam as mudanças de estado com um salto para o novo estado. Este tipo de troca de estado é freqüentemente usado no kernel Linux.

No entanto, é possível estruturar esses sistemas tornando cada mudança de estado um subprograma separado e usando uma variável para indicar o estado ativo (ver trampolim ). Alternativamente, eles podem ser implementados por meio de corrotinas, que dispensam o trampolim.

Veja também

Referências

Citações

Fontes

links externos

  • BPStruct - Uma ferramenta para estruturar sistemas concorrentes (programas, modelos de processo)
  • J. Darlinton; M. Ghanem; HW To (1993), "Structured Parallel Programming", In Programming Models for Massively Parallel Computers. IEEE Computer Society Press. 1993 : 160–169, CiteSeerX  10.1.1.37.4610