Que mudanças são grandes demais para serem facilitadas pelo design adequado?

11

Essa é uma pergunta bastante vaga, mas é algo que nunca senti que foi respondido de maneira satisfatória ao ler sobre o design adequado.

Geralmente, ao aprender sobre programação orientada a objetos, abstração, fatoração, etc., o santo graal do design - e a razão pela qual eles sempre afirmam que você está usando as técnicas de desenvolvimento em questão - é que isso tornará seu programa "fácil de mudar" , "manutenível", "flexível" ou qualquer um dos sinônimos usados ​​para expressar um conceito tão produtivo. Marcando a ivars como privada, dividindo o código em vários métodos pequenos e independentes, mantendo as interfaces gerais, você supostamente ganha a capacidade de modificar seu programa com total facilidade e graça.

Para mudanças relativamente pequenas, isso funcionou bem para mim. Alterações nas estruturas de dados internas usadas por uma classe para aumentar o desempenho nunca foram uma grande dificuldade e também não ocorreram alterações na interface com o usuário, independentemente da API, como redesenhar um sistema de entrada de texto ou revisar os gráficos de um elemento de jogo .

Todas essas mudanças parecem inerentemente independentes. Nenhum deles envolve alterações no comportamento ou design do componente do seu programa que está sendo modificado, no que diz respeito ao código externo. Independentemente de você estar escrevendo de maneira processual ou no estilo OO, com funções grandes ou pequenas, essas alterações são fáceis de fazer, mesmo que você tenha apenas um design moderadamente bom.

No entanto, sempre que as mudanças se tornam grandes e complicadas - ou seja, alterações na API - nenhum dos meus preciosos "padrões" é sempre resgatado. A grande mudança continua grande, o código afetado permanece afetado e muitas horas de trabalho para gerar bugs estão à minha frente.

Então, minha pergunta é essa. Quão grande é a mudança que o design adequado afirma ser capaz de facilitar? Existe alguma técnica de design adicional, desconhecida para mim ou que eu não consegui implementar, que realmente torna simples a modificação do som pegajoso ou essa promessa (que ouvi de tantos paradigmas diferentes) é apenas uma boa idéia, completamente desconectado das verdades imutáveis ​​do desenvolvimento de software? Existe uma "ferramenta de alteração" que posso adicionar ao meu cinto de ferramentas?

Especificamente, o problema que estou enfrentando que me levou aos fóruns é o seguinte: estou trabalhando na implementação de uma linguagem de programação interpretada (implementada em D, mas isso não é relevante), e decidi que os argumentos dos meus encerramentos deveriam ser com base em palavras-chave, em vez de posicionais como atualmente. Isso requer a modificação de todo o código existente que chama funções anônimas, o que, felizmente, é bastante pequeno porque estou no início do desenvolvimento da minha linguagem (<2000 linhas), mas seria enorme se eu tivesse tomado essa decisão posteriormente. Em tal situação, existe alguma maneira de que, através da previsão adequada no design, eu possa ter facilitado essa modificação ou algumas (a maioria) mudanças sejam intrinsecamente abrangentes? Estou curioso para saber se isso é de alguma forma uma falha nas minhas próprias habilidades de design - se for, eu

Para ser claro, não sou cético em relação ao POO ou a qualquer outro padrão comumente usado. Para mim, no entanto, suas virtudes estão na escrita original, e não na manutenção, das bases de código. A herança permite abstrair bem padrões repetitivos, o polimorfismo permite separar o código por função compreendida pelo homem (qual classe) e não pelo efeito compreendido pela máquina (qual ramo da switchdeclaração) e funções pequenas e independentes permitem que você escreva em um estilo "de baixo para cima" muito agradável. No entanto, sou cético em relação a suas reivindicações de flexibilidade.

existe-forall
fonte
Não vejo como a mudança no seu idioma afeta nada além do analisador. Você poderia esclarecer isso?
scarfridge
Em uma linguagem do tipo smalltalk, é verdade que você só precisaria modificar o analisador, porque seria uma simples questão de tratar foo metarg1: bar metarg2: bazcomo foo.metarg1_metarg2_(bar, baz). No entanto, meu idioma está fazendo uma alteração maior, ou seja, parâmetros baseados em lista para parâmetros baseados em dicionário, o que afeta o tempo de execução, mas não o analisador, na verdade, devido às propriedades específicas do meu idioma, não abordarei agora. Como não há um mapeamento claro entre palavras-chave não ordenadas e argumentos posicionais, o tempo de execução é a questão principal.
existe-forall

Respostas:

13

Às vezes, uma mudança é grande o suficiente para que você tenha que criar um caminho de migração. Mesmo que os pontos inicial e final sejam bem projetados, muitas vezes você não pode simplesmente mudar de peru frio. Muitos bons designers não conseguem criar bons caminhos de migração.

É por isso que acho que todo programador deve fazer um software de gravação para ambientes de produção 24/7. Há algo sobre as perdas citadas na ordem do seu salário anual por minuto que o motiva a aprender a escrever um código de migração robusto.

O que as pessoas naturalmente fazem para uma grande mudança é arrancar da maneira antiga, colocar da nova maneira, depois passar alguns dias consertando um bazilhão de erros de compilação, até que seu código esteja finalmente compilando novamente para que você possa começar a testar. Como o código não pode ser testado por dois dias, de repente você tem uma enorme bagunça de bugs emaranhados para resolver.

Para uma grande mudança no design, como mudar de argumentos posicionais para palavras-chave, um bom caminho de migração seria adicionar primeiro suporte a argumentos de palavras-chave sem remover o suporte posicional . Em seguida, você altera metodicamente o código de chamada para usar argumentos de palavra-chave. Isso é semelhante a como você fez isso antes, exceto que desta vez você pode testar à medida que avança, porque o código de chamada inalterado ainda funciona. Como suas alterações são menores entre os testes, os bugs são mais fáceis de corrigir anteriormente. Então, quando todo o código de chamada foi alterado, é trivial remover o suporte posicional.

É por isso que as APIs publicadas publicamente preterem os métodos antigos, em vez de removê-los. Ele fornece um caminho de migração contínuo para chamar código. Pode parecer mais trabalho dar suporte a ambos por um tempo, mas você investe o tempo nos testes.

Portanto, para responder à sua pergunta conforme formulada originalmente, quase não há alterações grandes demais para serem facilitadas pelo design adequado. Se sua alteração parecer muito grande, você só precisará criar alguns estágios intermediários temporários para a migração do seu código.

Karl Bielefeldt
fonte
+1 para apoiar o novo e o antigo caso, para que você possa eliminar progressivamente.
precisa
9

Eu recomendo que você leia o ensaio Big Ball of Mud .

Basicamente, o ponto em que o design tende a se deteriorar à medida que você avança no desenvolvimento e precisa dedicar trabalho para conter a complexidade. A complexidade em si não pode ser eliminada, apenas contida, porque é inerente ao problema que você está tentando resolver.

Isso leva aos principais princípios do desenvolvimento ágil. Os dois mais relevantes são "você não vai precisar dele", dizendo para não preparar o design para os recursos que você ainda não implementará, porque é improvável que você consiga acertar o design de qualquer maneira e "refatorar sem piedade", pedindo que você trabalhe em manter a sanidade do código ao longo do projeto.

Parece que a resposta é que nenhum projeto é capaz de facilitar qualquer alteração além dos exemplos planejados, projetados especificamente para mostrar que o projeto é mais forte do que realmente é.

Em uma nota lateral, você deve ser cético em relação à programação orientada a objetos. É uma ótima ferramenta em muitos contextos, mas você precisa estar ciente de seus limites. Especialmente o uso indevido de herança pode levar a um código incrivelmente complicado. Claro que você deve ser igualmente cético em relação a qualquer outra técnica de design. Cada um tem seus usos e cada um tem seus pontos fracos. Não há bala de prata.

Jan Hudec
fonte
1
+1: o novo design inicial raramente é bem-sucedido. Projetos bem-sucedidos são recapitulações de projetos comprovados ou foram iterativamente refinados.
kevin Cline
1
Com +1, você deve ser cético em relação a todos os conceitos de programação, apenas através de críticas objetivas aplicamos nossas habilidades analíticas para encontrar a solução certa, e não apenas uma solução .
21813 Jimmy Hoffa
4

Em termos gerais, há "partes de código" (funções, métodos, objetos, qualquer que seja) e "interfaces entre partes de código" (APIs, declarações de função, qualquer que seja; incluindo o comportamento).

Se um trecho de código puder ser alterado sem alterar a interface da qual outros trechos de código dependem, a alteração será mais fácil (e não importa se você usa OOP ou não).

Se um trecho de código não puder ser alterado sem alterar a interface da qual outros trechos de código dependem, a alteração será mais difícil (e não importa se você usa OOP ou não).

Os principais benefícios do OOP são que as "interfaces" públicas estão claramente marcadas (por exemplo, métodos públicos de um objeto) e outro código não pode usar interfaces internas (por exemplo, métodos particulares do objeto). Como as interfaces públicas são claramente marcadas, as pessoas tendem a ter mais cuidado ao projetar essas interfaces públicas (o que ajuda a evitar mudanças mais difíceis). Como as interfaces internas não podem ser usadas por outro código, você pode alterá-las sem se preocupar com outro código, dependendo delas.

Brendan
fonte
Outra vantagem do OOP é que encapsular os dados com a lógica que opera nele leva a interfaces públicas menores (se bem executadas).
Michael Borgwardt
2

Não existe uma metodologia de design que possa salvá-lo dos aborrecimentos de uma grande alteração de requisitos / escopo. É simplesmente impossível imaginar e explicar todas as possíveis mudanças que poderiam ser pensadas posteriormente.

Onde um bom projeto faz ajudá-lo é para ajudar você a entender como todas as peças se encaixam e que serão afetados pela mudança proposta.
Além disso, um bom design pode limitar as partes afetadas pela maioria das alterações propostas.

Em resumo, todas as alterações são facilitadas por um bom design, mas um bom design não pode transformar uma grande mudança em uma pequena. (um design ruim / inexistente, por outro lado, pode transformar qualquer pequena alteração em grande).

Bart van Ingen Schenau
fonte
1

Como o design tem como objetivo levar um projeto de desenvolvimento de zero nada em branco para um produto final confiável (pelo menos o design de um) e essa é a maior mudança possível em um projeto, duvido que exista algo como uma mudança grande demais para suportar.

Se alguma coisa é o contrário. Algumas mudanças são pequenas demais para se preocupar com qualquer "design" ou pensamento sofisticado. Correções simples de erros, por exemplo.

Lembre-se de que os padrões de design ajudam a pensar com clareza e a encontrar boas soluções de design para situações comuns, como a criação de um grande número de pequenos objetos do mesmo tipo. Nenhuma lista de padrões está completa. Você e todo o resto de nós terão problemas de design que não são comuns, que não cabem em nenhum buraco de pombo. Tentativas de fazer cada pequeno movimento em um processo de desenvolvimento de software, de acordo com um padrão oficial ou outro, é uma maneira excessivamente religiosa de fazer as coisas e leva a bagunças não sustentáveis.

Todo o trabalho em software é naturalmente gerado por erros. Jogadores de futebol se machucam. Músicos têm calos ou espasmos labiais, dependendo do instrumento. (Se você tiver espasmos labiais enquanto toca guitarra, está fazendo algo errado.) Os desenvolvedores de software recebem bugs. Adoramos encontrar maneiras de reduzir a taxa de desova, mas nunca será zero.

DarenW
fonte
3
A posição inicial também pode ser negativa. Não subestime o poder de Legado :)
Maglob
Projetar projeto do zero é a parte mais fácil. Mas mesmo se você começar um novo projeto a partir do zero, o que é raro em si, você só gostará de construir do nada nos primeiros dias. A primeira decisão inicial do projeto parece estar errada e as coisas só começarão a decair a partir daí. Design do zero é totalmente irrelevante na prática.
Jan Hudec
1

O custo de varrer as alterações geralmente é proporcional ao tamanho da base de código. Portanto, a redução do tamanho da base de código sempre deve ser um dos objetivos de qualquer design adequado.

No seu exemplo, o design adequado já facilitou seu trabalho: ter que fazer alterações em 2000 linhas de base de código, em vez de 20000 ou 200000 linhas de base de código.

O design adequado reduz as mudanças radicais, mas não as elimina.

Mudanças radicais são facilmente automatizáveis ​​se houver suporte adequado das ferramentas de análise de linguagem. Uma alteração geral da refatoração pode ser aplicada a toda a base de código em um estilo de busca e substituição (respeitando adequadamente as regras de idioma). O programador só precisa fazer um julgamento sim / não para cada resultado da pesquisa.

rwong
fonte
+1 para sugestão de automação. Eu acho que vou usar isso para muitas das funções da minha biblioteca que chamam de fechamento - é simples detectar quais funções pegam blocos e modificá-las para ter um parâmetro de símbolo extra para o nome do argumento passar o valor como.
existe-forall