Como grandes bases de código não OO são gerenciadas?


27

Eu sempre vejo que a abstração é um recurso muito útil que o OO fornece para gerenciar a base de código. Mas como são gerenciadas grandes bases de código não OO? Ou eles acabam se tornando uma " grande bola de lama " eventualmente?

Atualização:
parecia que todo mundo estava pensando que 'abstração' é apenas modularização ou ocultação de dados. Mas IMHO, também significa o uso de 'Classes Abstratas' ou 'Interfaces', que são uma obrigação para injeção de dependência e, portanto, testes. Como as bases de código não OO gerenciam isso? Além disso, além da abstração, o encapsulamento também ajuda muito a gerenciar grandes bases de códigos, pois define e restringe a relação entre dados e funções.

Com C, é muito possível escrever código pseudo-OO. Não sei muito sobre outros idiomas que não sejam OO. Então, é a maneira de gerenciar grandes bases de código C?


6
De uma maneira independente da linguagem, descreva um objeto. O que é, como é modificado, o que deve herdar e o que deve fornecer? O kernel do Linux está cheio de estruturas alocadas com muitos auxiliares e indicadores de função, mas isso provavelmente não satisfaria a definição de objeto orientado para a maioria. No entanto, é um dos melhores exemplos de uma base de código muito bem mantida. Por quê? Porque todo mantenedor do subsistema sabe o que está em sua área de responsabilidade.
Tim Post

De uma maneira independente de idioma, descreva como você vê as bases de código sendo gerenciadas e o que o OO tem a ver com isso.
David Thornley 29/11

@ Tim Post Estou interessado no gerenciamento de código fonte do kernel Linux. Você poderia descrever o sistema mais? Talvez como resposta com um exemplo?
Gulshan #

7
Antigamente, usamos links separados para zombarias e stubs para testes de unidade. Injeção de Dependência é apenas uma técnica entre várias. Compilação condicional é outra.
Macneil

Eu acho que é exagero se referir a grandes bases de código (OO ou não) como "gerenciadas". Seria bom ter uma melhor definição do termo central em sua pergunta.
precisa saber é

Respostas:


43

Você parece pensar que OOP é o único meio de alcançar a abstração.

Enquanto OOP é certamente muito bom em fazer isso, não é o único caminho. Projetos grandes também podem ser mantidos gerenciáveis ​​por modularização intransigente (basta olhar para Perl ou Python, que se destacaram nisso, e linguagens funcionais como ML e Haskell) e usando mecanismos como modelos (em C ++).


27
+1 Além disso, é possível escrever uma "Big Ball of Mud" usando OOP se você não sabe o que está fazendo.
Larry Coleman

E as bases de código C?
precisa

6
@ Gulshan: Muitas grandes bases de código C são OOP. Só porque C não tem classes, não significa que o POO não possa ser alcançado com um pouco de esforço. Além disso, C permite uma boa modularização usando cabeçalhos e o idioma PIMPL. Não é tão confortável ou poderoso quanto os módulos nas linguagens modernas, mas mais uma vez é bom o suficiente.
Konrad Rudolph

9
C permite modularização no nível do arquivo. A interface entra no arquivo .h, as funções publicamente disponíveis no arquivo .c e as variáveis ​​e funções privadas obtêm o staticmodificador de acesso anexado.
David Thornley 29/11

1
@ Konrad: embora eu concorde que o OOP não é a única maneira de fazê-lo, acredito que o OP provavelmente tinha estritamente C em mente, o que não é uma linguagem funcional nem dinâmica. Portanto, duvido que mencionar Perl e Haskell seja útil para ele / ela. Na verdade, acho o seu comentário mais relevante e útil para o OP ( não significa que o POO não possa ser alcançado com um pouco de esforço ); considere adicioná-lo como uma resposta separada, com detalhes adicionais, talvez com suporte em um snippet de código ou em alguns links. Pelo menos ganharia meu voto e, possivelmente, OP. :)
Groo

11

Módulos, funções (externas / internas), sub-rotinas ...

como disse Konrad, OOP não é a única maneira de gerenciar grandes bases de código. Por uma questão de fato, um monte de software foi escrito antes dele (antes de C ++ *).


* E sim, eu sei que C ++ não é o único que suporta OOP, mas de alguma forma foi quando essa abordagem começou a ficar inércia.
Rook

8

O princípio da modularidade não se limita às linguagens orientadas a objetos.


6

Realisticamente, mudanças pouco frequentes (pense nos cálculos de aposentadoria da Seguridade Social) e / ou conhecimento profundamente arraigado porque as pessoas que mantêm esse sistema o fazem há algum tempo (tomada cínica é segurança no emprego).

As melhores soluções são a validação repetível, com o que quero dizer teste automatizado (por exemplo, teste de unidade) e teste humano que segue as etapas prescritas (por exemplo, teste de regressão) "em vez de clicar e ver o que quebra".

Para começar a avançar para algum tipo de teste automatizado com uma base de código existente, recomendo a leitura de Working Factor with Legacy Code , de Michael Feather , que detalha abordagens para levar as bases de código existentes até algum tipo de estrutura de teste repetível OO ou não. Isso leva ao tipo de idéias que outras pessoas responderam, como a modularização, mas o livro descreve a abordagem correta para fazer isso sem quebrar as coisas.


+1 para o livro de Michael Feather. Quando você se sentir deprimido sobre uma grande base de código feio, (re) -interprete it :)
Matthieu

5

Embora a injeção de dependência baseada em interfaces ou classes abstratas seja uma maneira muito agradável de se testar, não é necessário. Não se esqueça que quase qualquer idioma tem um ponteiro de função ou uma avaliação, o que pode fazer qualquer coisa com uma interface ou classe abstrata (o problema é que eles podem fazer mais , incluindo muitas coisas ruins, e que não eles mesmos fornecem metadados). Esse programa pode realmente obter injeção de dependência com esses mecanismos.

Eu descobri que ser rigoroso com os metadados é muito útil. Nas linguagens OO, os relacionamentos entre bits de código são definidos (até certo ponto) pela estrutura da classe, de maneira padronizada o suficiente para ter coisas como uma API de reflexão. Nas linguagens processuais, pode ser útil inventar essas informações você mesmo.

Também descobri que a geração de código é muito mais útil em uma linguagem processual (em comparação com uma linguagem orientada a objetos). Isso garante que os metadados estejam em sincronia com o código (já que são usados ​​para gerá-los) e oferece algo parecido com os pontos de corte da programação orientada a aspectos - um local em que você pode injetar código quando necessário. Às vezes, é a única maneira de fazer a programação DRY em um ambiente que eu possa descobrir.


3

Na verdade, como você descobriu recentemente , as funções de primeira ordem são tudo o que você precisa para a inversão de dependência.

C suporta funções de primeira ordem e até fechamentos até certo ponto . As macros C são um recurso poderoso para programação genérica, se manuseadas com os cuidados necessários.

Está tudo lá. SGLIB é um bom exemplo de como C pode ser usado para escrever código altamente reutilizável. E acredito que há muito mais por aí.


2

Mesmo sem abstração, a maioria dos programas é dividida em seções de algum tipo. Essas seções geralmente se relacionam a tarefas ou atividades específicas e você trabalha naquelas da mesma maneira que trabalharia nos bits mais específicos dos programas abstraídos.

Em projetos de pequeno a médio porte, é realmente mais fácil fazer isso com uma implementação OO purista às vezes.


2

Abstração, classes abstratas, injeção de dependência, encapsulamento, interfaces e assim por diante, não são a única maneira de controlar grandes bases de código; Esta é uma maneira justa e orientada a objetos.

O principal segredo é evitar pensar em POO ao codificar não POO.

Modularidade é a chave em idiomas não OO. Em C, isso é alcançado exatamente como David Thornley mencionou em um comentário:

A interface entra no arquivo .h, as funções publicamente disponíveis no arquivo .c e as variáveis ​​e funções privadas obtêm o modificador de acesso estático anexado.


1

Uma maneira de gerenciar o código é decompô-lo nos seguintes tipos de código, seguindo as linhas da arquitetura MVC (model-view-controller).

  • Manipuladores de entrada - Esse código lida com dispositivos de entrada, como mouse, teclado, porta de rede ou abstrações de nível superior, como eventos do sistema.
  • Manipuladores de saída - Esse código trata do uso de dados para manipular dispositivos externos, como monitores, luzes, portas de rede etc.
  • Modelos - este código lida com a declaração da estrutura dos dados persistentes, regras para validar dados persistentes e salvar dados persistentes no disco (ou outro dispositivo de dados persistentes).
  • Exibições - Este código trata da formatação de dados para atender aos requisitos de vários métodos de exibição, como navegadores da web (HTML / CSS), GUI, linha de comando, formatos de dados do protocolo de comunicação (por exemplo, JSON, XML, ASN.1, etc.).
  • Algoritmos - Esse código transforma repetidamente um conjunto de dados de entrada em um conjunto de dados de saída o mais rápido possível.
  • Controladores - esse código recebe entradas pelos manipuladores de entrada, analisa as entradas usando algoritmos e depois transforma os dados com outros algoritmos combinando opcionalmente entradas com dados persistentes ou apenas transformando as entradas e, opcionalmente, salvando os dados transformados em persistente pelo modelo e, opcionalmente, transformar os dados através do software de exibição para renderizar em um dispositivo de saída.

Esse método de organização de código funciona bem para softwares escritos em qualquer linguagem OO ou não-OO, porque padrões de design comuns geralmente são comuns a cada uma das áreas. Além disso, esses tipos de limites de código geralmente são os mais fracamente acoplados, exceto algoritmos, porque vinculam os formatos de dados das entradas ao modelo e depois às saídas.

As evoluções do sistema geralmente assumem a forma de fazer com que seu software lide com mais tipos de entradas ou mais tipos de saídas, mas os modelos e visualizações são os mesmos e os controladores se comportam de maneira muito semelhante. Ou um sistema pode precisar, com o tempo, suportar mais e mais tipos diferentes de saídas, mesmo que as entradas, modelos, algoritmos sejam os mesmos e os controladores e visualizações sejam semelhantes. Ou um sistema pode ser aumentado para adicionar novos modelos e algoritmos para o mesmo conjunto de entradas, saídas e visualizações semelhantes.

Uma maneira pela qual a programação OO dificulta a organização do código é porque algumas classes estão profundamente ligadas às estruturas de dados persistentes e outras não. Se as estruturas de dados persistentes estão intimamente relacionadas a coisas como relacionamentos 1: N em cascata ou relacionamentos m: n, é muito difícil decidir os limites de classe até que você codifique uma parte significativa e significativa do seu sistema antes de saber que está certo . Qualquer classe vinculada às estruturas de dados persistentes será difícil de evoluir quando o esquema dos dados persistentes mudar. Classes que lidam com algoritmos, formatação e análise têm menos probabilidade de serem vulneráveis ​​a alterações no esquema das estruturas de dados persistentes. O uso de um tipo de organização de código MVC isola melhor as alterações de código mais confusas no código do modelo.


0

Ao trabalhar em idiomas que não possuem estrutura interna e recursos de organização (por exemplo, se não houver namespaces, pacotes, assemblies etc ...) ou onde forem insuficientes para manter uma base de código desse tamanho sob controle, a resposta natural será desenvolver nossas próprias estratégias para organizar o código.

Essa estratégia da organização provavelmente inclui padrões relacionados a onde diferentes arquivos devem ser mantidos, coisas que precisam acontecer antes / depois de certos tipos de operações e convenções de nomenclatura e outros padrões de codificação, além de muito "é assim que é configurado" - não mexa com isso! " digite comentários - que são válidos desde que expliquem o porquê!

Como a estratégia provavelmente acabará sendo adaptada às necessidades específicas do projeto (pessoas, tecnologias, ambiente etc ...), é difícil fornecer uma solução única para o gerenciamento de grandes bases de códigos.

Portanto, acredito que o melhor conselho é abraçar a estratégia específica do projeto e tornar o gerenciamento uma prioridade-chave: documentar a estrutura, por que é assim, os processos para fazer alterações, auditar para garantir que está sendo respeitada, e crucialmente: mude quando precisar mudar.

Estamos familiarizados principalmente com classes e métodos de refatoração, mas com uma grande base de código em um idioma como esse, é a própria estratégia de organização (completa com a documentação) que precisa ser refatorada conforme e quando necessário.

O raciocínio é o mesmo da refatoração: você desenvolverá um bloqueio mental para trabalhar em pequenas partes do sistema, se achar que a organização geral está uma bagunça e, eventualmente, permitirá que ele se deteriore (pelo menos é a minha opinião) isto).

As advertências também são as mesmas: use testes de regressão, verifique se você pode reverter facilmente se a refatoração der errado e projete-o para facilitar a refatoração em primeiro lugar (ou você simplesmente não fará isso!).

Concordo que isso é muito mais complicado do que refatorar o código direto, e é mais difícil validar / ocultar o tempo de gerentes / clientes que podem não entender por que isso precisa ser feito, mas esses também são os tipos de projeto mais propensos à podridão de software causada por projetos inflexíveis de nível superior ...


0

Se você está perguntando sobre o gerenciamento de uma grande base de código, está perguntando como manter sua base de código bem estruturada em um nível relativamente aproximado (bibliotecas / módulos / construção de subsistemas / uso de espaços para nome / documentação correta nos lugares certos) etc.) Os princípios de OO, especialmente 'classes abstratas' ou 'interfaces', são princípios para manter seu código limpo internamente, em um nível muito detalhado. Portanto, as técnicas para manter uma base de código grande gerenciável não diferem para código OO ou não OO.


0

Como é tratado é que você descobre as bordas dos elementos que usa. Por exemplo, os seguintes elementos em C ++ têm uma borda clara e quaisquer dependências fora da borda devem ser cuidadosamente pensadas:

  1. função livre
  2. função membro
  3. classe
  4. objeto
  5. interface
  6. expressão
  7. chamada do construtor / criação de objetos
  8. chamada de função
  9. tipo de parâmetro do modelo

Combinando esses elementos e reconhecendo suas bordas, você pode criar praticamente qualquer estilo de programação que desejar no c ++.

Um exemplo disso é que uma função seria reconhecer que é ruim chamar outras funções de uma função, porque causa dependência; em vez disso, você deve chamar apenas funções membro dos parâmetros da função original.


-1

O maior desafio técnico é o problema do espaço para nome. A ligação parcial pode ser usada para contornar isso. A melhor abordagem é projetar usando padrões de codificação. Caso contrário, todos os símbolos se tornam uma bagunça.


-2

O Emacs é um bom exemplo disso:

Arquitetura Emacs

Componentes do Emacs

Os testes do Emacs Lisp usam skip-unlesse let-bindexecutam dispositivos de detecção e teste de recursos:

Às vezes, não faz sentido executar um teste devido a condições prévias ausentes. Um recurso Emacs necessário pode não estar compilado; a função a ser testada pode chamar um binário externo que pode não estar disponível na máquina de teste, o nome dele. Nesse caso, a macro skip-unlesspode ser usada para ignorar o teste:

 (ert-deftest test-dbus ()
   "A test that checks D-BUS functionality."
   (skip-unless (featurep 'dbusbind))
   ...)

O resultado da execução de um teste não deve depender do estado atual do ambiente, e cada teste deve deixar seu ambiente no mesmo estado em que foi encontrado. Em particular, um teste não deve depender de variáveis ​​ou ganchos de personalização do Emacs, e se precisar fazer alterações no estado do Emacs ou no estado externo ao Emacs (como o sistema de arquivos), deve desfazer essas alterações antes de retornar, independentemente de ter sido aprovado ou reprovado.

Os testes não devem depender do ambiente, porque essas dependências podem tornar o teste frágil ou levar a falhas que ocorrem apenas sob determinadas circunstâncias e são difíceis de reproduzir. Obviamente, o código em teste pode ter configurações que afetam seu comportamento. Nesse caso, é melhor fazer o teste de let-bindtodas essas variáveis ​​de configuração para definir uma configuração específica para a duração do teste. O teste também pode definir várias configurações diferentes e executar o código em teste com cada uma delas.

Como é o SQLite. Aqui está o design:

  1. sqlite3_open () → Abra uma conexão com um banco de dados SQLite novo ou existente. O construtor para o sqlite3.

  2. sqlite3 → O objeto de conexão com o banco de dados. Criado por sqlite3_open () e destruído por sqlite3_close ().

  3. sqlite3_stmt → O objeto de instrução preparado. Criado por sqlite3_prepare () e destruído por sqlite3_finalize ().

  4. sqlite3_prepare () → Compile texto SQL em código de bytes que fará o trabalho de consultar ou atualizar o banco de dados. O construtor para sqlite3_stmt.

  5. sqlite3_bind () → Armazena dados do aplicativo em parâmetros do SQL original.

  6. sqlite3_step () → Avança um sqlite3_stmt para a próxima linha de resultados ou para a conclusão.

  7. sqlite3_column () → Valores da coluna na linha de resultados atual para um sqlite3_stmt.

  8. sqlite3_finalize () → Destruidor para sqlite3_stmt.

  9. sqlite3_exec () → Uma função de wrapper que executa sqlite3_prepare (), sqlite3_step (), sqlite3_column () e sqlite3_finalize () para uma cadeia de caracteres de uma ou mais instruções SQL.

  10. sqlite3_close () → Destruidor para sqlite3.

arquitetura sqlite3

Os componentes Tokenizer, Parser e Code Generator são usados ​​para processar instruções SQL e convertê-las em programas executáveis ​​em uma linguagem de máquina virtual ou código de bytes. Grosso modo, essas três camadas principais implementam sqlite3_prepare_v2 () . O código de bytes gerado pelas três principais camadas é uma instrução preparada. O módulo Máquina Virtual é responsável por executar o código de bytes da instrução SQL. O módulo B-Tree organiza um arquivo de banco de dados em vários armazenamentos de chave / valor com chaves ordenadas e desempenho logarítmico. O módulo Pager é responsável por carregar páginas do arquivo de banco de dados na memória, implementar e controlar transações e criar e manter os arquivos de diário que impedem a corrupção do banco de dados após uma falha ou falha de energia. A interface do sistema operacional é uma abstração fina que fornece um conjunto comum de rotinas para adaptar o SQLite à execução em diferentes sistemas operacionais. Grosso modo, as quatro camadas inferiores implementam sqlite3_step () .

tabela virtual sqlite3

Uma tabela virtual é um objeto registrado com uma conexão de banco de dados SQLite aberta. Da perspectiva de uma instrução SQL, o objeto de tabela virtual se parece com qualquer outra tabela ou exibição. Mas nos bastidores, consultas e atualizações em uma tabela virtual invocam métodos de retorno de chamada do objeto de tabela virtual em vez de ler e gravar no arquivo de banco de dados.

Uma tabela virtual pode representar estruturas de dados na memória. Ou pode representar uma exibição de dados no disco que não está no formato SQLite. Ou o aplicativo pode calcular o conteúdo da tabela virtual sob demanda.

Aqui estão alguns usos existentes e postulados para tabelas virtuais:

Uma interface de pesquisa de texto completo
Índices espaciais usando árvores R
Inspecionar o conteúdo do disco de um arquivo de banco de dados SQLite (a tabela virtual dbstat)
Leia e / ou grave o conteúdo de um arquivo de valor separado por vírgula (CSV)
Acesse o sistema de arquivos do computador host como se fosse uma tabela de banco de dados
Ativando a manipulação SQL de dados em pacotes de estatísticas como R

O SQLite usa uma variedade de técnicas de teste, incluindo:

Três chicotes de teste desenvolvidos de forma independente
100% de cobertura de teste de filial em uma configuração implantada
Milhões e milhões de casos de teste
Testes de falta de memória
Testes de erro de E / S
Testes de colisão e perda de energia
Testes de fuzz
Testes de valor limite
Testes de otimização desativados
Testes de regressão
Testes de banco de dados malformados
Uso extensivo de assert () e verificações em tempo de execução
Análise Valgrind
Verificações de comportamento indefinidas
Lista de verificação

Referências

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.