A solução deve ser tão genérica quanto possível ou tão específica quanto possível?

122

Digamos que eu tenha uma entidade que tenha o atributo "type". Pode haver mais de 20 tipos possíveis.

Agora me pedem para implementar algo que permita alterar o tipo de A- > B, que é o único caso de uso.

Então, devo implementar algo que permita alterações arbitrárias de tipo, desde que sejam tipos válidos? Ou devo SOMENTE permitir que ele mude de A- > B para o requisito e rejeite qualquer outro tipo de alteração, como B- > A ou A- > C?

Eu posso ver os prós e contras de ambos os lados, onde uma solução genérica significaria menos trabalho no caso de um requisito semelhante surgir no futuro, mas também significaria mais chance de dar errado (embora nós 100% controlemos o chamador neste ponto).
Uma solução específica é menos propensa a erros, mas requer mais trabalho no futuro, se um requisito similar surgir.

Eu continuo ouvindo que um bom desenvolvedor deve tentar antecipar a mudança e projetar o sistema para que seja fácil estendê-lo no futuro, o que soa como uma solução genérica é o caminho a percorrer?

Editar:

Adicionando mais detalhes ao meu exemplo não tão específico: A solução "genérica", neste caso, requer menos trabalho do que a solução "específica", já que a solução específica requer validação no tipo antigo e novo, enquanto a solução genérica precisa apenas validar o novo tipo.

    
por LoveProgramming 30.11.2017 / 19:31
fonte

14 respostas

296

Minha regra básica:

  1. na primeira vez que você encontrar o problema, resolva apenas o problema específico (este é o princípio YAGNI )
  2. na segunda vez que você se deparar com o mesmo problema, considere generalizar o primeiro caso, se não for muito trabalho
  3. uma vez que você tem três casos específicos onde você pode usar a versão generalizada, você deve começar a planejar a versão generalizada - agora, você deve entender o problema o suficiente para realmente generalizá-lo.

É claro que esta é uma diretriz e não uma regra rígida e rápida: a verdadeira resposta é usar seu melhor julgamento, caso a caso.

    
por 30.11.2017 / 19:37
fonte
95

A specific solution [...] requires more work in the future if similar requirement is needed

Eu ouvi esse argumento várias dúzias de vezes, e - para minha experiência - ele regularmente se revela uma falácia. Se você generalizar agora ou mais tarde, quando o segundo requisito semelhante surgir, o trabalho será quase o mesmo no total. Portanto, não há absolutamente nenhum ponto para investir esforço adicional na generalização, quando você não sabe que esse esforço valerá a pena.

(Obviamente isto não se aplica quando uma solução mais geral é menos complicada e requer menos esforço do que uma específica, mas para minha experiência, estes são casos raros. Tal tipo de cenário foi editado posteriormente na questão, e não é essa a minha resposta é sobre).

Quando o "segundo caso similar" aparecer, então é hora de começar a pensar em generalizar. Será muito mais fácil generalizar corretamente , pois esse segundo requisito fornece um cenário onde você pode validar se você fez as coisas certas genéricas. Ao tentar generalizar apenas para um caso, você está atirando no escuro. As chances são altas de você generalizar certas coisas que não precisam ser generalizadas, e perder outras partes que deveriam. E quando surge um segundo caso e você percebe que generalizou as coisas erradas, então você tem muito mais trabalho para corrigir esse problema.

Então, eu recomendo atrasar qualquer tentação de fazer as coisas "apenas no caso". Essa abordagem só levará a mais esforços de trabalho e manutenção quando você perder a oportunidade de generalizar três, quatro ou mais vezes e, em seguida, terá uma pilha de código de aparência semelhante (duplicado) para manter.

    
por 30.11.2017 / 21:06
fonte
63

TL; DR: depende do que você está tentando resolver.

Eu tive uma conversa semelhante com o meu vovô sobre isso, enquanto estávamos falando sobre como Func e Ação no C # são incríveis. My Gramps é um programador temporizador muito antigo, que tem estado em torno de códigos-fonte desde que o software foi executado em computadores que levaram uma sala inteira.

Ele mudou de tecnologia várias vezes em sua vida. Ele escreveu o código em C, COBOL, Pascal, BASIC, Fortran, Smalltalk, Java e, eventualmente, iniciou o C # como hobby. Eu aprendi a programar com ele, sentado em seu colo enquanto eu era apenas um devling, esculpindo minhas primeiras linhas de código no editor azul do SideKick da IBM. Quando eu tinha 20 anos, eu já havia passado mais tempo codificando do que jogando do lado de fora.

Essas são algumas das minhas lembranças, então me desculpe se não sou exatamente prático ao recontá-las. Eu gosto um pouco desses momentos.

Foi o que ele me disse:

"Devemos ir para a generalização de um problema, ou resolvê-lo no escopo específico, você pergunta? Bem, isso é uma ... pergunta."

Gramps fez uma pausa para pensar sobre isso por um breve momento, enquanto corrigia a posição de seus óculos no rosto. Ele estava jogando um jogo match-3 em seu computador, enquanto ouvia um LP do Deep Purple em seu antigo sistema de som.

"Bem, isso dependeria do problema que você está tentando resolver", ele me disse. "É tentador acreditar que existe uma solução única e sagrada para todas as escolhas de design, mas não existe uma. A arquitetura de software é como o queijo, você vê."

"... queijo, vovô?"

"Não importa o que você pensa sobre o seu favorito, sempre haverá alguém que acha que é fedorento".

Eu pisquei em confusão por um momento, mas antes que eu pudesse dizer qualquer coisa, Gramps continuou.

"Quando você está construindo um carro, como você escolhe o material para uma peça?"

"Eu ... eu acho que depende dos custos envolvidos e do que a peça deve fazer, suponho."

"Depende do problema que a peça está tentando resolver. Você não vai fazer um pneu feito de aço, ou um pára-brisa feito de couro. Você escolhe o material que melhor resolve o problema que você tem em mãos. Agora, o que é uma solução genérica? Ou uma solução específica? Para qual problema, para qual caso de uso? Você deve ir com uma abordagem funcional completa, para dar flexibilidade máxima a um código que será usado apenas uma vez? código frágil para uma parte do seu sistema que vai ver muitos e muitos usos e, possivelmente, muitas mudanças? Opções de design como essas são como os materiais que você escolhe para uma peça em um carro ou a forma do tijolo Lego que você escolhe para construir uma pequena casa. Que tijolo de Lego é o melhor? "

O programador idoso pegou um pequeno modelo de trem Lego que ele tem em sua mesa antes de continuar.

"Você só pode responder que, se você sabe para o que você precisa desse bloco. Como você saberá se a solução específica é melhor que a genérica, ou vice-versa, se você nem sabe qual problema você está tentando resolver? Você não pode ver além de uma escolha que você não entende. "

".. Você acabou de citar The Matrix? "

"O que?"

"Nada, vá em frente."

"Bem, suponha que você esteja tentando criar algo para o Sistema Nacional de Faturas. Você sabe como é essa API infernal e seu arquivo XML de trinta mil linhas de dentro. Como uma solução 'genérica' para criar esse arquivo Seria parecido? O arquivo está cheio de parâmetros opcionais, cheio de casos que apenas ramos muito específicos de negócios devem usar. Para a maioria dos casos, você pode ignorá-los com segurança. Você não precisa criar um sistema de fatura genérico se o único Uma coisa que você vai vender é calçado, basta criar um sistema para vender sapatos e torná-lo o melhor sistema de faturamento de venda de sapatos. Agora, se você tivesse que criar um sistema de faturas para qualquer tipo de cliente, ampla aplicação - para ser revendida como um sistema de vendas independente e genérico, por exemplo - agora é interessante implementar as opções que são usadas apenas para gás, alimentos ou álcool. Agora esses são possíveis casos de uso. Antes de serem apenas alguns casos hipotéticos de Não use , um d você não deseja implementar os casos Não use . Não use é o irmão mais novo de Não precisa . "

Gramps colocar o trem lego de volta em seu lugar e voltou para seu jogo de combinar 3.

"Então, para escolher uma solução genérica ou específica para um determinado problema, primeiro você precisa entender qual é o problema. Caso contrário, você está apenas adivinhando e adivinhando é o trabalho dos gerentes, não dos programadores. Como quase tudo em TI, isso depende ".

Então, você tem isso. "Depende". Essa é provavelmente a expressão de duas palavras mais poderosa quando se pensa em design de software.

    
por 30.11.2017 / 21:01
fonte
14

Primeiramente, você deve tentar antecipar se é provável que tal mudança ocorra - não apenas uma possibilidade remota em algum momento.

Se não, geralmente é melhor optar pela solução simples agora e estendê-la depois. É bem possível que você tenha uma imagem muito mais clara do que é necessário.

    
por 30.11.2017 / 19:38
fonte
13

Se você está trabalhando em domínios novos para você, do que a Regra de três que Daniel Pryden mencionado definitivamente deve ser aplicado. Afinal, como você deve construir abstrações úteis se você é um novato nessa área? As pessoas muitas vezes são autoconfiantes em sua capacidade de capturar abstrações, embora raramente seja o caso. Para minha experiência, a abstração prematura não é menos mal do que a duplicação de código. Abstrações erradas são realmente dolorosas de compreender. Às vezes até mais doloroso para refatorar.

Existe um livro que aborda o meu ponto sobre uma área desconhecida em que o programador está a trabalhar. É composto por domínios específicos com abstrações úteis extraídas.

    
por 30.11.2017 / 20:00
fonte
4

Dada a natureza do corpo de sua pergunta, supondo que eu entendi corretamente, na verdade vejo isso como uma questão de design das características do sistema central mais do que uma questão sobre soluções genéricas vs. específicas.

E quando se trata de recursos e recursos do sistema central, os mais confiáveis são os que não estão lá. Vale a pena errar do lado do minimalismo, especialmente considerando que geralmente é mais fácil adicionar funcionalidade centralmente, há muito tempo desejada, do que remover funcionalidades problemáticas, muito indesejadas, com inúmeras dependências, porque tornou o trabalho com o sistema muito mais difícil do que precisa ser ao levantar questões de design sem fim a cada novo recurso.

Na verdade, faltando uma strong antecipação de se isso será necessário com frequência no futuro, eu procuraria evitar olhar para isso como um substituto de tipo de A para B, se possível, e em vez disso apenas procurá-lo como uma maneira de transformar Estado de A. Por exemplo, defina alguns campos em A para transformá-lo em um tipo diferente de coisa - você poderia potencialmente criar uma loja B privadamente usando funções de composição e chamada em B quando o estado de A está definido para indicar que deve imitar B para simplificar a implementação, se possível. Essa deve ser uma solução muito simples e minimamente invasiva.

De qualquer forma, repetindo muitas outras, eu sugeriria errar do lado de evitar soluções genéricas neste caso, mas mais ainda porque eu suponho que isso seja uma adição de uma capacidade muito ousada ao sistema central, e Eu sugeriria errar do lado de deixar de fora, especialmente por agora.

    
por 30.11.2017 / 21:20
fonte
3

É difícil dar uma resposta genérica para esse problema específico ;-)

Quanto mais genérico for, mais tempo você ganhará para futuras alterações. Por exemplo, por esse motivo, muitos programas de jogos usam o padrão de componente de entidade de construir um sistema de tipo muito elaborado mas rígido dos personagens e objetos no jogo.

Por outro lado, fazer algo genérico requer um investimento antecipado de tempo e esforço no design, que é muito mais alto do que para algo muito específico. Isso implica o risco de excesso de engenharia e até de se perder em possíveis exigências futuras.

Vale sempre a pena ver se existe uma generalização natural que lhe dará um grande avanço. No entanto, no final, é uma questão de equilíbrio, entre o esforço que você pode gastar agora e o esforço que você pode precisar no futuro.

    
por 30.11.2017 / 19:53
fonte
3
  1. híbrido. Isso não precisa ser uma pergunta ou / ou. Você pode projetar a API para conversões de tipo gerais e, ao mesmo tempo, implementar apenas a conversão específica que precisa. (Apenas certifique-se de que, se alguém chamar sua API geral com uma conversão não suportada, ela falhará com um status de erro "não suportado").

  2. Teste. Para a conversão A- > B, terei que escrever um (ou um pequeno número) de testes. Para uma conversão genérica x > y, talvez tenha que escrever uma matriz inteira de testes. Isso é substancialmente mais trabalhoso, mesmo que todas as conversões compartilhem uma única implementação genérica.

    Se, por outro lado, existe uma maneira genérica de testar todas as conversões possíveis, então não há muito mais trabalho e posso estar inclinado a ir para uma solução genérica mais cedo.

  3. Acoplamento. Um conversor de A para B pode precisar necessariamente conhecer detalhes de implementação sobre A e B (acoplamento apertado). Se A e B ainda estão evoluindo, isso significa que eu poderia ter que continuar revisitando o conversor (e seus testes), o que é uma droga, mas pelo menos é limitado a A e B's.

    Se eu tivesse uma solução genérica que precisasse de acesso aos detalhes de todos os tipos, mesmo que os C's e D's evoluíssem, talvez eu precisasse continuar aprimorando o conversor genérico (e um monte de testes), o que pode diminuir me down mesmo que ninguém ainda precise converter para um C ou um D .

    Se tanto as conversões genéricas quanto as conversões específicas puderem ser implementadas de uma maneira que seja apenas fracamente acoplada aos detalhes dos tipos, então eu não me preocuparia com isso. Se um desses pode ser feito de uma maneira fracamente acoplada, mas o outro requer um acoplamento strong, então esse é um strong argumento para a abordagem fracamente acoplada desde o início.

por 30.11.2017 / 23:57
fonte
3

Should the solution be as generic as possible or as specific as possible?

Essa não é uma pergunta respondível.

O melhor que você pode razoavelmente obter é algumas heurísticas para decidir quão geral ou específico é fazer uma determinada solução. Trabalhando com algo como o processo abaixo, geralmente a aproximação de primeira ordem é correta (ou boa o suficiente). Quando não é, o motivo provavelmente é muito específico do domínio para ser abordado detalhadamente aqui.

  1. Aproximação de primeira ordem: a regra YAGNI usual de três, conforme descrito por Daniel Pryden, Doc Brown, et al .

    Esta é uma heurística geralmente útil porque é provavelmente o melhor que você pode fazer que não dependa do domínio e de outras variáveis.

    Então, a suposição inicial é: nós fazemos a coisa mais específica.

  2. Aproximação de segunda ordem: com base no seu conhecimento especializado do domínio da solução , você diz

    The "generic" solution in this case requires less work than the "specific" solution

    para que possamos reinterpretar a YAGNI como recomendando que evitemos trabalho desnecessário, em vez de evitar uma generalidade desnecessária. Então, podemos modificar nossa presunção inicial e, em vez disso, fazer a coisa mais fácil .

    No entanto, se o conhecimento do seu domínio de solução indicar que a solução mais fácil provavelmente abrirá muitos bugs, ou será difícil testar adequadamente ou causar qualquer outro problema, ser mais fácil codificar não é necessariamente uma razão suficiente para mudar nossa escolha original.

  3. Aproximação de terceira ordem: o conhecimento do seu problema de domínio sugere que a solução mais fácil está correta, ou você está permitindo muitas transições que você sabe que são sem sentido ou erradas?

    Se a solução simples, mas genérica, parecer problemática, ou se você não estiver confiante em sua capacidade de avaliar esses riscos, provavelmente será melhor fazer o trabalho extra e ficar com sua estimativa inicial.

  4. Aproximação de quarta ordem: o seu conhecimento do comportamento do cliente, ou como esse recurso está relacionado a outros, ou prioridades de gerenciamento de projetos ou ... quaisquer outras considerações não estritamente técnicas modificam sua decisão de trabalho atual?

por 01.12.2017 / 14:56
fonte
2

Esta não é uma pergunta fácil de responder com uma resposta simples. Muitas respostas deram heurísticas construídas em torno de uma regra de 3, ou algo similar. Ir além das regras gerais é difícil.

Para realmente realmente responder a sua pergunta, você deve considerar que seu trabalho provavelmente não implementará algo que mude A- > B. Se você é um contratado, talvez esse seja o requisito, mas se você é um funcionário, você foi contratado para fazer muitas tarefas menores para a empresa. Alterar A- > B é apenas uma dessas tarefas. Sua empresa vai se preocupar com o quão bem as mudanças futuras podem ser feitas também, mesmo que isso não seja indicado na solicitação. Para encontrar "TheBestImplementation (tm)", você tem que olhar para a imagem maior do que você está realmente sendo solicitado a fazer, e então usar isso para interpretar a pequena requisição que você recebeu para mudar A- > B.

Se você é um programador de entrada de baixo nível recém-saído da faculdade, é sempre aconselhável fazer exatamente o que lhe foi dito para fazer. Se você foi contratado como arquiteto de software com 15 anos de experiência, é aconselhável pensar em grandes detalhes. Todo trabalho real vai cair em algum lugar entre "fazer exatamente o que a tarefa é estreita" e "pensar sobre o quadro geral". Você vai se sentir onde seu trabalho se encaixa nesse espectro, se você conversar com as pessoas o suficiente e fizer o suficiente para elas.

Eu posso dar alguns exemplos concretos onde sua pergunta tem uma resposta definitiva baseada no contexto. Considere o caso em que você está escrevendo um software crítico para segurança. Isso significa que você tem uma equipe de teste para garantir que o produto tenha o desempenho prometido. Algumas dessas equipes de teste precisam testar cada caminho possível por meio do código. Se você conversar com eles, poderá descobrir que, se generalizar o comportamento, adicionará US $ 30.000 aos custos de teste, pois eles terão que testar todos esses caminhos extras. Nesse caso, não adiciona funcionalidade generalizada, mesmo que você tenha que duplicar o trabalho 7 ou 8 vezes por causa disso. Economize o dinheiro da empresa e faça exatamente o que a solicitação afirmou.

Por outro lado, considere que você está criando uma API para permitir que os clientes acessem dados em um programa de banco de dados feito por sua empresa. Um cliente solicita permissão para alterações A- > B. As APIs geralmente têm um aspecto de algemas douradas: quando você adiciona funcionalidade a uma API, normalmente não é necessário remover essa funcionalidade (até o próximo número de versão principal). Muitos de seus clientes podem não estar dispostos a pagar pelo custo de atualização para o próximo número de versão principal, portanto, você pode ficar preso a qualquer solução que escolher por um longo tempo. Neste caso, eu altamente recomendo criar a solução genérica desde o início. Você realmente não quer desenvolver uma API ruim cheia de comportamentos pontuais.

    
por 01.12.2017 / 02:37
fonte
1

Hmm ... não há muito contexto para uma resposta ... ecoando respostas anteriores, "Depende".

De certo modo, você precisa recorrer à sua experiência. Se não for seu, então alguém mais sênior no domínio. Você poderia discutir sobre os critérios de aceitação. Se for algo como 'o usuário deve poder alterar o tipo de' A 'para' B '' versus 'o usuário deve poder alterar o tipo do seu valor atual para qualquer valor alternativo permitido'.

Frequentemente os critérios de aceitação estão sujeitos a interpretação, mas uma boa equipe de QA pode escrever critérios apropriados à tarefa em questão, minimizando a interpretação necessária.

Existem restrições de domínio que não permitem a mudança de "A" para "C" ou qualquer outra opção, mas apenas "A" para "B"? Ou isso é apenas um requisito específico que não é "prospectivo"?

Se o caso geral fosse mais difícil, eu perguntaria antes de iniciar o trabalho, mas no seu caso, se eu pudesse 'prever' que outros pedidos de mudança 'tipo' estariam vindo no futuro, eu ficaria tentado a : a) escreva algo reutilizável para o caso geral e b) envolvê-lo em uma condição que só permite A - > B por enquanto.

Fácil o bastante para verificar o caso atual em testes automatizados e fácil o bastante para abrir outras opções posteriormente se / quando diferentes casos de uso surgirem.

    
por 01.12.2017 / 06:56
fonte
1

Para mim, uma diretriz que estabeleci há algum tempo é: "Para requisitos hipotéticos, só escrevemos código hipotético." Isto é - se você antecipar requisitos adicionais, você deve pensar um pouco sobre como implementá-los e estruturar seu código atual para que ele não bloqueie dessa maneira.

Mas não escreva código real para isso agora - pense um pouco sobre o que você faria. Caso contrário, você normalmente tornará as coisas desnecessariamente complexas, e provavelmente ficará irritado mais tarde quando os requisitos reais forem diferentes do que você esperava.

Quatro seu exemplo: se você tem todos os usos do método convert sob seu controle, você pode chamá-lo convertAToB por enquanto e planeja usar uma refatoração "rename method" no IDE para renomeá-lo se você precisar de mais geral funcionalidade mais tarde. No entanto, se o método de conversão fizer parte de uma API pública, isso poderá ser bem diferente: sendo esse específico, bloquearia a generalização posteriormente, já que é difícil renomear as coisas nesse caso.

    
por 05.12.2017 / 12:46
fonte
0

I keep hearing that a good developer should try to anticipate the change and design the system so that it's easy to extend in the future,

Em princípio, sim. Mas isso não necessariamente leva necessariamente a soluções genéricas.

Existem dois tipos de tópicos no desenvolvimento de software, no que me diz respeito, onde você deve prever futuras alterações:

  • Bibliotecas destinadas a serem usadas por terceiros e
  • arquitetura geral de software.

O primeiro caso é resolvido observando sua coesão / acoplamento, injeção de dependência ou qualquer outra coisa. O segundo caso está em um nível mais abstrato, por exemplo, escolhendo uma arquitetura orientada a serviços em vez de um grande bloco de código monolótico para uma grande aplicação.

No seu caso, você está pedindo uma solução específica para um problema específico, que não tem nenhum impacto previsível no futuro. Neste caso, YAGNI e DRY são boas divisas para filmar:

  • YAGNI (você não precisará) lhe diz para implementar as coisas básicas absolutamente mínimas que você precisa e usa agora . Significa implementar o mínimo que faz com que sua suíte de testes atual mude de vermelho para verde, se você deve usar o desenvolvimento de estilo TDD / BDD / FDD. Nem uma única linha mais.
  • DRY (não repita a si mesmo) significa que, se ocorrer um problema semelhante novamente, então você examinará se precisa de uma solução genérica.

Combinado com outras práticas modernas (como boa cobertura de teste para refatorar com segurança), isso significa que você acaba com um código rápido, simples e escrito que cresce conforme necessário.

which sounds like a generic solution is the way to go?

Não, parece que você deve ter ambientes de programação, idiomas e ferramentas que tornam mais fácil e divertido refatorar quando você precisar. Soluções genéricas não fornecem isso; eles dissociam o aplicativo do domínio real.

Dê uma olhada nas estruturas ORMs ou MVC modernas, por exemplo, Ruby on Rails; no nível do aplicativo, todo o foco está em fazer um trabalho não genérico. As bibliotecas de rails em si são obviamente quase 100% genéricas, mas o código do domínio (sobre o qual sua pergunta se refere) deveria estar fazendo pequenas travessuras nesse aspecto.

    
por 03.12.2017 / 14:11
fonte
0

Uma maneira diferente de pensar sobre o problema é considerar o que faz sentido.

Por exemplo, havia um aplicativo que eu estava desenvolvendo que tinha seções que faziam quase a mesma coisa, mas tinham regras de permissão inconsistentes. Como não havia razão para mantê-los diferentes, quando refatorei essa seção, fiz com que todos eles fizessem as mesmas permissões. O código geral ficou menor, mais simples e a interface mais consistente.

Quando o gerenciamento decidiu permitir o acesso de outras pessoas a um recurso, conseguimos alterá-lo apenas alterando um sinalizador.

Obviamente, faz sentido fazer a conversão de tipo específica. Faz sentido também fazer conversões de tipo adicionais?

Lembre-se de que, se a solução geral for mais rápida de implementar, o caso específico também é fácil. Basta verificar se é a única conversão de tipo que você está permitindo.

Se o aplicativo estiver em uma área altamente regulamentada (um aplicativo médico ou financeiro), tente envolver mais pessoas em seu design.

    
por 03.12.2017 / 15:18
fonte