Em que ponto a YAGNI deve ter precedência contra boas práticas de codificação e vice-versa? Estou trabalhando em um projeto no trabalho e quero lentamente introduzir bons padrões de código para meus colegas de trabalho (atualmente não há nenhum e tudo é meio que hackeado sem rima ou razão), mas depois de criar uma série de classes (nós Não faça TDD ou, infelizmente, nenhum tipo de teste de unidade. Dei um passo para trás e achei que estava violando o YAGNI porque sei com certeza que não precisamos da extensão de algumas dessas classes.
Aqui está um exemplo concreto do que quero dizer: tenho uma camada de acesso a dados que envolve um conjunto de procedimentos armazenados, que usa um padrão rudimentar de estilo de repositório com funções básicas de CRUD. Como existem vários métodos que todas as minhas classes de repositório precisam, criei uma interface genérica para meus repositórios, chamada IRepository
. No entanto, criei uma interface de "marcador" (ou seja, interface que não adiciona nenhuma nova funcionalidade) para cada tipo de repositório (por exemplo ICustomerRepository
) e a classe concreta implementa isso. Fiz a mesma coisa com uma implementação de fábrica para criar os objetos de negócios de DataReaders / DataSets retornados pelo Stored Procedure; a assinatura da minha classe de repositório tende a se parecer com isso:
public class CustomerRepository : ICustomerRepository
{
ICustomerFactory factory = null;
public CustomerRepository() : this(new CustomerFactory() { }
public CustomerRepository(ICustomerFactory factory) {
this.factory = factory;
}
public Customer Find(int customerID)
{
// data access stuff here
return factory.Build(ds.Tables[0].Rows[0]);
}
}
Minha preocupação aqui é que eu estou violando o YAGNI porque sei com 99% de certeza que nunca haverá uma razão para dar algo que não seja concreto CustomerFactory
a este repositório; como não temos testes de unidade, não preciso de MockCustomerFactory
coisas semelhantes ou semelhantes, e ter tantas interfaces pode confundir meus colegas de trabalho. Por outro lado, usar uma implementação concreta da fábrica parece um cheiro de design.
Existe uma boa maneira de chegar a um acordo entre o design adequado do software e não a arquitetura excessiva da solução? Estou questionando se preciso ter todas as "interfaces de implementação únicas" ou se posso sacrificar um pouco de bom design e apenas ter, por exemplo, a interface base e, em seguida, o concreto único, e não me preocupar com a programação para o interface, se for a implementação que será usada.
fonte
Respostas:
YAGNI.
Falsa suposição.
Isso não é um "sacrifício". Esse é um bom design.
fonte
Na maioria dos casos, evitar o código que você não precisará leva a um design melhor . O design mais sustentável e à prova de futuro é aquele que usa a menor quantidade de código simples e bem nomeado que atende aos requisitos.
Os projetos mais simples são os mais fáceis de evoluir. Nada mata a manutenção, como camadas de abstração inúteis e com excesso de engenharia.
fonte
YAGNI e SOLID (ou qualquer outra metodologia de design) não são mutuamente exclusivos. No entanto, eles são opostos quase polares. Você não precisa aderir 100% a nenhum dos dois, mas haverá algumas opções para dar e receber; quanto mais você olha para um padrão altamente abstrato usado por uma classe em um só lugar, diz YAGNI e simplifica, menos SÓLIDO o design se torna. O contrário também pode ser verdadeiro; muitas vezes no desenvolvimento, um projeto é implementado SOLIDly "na fé"; você não vê como precisará disso, mas apenas tem um palpite. Isso pode ser verdade (e é cada vez mais provável que seja verdade quanto mais experiência você ganha), mas também pode colocar você em dívidas técnicas como uma abordagem tapa-rápido; em vez de uma base de código "código espaguete" DIL, você pode acabar com "código lasanha", ter tantas camadas que simplesmente adicionar um método ou um novo campo de dados se torna um processo de vários dias para percorrer proxys de serviço e dependências fracamente acopladas com apenas uma implementação. Ou você pode acabar com o "código ravioli", que vem em pequenos pedaços pequenos que, movendo para cima, para baixo, para a esquerda ou para a direita na arquitetura, leva você a 50 métodos com 3 linhas cada.
Eu já disse isso em outras respostas, mas aqui está: Na primeira passagem, faça funcionar. No segundo passe, torne-o elegante. Na terceira passagem, torne-o SÓLIDO.
Quebrando isso:
Quando você escreve uma linha de código pela primeira vez, ela simplesmente tem que funcionar. Neste ponto, pelo que você sabe, é único. Portanto, você não recebe nenhum ponto de estilo por construir uma arquitetura "torre de marfim" para adicionar 2 e 2. Faça o que precisa e assuma que nunca mais a verá.
Na próxima vez que seu cursor entrar nessa linha de código, você já refutou sua hipótese desde a primeira vez que a escreveu. Você está revisitando esse código, provavelmente para estendê-lo ou usá-lo em outro lugar, para que não seja único. Agora, alguns princípios básicos como DRY (não se repita) e outras regras simples para o design do código devem ser implementados; extrair métodos e / ou loops de formulário para código repetido, extrair variáveis para literais ou expressões comuns, talvez adicionar alguns comentários, mas, em geral, seu código deve se auto-documentar. Agora, seu código está bem organizado, se ainda estiver bem acoplado, e qualquer pessoa que o veja pode aprender facilmente o que está fazendo lendo o código, em vez de rastreá-lo linha por linha.
A terceira vez que o cursor digita esse código, provavelmente é um grande problema; você está estendendo o arquivo novamente ou ele se tornou útil em pelo menos três outros locais diferentes na base de código. Neste ponto, é um elemento-chave, se não o principal, do seu sistema e deve ser arquitetado como tal. Neste ponto, você também costuma ter o conhecimento de como ele foi usado até agora, o que permitirá que você tome boas decisões de design com relação a como arquitetar o design para otimizar esses usos e quaisquer novos. Agora as regras do SOLID devem inserir a equação; extrair classes contendo código com finalidades específicas, definir interfaces comuns para quaisquer classes que tenham finalidades ou funcionalidades semelhantes, configurar dependências fracamente acopladas entre classes e projetar as dependências para que você possa facilmente adicionar, remover ou trocar.
A partir deste momento, se você precisar estender, reimplementar ou reutilizar ainda mais esse código, tudo estará bem empacotado e abstraído no formato "black box" que todos conhecemos e amamos; conecte-o onde quer que você precise ou adicione uma nova variação no tema como uma nova implementação da interface sem precisar alterar o uso da referida interface.
fonte
Em vez de qualquer um desses, eu prefiro WTSTWCDTUAWCROT?
(Qual é a coisa mais simples que podemos fazer que é útil e que podemos lançar na quinta-feira?)
Acrônimos mais simples estão na minha lista de coisas a fazer, mas não são uma prioridade.
fonte
YAGNI e bom design não são conflitantes. YAGNI é sobre (não) apoiar necessidades futuras. Um bom design é tornar transparente o que seu software faz agora e como ele faz isso.
A introdução de uma fábrica tornará seu código existente mais simples? Caso contrário, não adicione. Se isso acontecer, por exemplo, quando você estiver adicionando testes (o que você deve fazer!), Adicione-o.
YAGNI é sobre não adicionar complexidade para suportar funções futuras.
O bom design trata da remoção da complexidade e ainda oferece suporte a todas as funções atuais.
fonte
Eles não estão em conflito, seus objetivos estão errados.
O que você está tentando realizar?
Você deseja escrever um software de qualidade e, para isso, manter a base de código pequena e sem problemas.
Agora chegamos a um conflito, como encobrir todos os casos se não escrevemos casos que não vamos usar?
Aqui está a aparência do seu problema.
(qualquer pessoa interessada, isso é chamado de nuvens evaporantes )
Então, o que está dirigindo isso?
Qual destes podemos resolver? Bem, parece que não querer perder tempo e inchar o código é um grande objetivo e faz sentido. E o primeiro? Podemos descobrir o que precisamos codificar?
Vamos reformular tudo isso
Você não precisa de um compromisso, precisa de alguém para gerenciar a equipe que seja competente e tenha visão de todo o projeto. Você precisa de alguém que possa planejar o que você precisará, em vez de cada um de vocês colocar coisas que você NÃO precisará, porque você é incerto quanto ao futuro porque ... por quê? Vou lhe dizer por que, é porque ninguém tem um plano maldito entre todos vocês. Você está tentando introduzir padrões de código para corrigir um problema totalmente separado. Seu problema PRIMÁRIO que você precisa resolver é um roteiro e projeto claros. Depois disso, você pode dizer "os padrões de código nos ajudam a atingir esse objetivo de maneira mais eficaz como uma equipe", que é a verdade absoluta, mas está fora do escopo desta pergunta.
Obtenha um gerente de projeto / equipe que possa fazer essas coisas. Se você tiver um, precisará pedir um mapa e explicar o problema do YAGNI que não está apresentando um mapa. Se eles forem extremamente incompetentes, escreva você mesmo o plano e diga "aqui está o meu relatório sobre as coisas que precisamos, revise-o e informe-nos sua decisão".
fonte
Permitindo que o seu código a ser ampliada com testes de unidade é nunca vai ser coberto por YAGNI, porque você vai precisar dele. No entanto, não estou convencido de que suas alterações de design em uma interface de implementação única estejam realmente aumentando a capacidade de teste do código porque o CustomerFactory já herda de uma interface e pode ser trocado por um MockCustomerFactory a qualquer momento.
fonte
A questão apresenta um falso dilema. A aplicação adequada do princípio YAGNI não é algo não relacionado. É um aspecto do bom design. Cada um dos princípios do SOLID também são aspectos de bom design. Você nem sempre pode aplicar totalmente todos os princípios em qualquer disciplina. Os problemas do mundo real impõem muitas forças ao seu código, e alguns deles pressionam em direções opostas. Os princípios de design devem ser responsáveis por todos esses aspectos, mas nenhum punhado de princípios pode atender a todas as situações.
Agora, vamos dar uma olhada em cada princípio com o entendimento de que, embora às vezes possam seguir direções diferentes, eles não estão inerentemente em conflito.
O YAGNI foi concebido para ajudar os desenvolvedores a evitar um tipo específico de retrabalho: o que resulta da construção da coisa errada. Isso é feito nos orientando a evitar decisões erradas muito cedo, com base em suposições ou previsões sobre o que pensamos que mudará ou será necessário no futuro. A experiência coletiva nos diz que, quando fazemos isso, geralmente estamos errados. Por exemplo, o YAGNI diria para você não criar uma interface com a finalidade de reutilização , a menos que saiba agora que precisa de vários implementadores. Da mesma forma, o YAGNI diria que não crie um "ScreenManager" para gerenciar o formulário único em um aplicativo, a menos que você saiba agora que terá mais de uma tela.
Ao contrário do que muitas pessoas pensam, o SOLID não se trata de reutilização, genericidade ou mesmo abstração. O SOLID tem como objetivo ajudá-lo a escrever o código preparado para a mudança , sem dizer nada sobre o que pode ser essa alteração específica. Os cinco princípios do SOLID criam uma estratégia para criar código flexível sem ser excessivamente genérico e simples sem ser ingênuo. A aplicação adequada do código SOLID produz classes pequenas e focadas, com papéis e limites bem definidos. O resultado prático é que, para qualquer alteração de requisitos necessária, um número mínimo de classes precisa ser tocado. E da mesma forma, para qualquer alteração de código, há uma quantidade minimizada de "ondulação" para outras classes.
Olhando para a situação de exemplo que você tem, vamos ver o que YAGNI e SOLID podem ter a dizer. Você está considerando uma interface de repositório comum devido ao fato de que todos os repositórios têm a mesma aparência do lado de fora. Mas o valor de uma interface comum e genérica é a capacidade de usar qualquer um dos implementadores sem precisar saber qual é o particular. A menos que haja algum lugar no seu aplicativo em que isso seja necessário ou útil, YAGNI diz que não faça isso.
Existem 5 princípios do SOLID a serem considerados. S é responsabilidade única. Isso não diz nada sobre a interface, mas pode dizer algo sobre suas classes concretas. Pode-se argumentar que o manuseio do acesso a dados em si pode ser uma responsabilidade de uma ou mais outras classes, enquanto a responsabilidade dos repositórios é traduzir de um contexto implícito (CustomerRepository é um repositório implicitamente para entidades do Cliente) em chamadas explícitas para o API de acesso a dados generalizada, especificando o tipo de entidade Cliente.
O é Aberto-Fechado. Isto é principalmente sobre herança. Isso se aplicaria se você estivesse tentando derivar seus repositórios de uma base comum implementando funcionalidades comuns ou se esperava derivar ainda mais dos diferentes repositórios. Mas você não é, então não.
L é substituibilidade de Liskov. Isso se aplica se você pretendeu usar os repositórios através da interface comum do repositório. Ele impõe restrições à interface e implementações para garantir consistência e evitar manuseio especial para diferentes impulsores. A razão para isso é que esse tratamento especial prejudica o objetivo de uma interface. Pode ser útil considerar esse princípio, pois pode alertá-lo para o uso da interface comum do repositório. Isso coincide com a orientação da YAGNI.
Eu sou Segregação de Interface. Isso pode ser aplicado se você começar a adicionar diferentes operações de consulta aos seus repositórios. A segregação de interface se aplica onde você pode dividir os membros de uma classe em dois subconjuntos, onde um será usado por determinados consumidores e o outro por outros, mas nenhum consumidor provavelmente usará os dois subconjuntos. A orientação é criar duas interfaces separadas, em vez de uma comum. No seu caso, é improvável que a busca e o salvamento de instâncias individuais sejam consumidos pelo mesmo código que faria consultas gerais; portanto, pode ser útil separá-las em duas interfaces.
D é injeção de dependência. Aqui voltamos ao mesmo ponto que o S. Se você separou o consumo da API de acesso a dados em um objeto separado, esse princípio diz que, em vez de apenas atualizar uma instância desse objeto, você deve transmiti-lo ao criar um repositório. Isso facilita o controle da vida útil do componente de acesso a dados, abrindo a possibilidade de compartilhar referências a ele entre seus repositórios, sem ter que seguir o caminho de torná-lo um singleton.
É importante observar que a maioria dos princípios do SOLID não se aplica necessariamente nesse estágio específico do desenvolvimento do seu aplicativo. Por exemplo, se você deve interromper o acesso aos dados depende de quão complicado é e se deseja testar a lógica do repositório sem atingir o banco de dados. Parece que isso é improvável (infelizmente, na minha opinião), então provavelmente não é necessário.
Portanto, depois de toda essa consideração, descobrimos que o YAGNI e o SOLID realmente fornecem um conselho comum e relevante imediatamente: provavelmente não é necessário criar uma interface genérica comum de repositório.
Todo esse pensamento cuidadoso é extremamente útil como um exercício de aprendizado. Consome tempo enquanto você aprende, mas com o tempo você desenvolve intuição e se torna muito rápido. Você saberá a coisa certa a fazer, mas não precisará pensar em todas essas palavras, a menos que alguém lhe peça uma explicação.
fonte
Você parece acreditar que "bom design" significa seguir algum tipo de idealogia e conjunto formal de regras que sempre devem ser aplicadas, mesmo quando inúteis.
OMI é um design ruim. YAGNI é um componente do bom design, nunca uma contradição.
fonte
No seu exemplo, eu diria que YAGNI deve prevalecer. Não lhe custará muito se você precisar adicionar interfaces posteriormente. A propósito, é realmente um bom design ter uma interface por classe se ela não serve a nenhum objetivo?
Mais um pensamento, às vezes o que você precisa não é um bom design, mas um design suficiente. Aqui está uma sequência muito interessante de postagens sobre o tema:
fonte
Algumas pessoas argumentam que os nomes de interface não devem começar com I. Especificamente, um motivo é que você está realmente perdendo a dependência de o tipo dado ser uma classe ou uma interface.
O que o proíbe de
CustomerFactory
ser uma classe no início e depois transformá-la em uma interface, que será implementada porDefaultCustormerFactory
ouUberMegaHappyCustomerPowerFactory3000
? A única coisa que você deve ter que mudar é o local onde a implementação é instanciada. E se você tem um design mais menos bom, esse é um punhado de lugares no máximo.A refatoração é uma parte do desenvolvimento. É melhor ter pouco código, fácil de refatorar, do que ter uma interface e uma classe declaradas para cada classe, forçando você a alterar todos os nomes de métodos em pelo menos dois lugares ao mesmo tempo.
O ponto real no uso de interfaces é alcançar a modularidade, que é possivelmente o pilar mais importante do bom design. Observe, no entanto, que um módulo não é apenas definido por sua dissociação do mundo externo (embora seja assim que o percebemos da perspectiva externa), mas igualmente por seu trabalho interno.
O que pretendo salientar é que dissociar as coisas, que inerentemente pertencem umas às outras, não faz muito sentido. De certa forma, é como ter um armário com uma prateleira individual por copo.
A importância reside em conceber um problema grande e complexo em subproblemas menores e mais simples. E você deve parar no ponto em que elas se tornam simples o suficiente, sem subdivisões, ou então elas se tornarão mais complicadas. Isso pode ser visto como um corolário de YAGNI. E isso definitivamente significa um bom design.
O objetivo não é, de alguma forma, resolver um problema local com um único repositório e uma única fábrica. O objetivo é que essa decisão não tenha efeito no restante do seu aplicativo. É disso que trata a modularidade.
Você deseja que seus colegas de trabalho examinem seu módulo, vejam uma fachada com algumas chamadas auto-explicativas e se sintam confiantes de que eles podem usá-los, sem ter que se preocupar com todas as canalizações internas potencialmente sofisticadas.
fonte
Você está criando
interface
várias implementações antecipadas no futuro. Você também pode ter umI<class>
para cada classe em sua base de código. Não.Basta usar a única classe de concreto de acordo com a YAGNI. Se você achar que precisa ter um objeto "simulado" para fins de teste, transforme a classe original em uma classe abstrata com duas implementações, uma com a classe concreta original e outra com a implementação simulada.
Obviamente, você precisará atualizar todas as instanciações da classe original para instanciar a nova classe concreta. Você pode contornar isso usando um construtor estático na frente.
YAGNI diz para não escrever código antes que ele precise ser escrito.
Bom design diz usar abstração.
Você pode ter os dois. Uma classe é uma abstração.
fonte
Por que o marcador faz interface? Parece-me que não está fazendo nada além de "marcação". Com uma "etiqueta" diferente para cada tipo de fábrica, qual é o objetivo?
O objetivo de uma interface é dar a uma classe um "comportamento como" comportamento, dar-lhe "capacidade de repositório", por assim dizer. Portanto, se todos os seus tipos de repositórios concretos se comportarem como o mesmo IRepository (todos implementam o IRepository), todos poderão ser tratados da mesma maneira por outro código - exatamente pelo mesmo código. Nesse ponto, seu design é extensível. Adicionando tipos de repositório mais concretos, todos tratados como IRepository (s) genéricos - o mesmo código manipula todos os tipos concretos como repositórios "genéricos".
As interfaces são para lidar com coisas com base em pontos comuns. Mas as interfaces de marcador personalizadas a) não adicionam comportamento. eb) forçá-lo a lidar com sua singularidade.
Na medida em que você cria interfaces úteis, você obtém a vantagem de não precisar escrever código especializado para lidar com classes, tipos ou interfaces de marcador personalizadas que têm uma correlação de 1: 1 com classes concretas. Isso é redundância sem sentido.
De imediato, posso ver uma interface de marcador se, por exemplo, você precisar de uma coleção fortemente tipada de várias classes diferentes. Na coleção, todos são "ImarkerInterface", mas, à medida que você os puxa, você precisa convertê-los nos tipos adequados.
fonte
Agora você pode escrever um ICustomerRepository vagamente razoável? Por exemplo, (realmente chegando aqui, provavelmente um péssimo exemplo) no passado, seus clientes sempre usavam o PayPal? Ou todo mundo na empresa está entusiasmado com a conexão com o Alibaba? Nesse caso, você pode usar o design mais complexo agora e parecer previdente aos seus chefes. :-)
Caso contrário, espere. Adivinhar em uma interface antes de você ter uma ou duas implementações reais geralmente falha. Em outras palavras, não generalize / abstraia / use um padrão de design sofisticado até ter alguns exemplos para generalizar.
fonte