Passei muito tempo lendo livros diferentes sobre "bom design", "padrões de design" etc. Eu sou um grande fã da abordagem SOLID e sempre que preciso escrever um código simples, penso em o futuro. Portanto, se implementar um novo recurso ou uma correção de bug exigir apenas a adição de três linhas de código como esta:
if(xxx) {
doSomething();
}
Isso não significa que farei dessa maneira. Se eu sinto que esse pedaço de código provavelmente se tornará maior no futuro próximo, pensarei em adicionar abstrações, mover essa funcionalidade para outro lugar e assim por diante. O objetivo que estou buscando é manter a complexidade média como antes das minhas alterações.
Acredito que, do ponto de vista do código, é uma boa idéia - meu código nunca é longo o suficiente e é muito fácil entender os significados para diferentes entidades, como classes, métodos e relações entre classes e objetos.
O problema é que leva muito tempo, e muitas vezes sinto que seria melhor se eu apenas implementasse esse recurso "como está". Trata-se de "três linhas de código" versus "nova interface + duas classes para implementar essa interface".
Do ponto de vista do produto (quando estamos falando sobre o resultado ), as coisas que faço são completamente sem sentido. Sei que, se vamos trabalhar na próxima versão, ter um bom código é realmente ótimo. Por outro lado, o tempo que você gastou para tornar seu código "bom" pode ter sido gasto na implementação de alguns recursos úteis.
Muitas vezes me sinto muito insatisfeito com meus resultados - um código bom que só pode fazer A é pior que um código ruim que pode fazer A, B, C e D.
É provável que essa abordagem resulte em um ganho líquido positivo para um projeto de software ou é uma perda de tempo?
fonte
Respostas:
Isso me cheira a generalidade especulativa . Sem saber (ou pelo menos ter certeza razoável) de que seus clientes precisarão dos recursos B, C e D, você apenas complicará desnecessariamente o design. Código mais complexo é mais difícil de entender e manter a longo prazo. A complexidade extra é justificada apenas por recursos extras úteis . Mas normalmente somos muito ruins em prever o futuro. A maioria dos recursos que pensamos pode ser necessário no futuro nunca vai ser solicitada na vida real.
Assim:
Um código bom que só pode executar A (mas está fazendo isso de maneira simples e limpa) é MELHOR que um código ruim que pode executar A, B, C, D (alguns dos quais podem ser necessários em algum momento no futuro).
fonte
Hora da anedota:
Eu tive dois desenvolvedores trabalhando para mim, que se inclinaram para o excesso de engenharia dessa maneira.
Para um deles, isso basicamente paralisou sua produtividade, especialmente ao iniciar um novo projeto. Especialmente se o projeto fosse, por natureza, bastante simples. Em última análise, um software que funciona agora é o que precisamos. Isso ficou tão ruim que eu tive que deixá-lo ir.
O outro desenvolvedor que era propenso ao excesso de engenharia compensou isso por ser extremamente produtivo. Como tal, apesar de todo o código estranho, ele ainda entregava mais rápido que a maioria. No entanto, agora que ele seguiu em frente, muitas vezes me vejo irritado com a quantidade de trabalho extra necessário para adicionar funcionalidade à medida que você precisa modificar as camadas de abstração (totalmente desnecessárias) etc.
Portanto, a moral é que o excesso de engenharia consumirá um tempo extra que poderia ser melhor gasto em coisas úteis. E não apenas seu tempo, mas também aqueles que precisam trabalhar com seu código.
Então não.
Você deseja que seu código seja o mais simples possível (e não mais simples). Lidar com 'talvez' não está tornando as coisas mais simples, se você errar, terá tornado o código mais complexo, sem nenhum ganho real para mostrar.
fonte
Os princípios SOLID e KISS / YAGNI são opostos quase polares. Alguém dirá que, se doSomething () não puder ser justificado como parte integrante do trabalho que a classe que está chamando, ela deverá estar em uma classe diferente que seja fracamente acoplada e injetada. O outro dirá que, se esse é o único lugar em que algo é usado, até a extração para um método pode ter sido um exagero.
É isso que faz bons programadores valerem seu peso em ouro. A estrutura "adequada" é uma base caso a caso, exigindo conhecimento da base de código atual, o caminho futuro do programa e as necessidades dos negócios por trás do programa.
Eu gosto de seguir essa metodologia simples de três etapas.
Basicamente, é assim que você combina o KISS com o SOLID. Quando você escreve uma linha de código pela primeira vez, pelo que você sabe, será uma ocorrência única; simplesmente tem que funcionar e ninguém se importa com isso, então não fique chique. Na segunda vez em que você coloca o cursor nessa linha de código, você refuta sua hipótese original; Ao revisitar esse código, é provável que você o estenda ou conecte a ele de outro lugar. Agora, você deve limpar um pouco; extrair métodos para petiscos comumente usados, reduzir ou eliminar a codificação de copiar / colar, adicionar alguns comentários, etc. Na terceira vez em que você voltar a esse código, agora é uma interseção importante dos caminhos de execução do seu programa. Agora você deve tratá-lo como uma parte essencial do seu programa e aplicar as regras do SOLID sempre que possível.
Exemplo: você precisa escrever uma simples linha de texto no console. A primeira vez que isso acontece, Console.WriteLine () está bem. Depois, você volta a esse código após novos requisitos também exigirem a gravação da mesma linha em um log de saída. Neste exemplo simples, pode não haver muito código repetitivo de "copiar / colar" (ou talvez exista), mas você ainda pode fazer uma limpeza básica, talvez extrair um método ou dois para evitar a inserção de operações de E / S em uma lógica de negócios mais profunda . Em seguida, você volta quando o cliente deseja a mesma saída de texto em um pipe nomeado para um servidor de monitoramento. Agora, essa rotina de saída é um grande negócio; você está transmitindo o mesmo texto de três maneiras. Este é o exemplo de um algoritmo que se beneficiaria de um padrão composto; desenvolva uma interface IWriter simples com um método Write (), implemente essa interface para criar as classes ConsoleWriter, FileWriter e NamedPipeWriter e mais uma vez para criar uma classe composta "MultiWriter", exponha uma dependência do IWriter em sua classe, configure o composto MultiWriter com os três gravadores reais e conecte-o. Agora, é bastante sólido; a partir deste momento, sempre que os requisitos determinarem que a saída seja nova em qualquer lugar, basta criar um novo IWriter e conectá-lo ao MultiWriter, sem a necessidade de tocar em nenhum código de trabalho existente.
fonte
1) Tenha um código que faça apenas o que deve ser feito.
2) Se você planeja seu código para executar A, B, C e D, o cliente solicitará E.
Seu código deve fazer o que é suposto fazer, você não deve pensar agora em implementações futuras, porque você nunca terminará de alterar seu código continuamente e, mais importante, irá sobrescrever seu código. Você deve refatorar seu código assim que achar necessário, devido aos recursos atuais, sem se esforçar para prepará-lo para algo que ainda não será feito, a menos que você o planeje como parte da arquitetura do projeto.
Sugiro que você leia um bom livro: O Programador Pragmático . Isso abrirá sua mente e ensinará o que você deve fazer e o que não deve fazer.
O Code Complete também é um ótimo recurso, cheio de informações úteis que todo desenvolvedor (não apenas programador) deve ler.
fonte
Talvez aqui esteja o problema.
Nos estágios iniciais, você não tem idéia do que seria o produto final. Ou, se você tiver, está errado. Com certeza. É como um garoto de 14 anos que perguntou há alguns dias aos programadores. Se ele deve, para sua futura carreira, escolher entre aplicativos da web e não me lembro mais o que mais: é óbvio que em poucos anos as coisas ele gosta vai mudar, ele vai se interessar por outros domínios, etc.
Se, para escrever três linhas de código, você cria uma nova interface e duas classes, está com excesso de engenharia. Você obterá um código que será difícil de manter e difícil de ler , apenas porque para cada linha de código útil, você tem duas linhas de código que não precisa. Sem contar documentação XML, testes de unidade, etc.
Pense nisso: se eu quiser saber como um recurso é implementado no seu código, seria mais fácil ler através de vinte linhas de código ou seria mais rápido e fácil abrir dezenas de classes semi-vazias e interfaces para descobrir qual deles usa quais, como eles estão relacionados etc.?
Lembre-se: maior base de código significa mais manutenção. Não escreva mais código quando puder evitá-lo.
Sua abordagem também é prejudicial por outros lados:
Se você precisar remover um recurso, não é mais fácil descobrir onde um método específico de vinte linhas é usado do que desperdiçar seu tempo para entender as dependências entre dezenas de classes?
Ao depurar, não é mais fácil rastrear uma pilha pequena ou você prefere ler dezenas de linhas para descobrir o que está sendo chamado pelo quê?
Para concluir, parece semelhante à otimização prematura . Você está tentando resolver o problema sem ao menos ter certeza se existe um problema em primeiro lugar e onde está. Ao trabalhar na versão 1 do seu produto, implemente os recursos que você precisa implementar agora; não pense em algo que você espera implementar em dois anos na versão 14.
fonte
Escrever muito código que (provavelmente) nunca será usado é uma maneira muito boa de obter um P45 . Você não tem uma bola de cristal e não tem idéia da direção final que o desenvolvimento levará; portanto, gastar tempo nessas funções custa apenas dinheiro sem retorno.
fonte
Tentar prever o que pode ser necessário a partir do código no futuro geralmente acaba sendo desnecessário excesso de engenharia (um hábito que atualmente estou tentando abandonar). Eu diria apenas faça as três linhas. Quando surgir a necessidade (e não antes), refatorar. Dessa forma, seu código sempre faz o que precisa sem ser muito complicado e desenvolve uma boa estrutura naturalmente através da refatoração.
fonte
Costumo dizer que a codificação é como o lado claro / lado escuro da Força - o lado "claro" exige mais esforço, mas produz melhores resultados. O lado "sombrio" é rápido e fácil e oferece maiores benefícios imediatamente, mas corrompe você no caminho. Depois de começar o caminho sombrio, para sempre ele dominará seu destino.
Eu me deparo com isso o tempo todo, em todos os empregos que já tive, é como uma maldição da qual não posso escapar. A cultura da empresa é sempre o caminho do lado sombrio, e correções / correções rápidas para promover novos recursos, e meus pedidos e pedidos de refatoração e gravação de código caem adequadamente em ouvidos surdos, se não levar à minha rescisão por " tentando mudar as coisas "(nenhuma brincadeira que tive isso aconteceu várias vezes, porque eu queria introduzir padrões de design e me afastar dos hacks rápidos).
A triste verdade é que muitas vezes a maneira idiota / obscura é a maneira que você tem que pisar, você só precisa ter certeza de pisar levemente. Lenta e tristemente, percebi que os programadores que entendem a maneira correta de escrever software, ou seja, seguir o SOLID, usar padrões de design, obedecer ao SoC etc. são muito menos comuns do que os hackers sem noção que escreverão uma
if
declaração para corrigir um bug, e quando mais bugs surgirem, apenas adicione essa declaração em vez de pensar "Talvez haja uma maneira melhor de fazer isso" e refatorar o código para ser adequadamente extensível.fonte
if
é muito mais fácil de manter do queIAbstractBugFixer
de umIAbstractBugFixerFactory
. Quando e se você adicionar um segundoif
, é hora de refatorar. Os padrões de design e o SOLID são muito importantes durante a fase de arquitetura, mas não quando o produto já está em execução e está escrito em um estilo que todos os membros da equipe concordaram.Estar ciente do que pode acontecer (futuro) NUNCA é ruim. Pensar no que pode ser uma maneira melhor de fazer algo faz parte do que o torna bom em seu trabalho. A parte mais difícil é determinar se o tempo gasto: taxa de retorno é justificado. Todos nós já vimos situações em que as pessoas fazem o "fácil se" para interromper o sangramento imediato (e / ou gritar) e que, à medida que se somam, você obtém um código confuso. Muitos de nós também experimentamos abstração exagerada que é um mistério quando o codificador original segue em frente, o que também produz código confuso.
Eu examinaria sua situação e faria estas perguntas:
Esse código é essencial para a missão e será significativamente mais estável se eu re-codificar? Na fala da cirurgia, essa refatoração salva a vida ou é meramente eletiva e cosmética?
Estou pensando em refatorar o código que substituiremos em 6 meses?
Estou disposto a dedicar tanto tempo para documentar o design e meu raciocínio para futuros desenvolvedores quanto gastar na refatoração?
Em relação ao meu design elegante para adicionar recursos futuros, é este código que os usuários solicitam alterações a cada semana ou é a primeira vez que o toquei este ano?
Há momentos em que YAGNI e KISS vencem o dia, mas há dias em que uma mudança fundamental fará com que você saia da espiral descendente para a porcaria. Desde que você seja realista em sua avaliação, não apenas do que deseja, mas do que os outros terão que fazer para manter seu trabalho, você poderá determinar melhor qual situação é qual. Ah, e não se esqueça de anotar o que você fez e por quê. Isso salvará aqueles que o seguem, mas também a si mesmo quando precisar voltar mais tarde.
fonte
Na segunda edição do Stroustrups 'A linguagem de programação C ++', (não tenho a página disponível), li
e fui bem seguindo o conselho. É claro que existem trocas, e você precisa encontrar um equilíbrio, mas fragmentos curtos são mais testáveis do que uma grande bagunça de espaguete.
Frequentemente, experimentei que, ao diferenciar de um caso para dois casos, se você pensa em 2 como n casos, abre uma porta para muitas novas possibilidades, nas quais talvez não tenha pensado.
Mas então você tem que fazer a pergunta YAGNI: Vale a pena? Será realmente útil? Ser experiente significa que você raramente estará errado e, como iniciante, mais frequentemente errado.
Você deve ser crítico o suficiente para reconhecer um padrão e detectar se seu código é difícil de manter, devido a muita abstração ou difícil de manter, porque tudo está resolvido no local.
A solução não é isso ou aquilo, mas pensar sobre isso. :)
fonte
"código bom que só pode executar A é pior que código ruim que pode executar A, B, C e D."
Isso pode fazer algum sentido no desenvolvimento de produtos; Mas a maioria dos profissionais de TI trabalha em 'projetos' em vez de desenvolver produtos.
Em 'Projetos de TI', se você programar um bom componente, ele funcionará sem problemas durante o tempo de vida do projeto - que pode não durar mais de 5 ou 10 anos até então, o cenário de negócios pode estar obsoleto e um novo projeto / ERP produto pode ter substituído. Durante esse período de vida de 5/10 anos, a menos que haja defeitos no seu código, ninguém notará sua existência e os méritos de seus melhores pensamentos passam despercebidos! (a menos que você seja bom em apostar sua própria trombeta alto!)
Poucos têm a oportunidade de programar o 'Windows Ctl + Alt + Del' e poucos conseguem essa chance, talvez não percebam o potencial futuro de seu código!
fonte
Muitos livros sobre desenvolvimento enxuto e / ou ágil ajudarão a reforçar essa abordagem: faça o que é necessário agora. Se você sabe que está construindo uma estrutura, adicione abstrações. Caso contrário, não adicione complexidade até precisar. Eu recomendo o Lean Software Development , que apresentará muitos outros conceitos que podem fazer uma diferença substancial na produtividade.
fonte
É engraçado como as pessoas falam sobre o jeito certo / errado de fazer as coisas. Ao mesmo tempo, a tarefa de programar ainda é dolorosamente difícil e não oferece boas soluções para a criação de grandes sistemas complexos.
Pode ser que algum dia nós, programadores, finalmente descubramos como escrever software complexo. Até lá, sugiro que você sempre comece primeiro com a implementação do protótipo "estúpido" e depois gaste tempo suficiente apenas na refatoração para que suas faculdades possam seguir seu código.
fonte
Tendo visto projetos prematuramente generalizados que não se encaixavam nos requisitos reais que vieram mais tarde, criei uma regra para mim:
Para requisitos hipotéticos, apenas escreva código hipotético.
Ou seja: é aconselhável pensar em mudanças que possam ocorrer mais tarde. Mas use essas informações apenas para escolher um design para o código que possa ser facilmente alterado e refatorado se esses requisitos realmente surgirem. Você pode até escrever algum código em sua cabeça (código hipotético) que gostaria de escrever nesse caso, mas não escreva nenhum código real!
fonte
Penso que a mentalidade que o ajudará é sempre buscar soluções concretas para problemas de codificação, em vez de soluções abstratas. As abstrações só devem ser adicionadas quando elas realmente ajudarem a simplificar a base de código (quando, por exemplo, elas permitem que você a diminua).
Muitos programadores descobriram que as abstrações surgem quase por conta própria, quando secam o código. Padrões de design e práticas recomendadas ajudam você a encontrar oportunidades para fazer exatamente isso, mas não são objetivos que valham a pena perseguir.
fonte
Eu acho que o excesso de engenharia geralmente parece ter insegurança em escrever código. Todos os princípios e padrões abstratos devem ser tratados como ferramentas para ajudá-lo. O que acontece com frequência é que eles são tratados como padrões, com os quais devemos obedecer.
Eu acredito que um programador está sempre em uma posição melhor para decidir como abstrair do que um axioma.
O resto já foi dito por KeithS
fonte
Pergunte a si mesmo quais são as vantagens de um bom design:
Agora, pergunte-se se a adição de todas essas camadas de abstração realmente adiciona a algum dos pontos mencionados acima. Caso contrário, você está fazendo errado .
Se você conseguir adicionar novos recursos, adicione 3 linhas de código como esta:
Então, por favor, faça-o. Isso indica que seu design anterior foi bom e fácil de adaptar. Somente quando suas classes começarem a crescer além de um certo ponto, use a refatoração para dividir funções e possivelmente extrair novas classes.
Minha regra geral é que os novos recursos devem ser implementados da maneira mais minimalista possível, somente quando algo é grande demais para ser compreendido antecipadamente (digamos, leva mais de um dia ou meio dia para implementar), você pode adicionar um design global aproximado. A partir daí, adicione apenas camadas de abstração quando o tamanho do código aumentar. Você então refatora depois! Depois de um tempo, deve ser natural para você quando você precisar projetar um pedaço um pouco mais ou seguir o caminho rápido. Outra indicação de que algum código pode usar alguma limpeza é quando você o reutiliza. Cada vez que você adiciona um novo recurso ou chama um código antigo em um novo local, é um bom momento para analisar o código antigo e verificar se você pode melhorá-lo um pouco antes de adicioná-lo novamente. Dessa forma, o código quente lentamente se tornará mais limpo, enquanto as partes desinteressantes apodrecem lentamente e não demoram muito tempo.
Se você trabalha assim, nunca projetará nada demais. Pode ser necessário um pouco de disciplina para voltar ao código antigo quando você deseja adicionar algo novo ou deixar um novo código um pouco mais feio do que o desejado, mas você estará trabalhando em direção a algo produtivo, em vez de ter uma engenharia excessiva.
fonte