Estou enfrentando problemas com o que considero muita abstração na base de código (ou pelo menos lidando com isso). A maioria dos métodos na base de código foi abstraída para incluir o pai mais alto A na base de código, mas o filho B desse pai tem um novo atributo que afeta a lógica de alguns desses métodos. O problema é que esses atributos não podem ser verificados nesses métodos porque a entrada é abstraída para A e, é claro, A não possui esse atributo. Se eu tentar criar um novo método para manipular B de maneira diferente, ele será chamado para duplicação de código. A sugestão do meu líder técnico é criar um método compartilhado que aceite parâmetros booleanos, mas o problema é que algumas pessoas veem isso como "fluxo de controle oculto", onde o método compartilhado tem uma lógica que pode não ser aparente para futuros desenvolvedores , e também esse método compartilhado se tornará excessivamente complexo / complicado uma vez se atributos futuros precisarem ser adicionados, mesmo que sejam divididos em métodos compartilhados menores. Isso também aumenta o acoplamento, diminui a coesão e viola o princípio da responsabilidade única, que alguém da minha equipe apontou.
Essencialmente, grande parte da abstração nessa base de código ajuda a reduzir a duplicação de código, mas dificulta a extensão / alteração de métodos quando eles são feitos para obter a abstração mais alta. O que devo fazer em uma situação como esta? Eu estou no centro da culpa, mesmo que todo mundo não possa concordar com o que eles consideram bom, então isso está me machucando no final.
Respostas:
Nem toda duplicação de código é criada da mesma forma.
Digamos que você tenha um método que use dois parâmetros e os adicione juntos, chamados
total()
. Digamos que você tenha outro chamadoadd()
. Suas implementações parecem completamente idênticas. Eles devem ser mesclados em um método? NÃO!!!O princípio de não repetir a si mesmo ou DRY não se refere à repetição de código. Trata-se de espalhar uma decisão, uma idéia, para que, se você mudar de idéia, tenha que reescrever todos os lugares em que espalhar essa idéia. Blegh. Isso é terrível. Não faça isso. Em vez disso, use DRY para ajudá-lo a tomar decisões em um só lugar .
Porém, o DRY pode ser corrompido com o hábito de digitalizar código, procurando uma implementação semelhante que parece ser uma cópia e colagem de outro lugar. Esta é a forma de morte cerebral do DRY. Inferno, você poderia fazer isso com uma ferramenta de análise estática. Isso não ajuda porque ignora o ponto de DRY, que é manter o código flexível.
Se meus requisitos totais mudarem, talvez seja necessário alterar minha
total
implementação. Isso não significa que eu preciso alterar minhaadd
implementação. Se algum goober os juntou em um único método, agora estou sentindo um pouco de dor desnecessária.Quanta dor? Certamente eu poderia apenas copiar o código e criar um novo método quando necessário. Então, não é grande coisa, certo? Malarky! Se nada mais me custar um bom nome! É difícil encontrar bons nomes e não responde bem quando você brinca com o significado deles. Bons nomes, que tornam clara a intenção, são mais importantes do que o risco de você copiar um bug que, francamente, é mais fácil de corrigir quando o método tem o nome correto.
Portanto, meu conselho é parar de deixar reações bruscas a códigos semelhantes amarrar sua base de código em nós. Não estou dizendo que você é livre para ignorar o fato de que existem métodos e, em vez disso, copie e cole de maneira indesejada. Não, cada método deve ter um nome muito bom que apóie a única ideia de que se trata. Se sua implementação coincidir com a implementação de alguma outra boa idéia, agora, hoje, quem se importa?
Por outro lado, se você tiver um
sum()
método que tenha uma implementação idêntica ou até mesmo diferentetotal()
, e mesmo assim, sempre que seus requisitos totais mudarem, você precisará mudar,sum()
então há uma boa chance de que sejam a mesma idéia sob dois nomes diferentes. O código não apenas seria mais flexível se fossem mesclados, como seria menos confuso de usar.Quanto aos parâmetros booleanos, sim, é um cheiro desagradável de código. O fluxo de controle não apenas é um problema, mas também mostra que você cortou uma abstração em um ponto ruim. As abstrações devem tornar as coisas mais simples de usar, não mais complicadas. Passar bools para um método para controlar seu comportamento é como criar uma linguagem secreta que decide qual método você está realmente chamando. Ow! Não faça isso comigo. Dê a cada método seu próprio nome, a menos que você tenha alguma honestidade quanto ao polimorfismo .
Agora, você parece esgotado com a abstração. Isso é muito ruim, porque a abstração é uma coisa maravilhosa quando bem feita. Você usa muito sem pensar nisso. Toda vez que você dirige um carro sem ter que entender o sistema de pinhão e cremalheira, toda vez que usa um comando de impressão sem pensar nas interrupções do sistema operacional e toda vez que escova os dentes sem pensar em cada cerda individual.
Não, o problema que você parece estar enfrentando é uma abstração ruim. Abstração criada para servir a um propósito diferente das suas necessidades. Você precisa de interfaces simples em objetos complexos que permitem solicitar que suas necessidades sejam atendidas sem precisar entender esses objetos.
Ao escrever o código do cliente que usa outro objeto, você sabe quais são suas necessidades e o que você precisa desse objeto. Não faz. É por isso que o código do cliente possui a interface. Quando você é o cliente, nada lhe diz quais são suas necessidades, exceto você. Você cria uma interface que mostra quais são suas necessidades e exige que tudo o que lhe é entregue atenda a essas necessidades.
Isso é abstração. Como cliente, nem sei com o que estou falando. Eu apenas sei o que preciso disso. Se isso significa que você precisa encerrar algo para alterar sua interface antes de me entregar bem. Eu não ligo Apenas faça o que eu preciso fazer. Pare de complicar.
Se eu tiver que olhar dentro de uma abstração para entender como usá-la, a abstração falhou. Eu não deveria saber como isso funciona. Apenas isso funciona. Dê um bom nome e, se eu olhar para dentro, não ficarei surpreso com o que acho. Não me faça continuar olhando para dentro para lembrar como usá-lo.
Quando você insiste que a abstração funciona dessa maneira, o número de níveis por trás dela não importa. Contanto que você não esteja olhando para trás da abstração. Você está insistindo que a abstração está de acordo com suas necessidades, não se adaptando às suas. Para que isso funcione, deve ser fácil de usar, ter um bom nome e não vazar .
Essa é a atitude que gerou a injeção de dependência (ou apenas passe de referência, se você é da velha escola como eu). Funciona bem com prefere composição e delegação sobre herança . A atitude tem muitos nomes. O meu favorito é dizer, não pergunte .
Eu poderia afogar você em princípios o dia todo. E parece que seus colegas de trabalho já são. Mas eis o seguinte: ao contrário de outros campos de engenharia, esse software tem menos de 100 anos. Ainda estamos todos descobrindo. Portanto, não deixe que alguém com muitos livros intimidadores aprenda você a escrever códigos difíceis de ler. Ouça-os, mas insista em que fazem sentido. Não tome nada com fé. Pessoas que codificam de alguma maneira só porque lhes disseram que é assim, sem saber por que fazem as maiores bagunças de todas.
fonte
O ditado usual de que todos lemos aqui e existe:
Bem, isso não é verdade! Seu exemplo mostra isso. Portanto, eu proporia a declaração levemente modificada (sinta-se à vontade para reutilizar ;-)):
Existem dois problemas diferentes no seu caso:
Ambos são correlacionados:
Shape
pode calcular isso desurface()
uma maneira especializada.Se você abstrair alguma operação em que exista um padrão comportamental geral comum, você terá duas opções:
Além disso, essa abordagem pode resultar em um efeito de acoplamento abstrato no nível do design. Toda vez que você quiser adicionar algum tipo de novo comportamento especializado, será necessário abstraí-lo, alterar o pai abstrato e atualizar todas as outras classes. Esse não é o tipo de propagação de mudanças que se pode desejar. E não está realmente no espírito das abstrações, não depende da especialização (pelo menos no design).
Não conheço o seu design e não posso ajudar mais. Talvez seja realmente um problema muito complexo e abstrato e não exista uma maneira melhor. Mas quais são as chances? Os sintomas da generalização excessiva estão aqui. Pode ser que esteja na hora de analisar novamente e considerar a composição em vez da generalização ?
fonte
Sempre que vejo um método em que o comportamento alterna com o tipo de parâmetro, considero imediatamente primeiro se esse método realmente pertence ao parâmetro method. Por exemplo, em vez de ter um método como:
Eu faria isso:
Mudamos o comportamento para o lugar que sabe quando usá-lo. Criamos uma abstração real na qual você não precisa conhecer os tipos ou os detalhes da implementação. Para sua situação, pode fazer mais sentido mover esse método da classe original (que chamarei
O
) para digitarA
e substituí-lo no tipoB
. Se o método for chamadodoIt
em algum objeto, vádoIt
paraA
e substitua pelo comportamento diferente emB
. Se houver bits de dados de ondedoIt
é originalmente chamado ou se o método for usado em locais suficientes, você poderá deixar o método original e delegar:No entanto, podemos mergulhar um pouco mais fundo. Vamos dar uma olhada na sugestão de usar um parâmetro booleano e ver o que podemos aprender sobre a maneira como seu colega de trabalho está pensando. Sua proposta é fazer:
Isso se parece muito com o
instanceof
que usei no meu primeiro exemplo, exceto pelo fato de estarmos externalizando essa verificação. Isso significa que teríamos que chamá-lo de duas maneiras:ou:
De primeira, o ponto de chamada não tem idéia de que tipo
A
tem. Portanto, devemos passar os booleanos por todo o caminho? Esse é realmente um padrão que queremos em toda a base de código? O que acontece se houver um terceiro tipo que precisamos levar em conta? Se é assim que o método é chamado, devemos movê-lo para o tipo e deixar o sistema escolher a implementação para nós polimorficamente.Da segunda maneira, já devemos saber o tipo de
a
ponto de chamada. Geralmente, isso significa que estamos criando a instância lá ou pegando uma instância desse tipo como parâmetro. Criar um métodoO
que leva umB
aqui funcionaria. O compilador saberia qual método escolher. Quando estamos conduzindo mudanças como essa, duplicar é melhor do que criar a abstração errada , pelo menos até descobrirmos para onde estamos realmente indo. Obviamente, estou sugerindo que realmente não terminamos, não importa o que mudamos até esse ponto.Precisamos olhar mais de perto a relação entre
A
eB
. Geralmente, nos dizem que devemos favorecer a composição sobre a herança . Isso não é verdade em todos os casos, mas é verdade em um número surpreendente de casos, uma vez que investigamos.B
Herda deA
, o que significa que acreditamosB
ser umA
.B
deve ser usado da mesma maneiraA
, exceto que funciona um pouco diferente. Mas quais são essas diferenças? Podemos dar às diferenças um nome mais concreto? NãoB
é umA
, mas realmenteA
tem umX
que poderia serA'
ouB'
? Como seria nosso código se fizéssemos isso?Se mudarmos o método para
A
o sugerido anteriormente, poderemos injetar uma instância deX
emA
e delegar esse método paraX
:Podemos implementar
A'
e nosB'
livrarB
. Melhoramos o código, dando um nome a um conceito que poderia ter sido mais implícito, e nos permitimos definir esse comportamento em tempo de execução, em vez de em tempo de compilação.A
tornou-se menos abstrato também. Em vez de um relacionamento de herança estendido, ele está chamando métodos em um objeto delegado. Esse objeto é abstrato, mas mais focado apenas nas diferenças de implementação.Há uma última coisa a se considerar. Vamos reverter a proposta do seu colega de trabalho. Se em todos os sites de chamadas soubermos explicitamente o tipo de
A
que possuímos, deveríamos fazer chamadas como:Assumimos anteriormente ao compor que
A
tem umX
que éA'
ouB'
. Mas talvez até essa suposição não esteja correta. É este o único lugar em que essa diferença entreA
eB
importa? Se for, talvez possamos adotar uma abordagem um pouco diferente. Ainda temos umX
que é umA'
ou outroB'
, mas não pertenceA
. Só seO.doIt
preocupa com isso, então vamos transmiti-lo paraO.doIt
:Agora, nosso site de chamadas se parece com:
Mais uma vez,
B
desaparece e a abstração se move para o mais focadoX
. Desta vez, porém,A
é ainda mais simples sabendo menos. É ainda menos abstrato.É importante reduzir a duplicação em uma base de código, mas devemos considerar por que a duplicação ocorre em primeiro lugar. A duplicação pode ser um sinal de abstrações mais profundas que estão tentando sair.
fonte
A abstração por herança pode se tornar bastante feia. Hierarquias de classes paralelas com fábricas típicas. A refatoração pode se tornar uma dor de cabeça. E também o desenvolvimento posterior, o local em que você está.
Existe uma alternativa: pontos de extensão , abstrações estritas e personalização em camadas. Diga uma personalização de clientes governamentais, com base nessa personalização para uma cidade específica.
Um aviso: Infelizmente, isso funciona melhor quando todas (ou a maioria) das classes são ampliadas. Nenhuma opção para você, talvez em pequenas.
Essa extensibilidade funciona com uma classe base de objeto extensível que contém extensões:
Internamente, existe um mapeamento lento de objeto para objetos estendidos por classe de extensão.
Para classes e componentes da GUI, a mesma extensibilidade, em parte com herança. Adicionando botões e tal.
No seu caso, uma validação deve verificar se foi estendida e se validar contra as extensões. A introdução de pontos de extensão apenas para um caso adiciona código incompreensível, nada bom.
Portanto, não há solução, a não ser tentar trabalhar no contexto atual.
fonte
'controle de fluxo oculto' parece muito ondulado para mim.
Qualquer construção ou elemento retirado do contexto pode ter essa característica.
Abstrações são boas. Eu os tempero com duas diretrizes:
Melhor não abstrair cedo demais. Aguarde mais exemplos de padrões antes de abstrair. É claro que "mais" é subjetivo e específico para a situação difícil.
Evite muitos níveis de abstração apenas porque a abstração é boa. Um programador terá que manter esses níveis em mente para obter códigos novos ou alterados à medida que eles exploram a base de código e avançam 12 níveis. O desejo de um código bem abstrato pode levar a tantos níveis que dificilmente serão seguidos por muitas pessoas. Isso também leva a bases de código 'ninja mantidas apenas'.
Nos dois casos, 'mais e' muitos 'não são números fixos. Depende. É isso que dificulta.
Também gosto deste artigo de Sandi Metz
https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction
duplicação é muito mais barata que a abstração errada
e
prefere duplicação à abstração errada
fonte