Por que precisamos de tantas classes em padrões de design?

204

Sou um desenvolvedor júnior entre os seniores e estou lutando muito para entender seu raciocínio e raciocínio.

Estou a ler o Design orientado por domínio (DDD) e não consigo entender por que precisamos de criar muitas classes. Se seguirmos esse método de projetar software, acabaremos com 20 a 30 classes, que podem ser substituídas por no máximo dois arquivos e 3-4 funções. Sim, isso pode ser confuso, mas é muito mais fácil de ler e manter.

Sempre que eu quiser ver o que algum tipo de EntityTransformationServiceImpl faz, preciso seguir muitas classes, interfaces, suas chamadas de função, construtores, sua criação e assim por diante.

Matemática simples:

  • 60 linhas de código fictício vs 10 classes X 10 (digamos que temos lógicas totalmente diferentes) = 600 linhas de código confuso vs. 100 classes + um pouco mais para envolvê-las e gerenciá-las; não se esqueça de adicionar injeção de dependência.
  • Lendo 600 linhas de código confuso = um dia
  • 100 aulas = uma semana, ainda esquece qual delas faz o quê, quando

Todo mundo está dizendo que é fácil de manter, mas para quê? Toda vez que você adicionar uma nova funcionalidade, você adicionará mais cinco classes com fábricas, entidades, serviços e valores. Eu sinto que esse tipo de código se move muito mais devagar do que o código bagunçado.

Digamos que, se você escrever 50K de código confuso de LOC em um mês, o DDD requer muitas revisões e alterações (não me importo com os testes em ambos os casos). Uma adição simples pode levar uma semana, se não mais.

Em um ano, você escreve um monte de código confuso e pode até reescrevê-lo várias vezes, mas com o estilo DDD, você ainda não tem recursos suficientes para competir com o código bagunçado.

Por favor explique. Por que precisamos desse estilo DDD e muitos padrões?

UPD 1 : Eu recebi muitas ótimas respostas, por favor, adicione comentários em algum lugar ou edite sua resposta com o link para a lista de leitura (não tenho certeza de onde começar, DDD, Padrões de Design, UML, Código Completo, Refatoração, Pragmático, ... tantos bons livros), é claro, com sequência, para que eu também possa começar a entender e se tornar sênior, como alguns de vocês fazem.

    
por user1318496 10.04.2018 / 18:10
fonte

10 respostas

328

Este é um problema de otimização

Um bom engenheiro entende que um problema de otimização não tem sentido sem um alvo. Você não pode simplesmente otimizar, você tem que otimizar para algo. Por exemplo, suas opções de compilador incluem otimizar a velocidade e otimizar o tamanho do código; estas são, por vezes, objetivos opostos.

Eu gosto de dizer à minha esposa que minha mesa está otimizada para adicionar. É apenas uma pilha e é muito fácil adicionar coisas. Minha esposa preferiria se eu otimizasse a recuperação, ou seja, organizei minhas coisas um pouco para poder encontrar coisas. Isso dificulta, é claro, adicionar.

O software é o mesmo. Você pode certamente otimizar a criação de produtos - gerar uma tonelada de código monolítico o mais rápido possível, sem se preocupar em organizar isto. Como você já percebeu, isso pode ser muito, muito rápido. A alternativa é otimizar a manutenção - tornar a criação mais difícil, mas tornar as modificações mais fáceis ou menos arriscadas. Esse é o propósito do código estruturado.

Eu sugeriria que um produto de software bem-sucedido seria criado apenas uma vez, mas modificado muitas e muitas vezes. Engenheiros experientes viram as bases de código não estruturadas adquirirem vida própria e se tornarem produtos, crescendo em tamanho e complexidade, até que até mesmo pequenas mudanças são muito difíceis de fazer sem introduzir riscos enormes. Se o código foi estruturado, o risco pode ser contido. É por isso que vamos a todo esse problema.

A complexidade vem das relações, não dos elementos

Eu percebo em sua análise que você está olhando para quantidades - quantidade de código, número de classes, etc. Embora sejam interessantes, o impacto real vem das relações entre os elementos, que explodem combinatoriamente. Por exemplo, se você tem 10 funções e nenhuma idéia de que depende, com quais 90 relações possíveis (dependências) você precisa se preocupar - cada uma das dez funções pode depender de qualquer uma das outras nove funções, e 9 x 10 = 90. Você pode não ter idéia de quais funções modificam quais variáveis ou como os dados são passados, então os codificadores têm uma tonelada de coisas para se preocupar quando resolvem qualquer problema em particular. Em contraste, se você tem 30 classes, mas elas são organizadas de forma inteligente, elas podem ter apenas 29 relações, por exemplo, se eles estiverem em camadas ou dispostos em uma pilha.

Como isso afeta o rendimento de sua equipe? Bem, há menos dependências, o problema é muito mais tratável; os codificadores não têm que manipular um zilhão de coisas em suas cabeças sempre que fizerem uma mudança. Portanto, minimizar as dependências pode ser um grande impulso para sua capacidade de raciocinar sobre um problema com competência. É por isso que dividimos as coisas em classes ou módulos, e as variáveis de escopo tão bem quanto possível, e usamos SOLID princípios.

    
por 10.04.2018 / 20:51
fonte
65

Bem, em primeiro lugar, a legibilidade e a facilidade de manutenção estão frequentemente no olho de quem vê.

O que é legível para você pode não ser para o seu vizinho.

A manutenção geralmente se resume à capacidade de descoberta (a facilidade com que um comportamento ou conceito é descoberto na base de código) e a descoberta é outra coisa subjetiva.

DDD

Uma das maneiras pelas quais o DDD ajuda equipes de desenvolvedores é sugerir uma maneira específica (ainda que subjetiva) de organizar seus conceitos e comportamentos de código. Esta convenção facilita a descoberta de coisas e, portanto, facilita a manutenção do aplicativo.

  • Os conceitos de domínio são codificados como Entidades e agregados
  • O comportamento do domínio reside em entidades ou serviços de domínio
  • A consistência é garantida pelas raízes agregadas
  • As preocupações de persistência são tratadas por Repositórios

Esse arranjo não é objetivamente mais fácil de manter. É, no entanto, mensuravelmente mais fácil de manter, quando todos entendem que estão operando em um contexto de DDD.

Classes

As aulas ajudam na capacidade de manutenção, legibilidade, descoberta, etc ... porque são uma convenção bem conhecida .

Nas configurações Orientadas a Objetos, as Classes são normalmente usadas para agrupar comportamentos relacionados e para encapsular o estado que precisa ser controlado com cuidado.

Eu sei que isso soa muito abstrato, mas você pode pensar da seguinte maneira:

Com as turmas, você não precisa necessariamente saber como o código nelas funciona. Você só precisa saber de que a classe é responsável.

As turmas permitem que você raciocine sobre seu aplicativo em termos de interações entre componentes bem definidos .

Isso reduz a carga cognitiva ao raciocinar sobre como o seu aplicativo funciona. Em vez de ter que lembrar o que 600 linhas de código realizam, é possível pensar em como 30 componentes interagem.

E, considerando esses 30 componentes provavelmente abrangem 3 camadas do seu aplicativo, você provavelmente só precisa raciocinar sobre 10 componentes por vez.

Isso parece bastante administrável.

Resumo

Essencialmente, o que você está vendo o desenvolvedor sênior é:

Eles estão dividindo o aplicativo em classes fáceis de avaliar .

Eles então organizam isso em camadas fáceis de raciocinar sobre .

Eles estão fazendo isso porque sabem que à medida que o aplicativo cresce, fica cada vez mais difícil raciocinar sobre isso como um todo. Quebrar em Layers e Classes significa que eles nunca precisam raciocinar sobre o aplicativo inteiro. Eles só precisam raciocinar sobre um pequeno subconjunto dele.

    
por 10.04.2018 / 18:35
fonte
29

Please explain me, why do we need this DDD style, lots of Patterns?

Primeiro, uma nota: a parte importante do DDD não são os padrões , mas o alinhamento do esforço de desenvolvimento com o negócio. Greg Young observou que os capítulos do livro azul estão na ordem errada .

Mas para sua pergunta específica: há muito mais classes do que você esperaria, (a) porque está sendo feito um esforço para distinguir o comportamento do domínio do encanamento e (b) porque um esforço extra está sendo feito para garantir que os conceitos no modelo de domínio sejam expressos explicitamente.

Sem rodeios, se você tiver dois conceitos diferentes no domínio, eles devem ser distintos no modelo mesmo que compartilhem o mesmo na representação da memória .

Com efeito, você está criando um idioma específico do domínio que descreve seu modelo no idioma da empresa, de modo que um especialista em domínio possa analisá-lo e identificar erros.

Além disso, você vê um pouco mais de atenção na separação de interesses; e a noção de isolar os consumidores de alguma capacidade dos detalhes de implementação. Consulte D. L. Parnas . Os limites claros permitem que você mude ou amplie a implementação sem que os efeitos sejam ondulados em toda a sua solução.

A motivação aqui: para um aplicativo que faz parte da competência principal de um negócio (ou seja, um lugar onde ele deriva uma vantagem competitiva), você desejará ser capaz de substituir um comportamento de domínio de maneira fácil e barata com uma melhor variação . Na verdade, você tem partes do programa que deseja evoluir rapidamente (como o estado evolui com o tempo) e outras partes que você deseja alterar lentamente (como o estado é armazenado); as camadas extras de abstração ajudam a evitar o acoplamento inadvertido de um ao outro.

Para ser justo: alguns deles também são danos cerebrais orientados a objetos. Os padrões originalmente descritos por Evans são baseados em projetos Java dos quais ele participou há mais de 15 anos; estado e comportamento estão strongmente associados a esse estilo, o que leva a complicações que você pode preferir evitar; veja Percepção e Ação de Stuart Halloway, ou Inlinando o código de John Carmack.

No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient. Carmack, 2012

    
por 10.04.2018 / 18:38
fonte
29

Há muitos pontos positivos nas outras respostas, mas acho que eles não percebem um erro conceitual importante:

Você está comparando o esforço para entender o programa completo.

Esta não é uma tarefa realista na maioria dos programas. Mesmo programas simples consistem em tanto código que é simplesmente impossível gerenciar tudo na cabeça a qualquer momento. Sua única chance é encontrar a parte de um programa que seja relevante para a tarefa em questão (corrigir um bug, implementar um novo recurso) e trabalhar com isso.

Se o seu programa consiste em grandes funções / métodos / classes, isso é quase impossível. Você terá que entender centenas de linhas de código apenas para decidir se esse pedaço de código é relevante para o seu problema. Com as estimativas que você deu, torna-se fácil passar uma semana apenas para encontrar o código que você precisa trabalhar.

Compare isso com uma base de código com pequenas funções / métodos / classes nomeadas e organizadas em pacotes / namespaces que tornam óbvio onde encontrar / colocar uma determinada parte da lógica. Quando feito corretamente em muitos casos, você pode pular direto para o lugar correto para resolver o seu problema, ou pelo menos para um lugar de onde acionar seu depurador o levará ao ponto certo em alguns saltos.

Eu trabalhei em ambos os tipos de sistemas. A diferença pode ser facilmente duas ordens de magnitude no desempenho para tarefas comparáveis e tamanho de sistema comparável.

O efeito que isso tem sobre outras atividades:

    O teste de
  • se torna muito mais fácil com unidades menores
  • menos conflitos de mesclagem porque as chances de dois desenvolvedores trabalharem no mesmo código são menores.
  • menos duplicação porque é mais fácil reutilizar peças (e encontrar as peças em primeiro lugar).
por 11.04.2018 / 08:05
fonte
19

Porque o código de teste é mais difícil do que escrever código

Muitas respostas deram um bom raciocínio do ponto de vista do desenvolvedor - que a manutenção pode ser reduzida, ao custo de tornar o código mais árduo para escrever, em primeiro lugar.

No entanto, há outro aspecto a ser considerado - o teste só pode ser refinado como seu código original.

Se você escrever tudo em um monolito, o único teste efetivo que você pode escrever é "considerando essas entradas, a saída está correta?". Isso significa que quaisquer bugs encontrados, são escopo para "em algum lugar nessa pilha gigantesca de código".

Claro, você pode ter um desenvolvedor sentado com um depurador e encontrar exatamente onde o problema começou e trabalhar em uma correção. Isso requer muitos recursos e é um mau uso do tempo do desenvolvedor. Imagine que um pequeno bug que você tem, resulta em um desenvolvedor que precisa depurar todo o programa novamente.

A solução: Muitos testes menores que identificam uma falha potencial específica cada.

Esses pequenos testes (por exemplo, Testes Unitários) têm a vantagem de verificar uma área específica da base de código e ajudar a encontrar erros dentro de um escopo limitado. Isso não só acelera a depuração quando um teste falha, mas também significa que, se todos os seus pequenos testes falharem, você poderá encontrar mais facilmente a falha em seus testes maiores (ou seja, se não estiver em uma função específica testada, deve estar na interação). entre eles).

Como deve ficar claro, para fazer testes menores, sua base de código precisa ser dividida em partes testáveis menores. A maneira de fazer isso, em uma grande base de código comercial, geralmente resulta em um código parecido com o que você está trabalhando.

Assim como uma nota lateral: Isso não quer dizer que as pessoas não levem as coisas "longe demais". Mas há uma razão legítima para separar codebases em partes menores / menos conectadas - se isso for feito de maneira sensata.

    
por 11.04.2018 / 15:09
fonte
17

Please explain me, why do we need this DDD style, lots of Patterns?

Muitos (muitos ...) de nós realmente não precisam deles . Teóricos e programadores muito avançados e experientes escrevem livros sobre teorias e metodologias como resultado de muitas pesquisas e de sua profunda experiência - isso não significa que tudo que eles escrevem seja aplicável a todos os programadores em suas práticas cotidianas.

Como desenvolvedor júnior, é bom ler livros como o que você mencionou para ampliar suas perspectivas e conscientizá-lo sobre determinados problemas. Isso também evitará que você fique envergonhado e desorientado quando seus colegas seniores usarem uma terminologia desconhecida. Se você encontrar algo muito difícil e não parece fazer sentido ou parecer útil, não se mate por isso - apenas coloque na sua cabeça que existe tal conceito ou abordagem.

No seu desenvolvimento diário, a menos que você seja um acadêmico, seu trabalho é encontrar soluções viáveis e sustentáveis. Se as ideias que você encontrar em um livro não ajudarem você a alcançar esse objetivo, então não se preocupe com isso agora, contanto que seu trabalho seja considerado satisfatório.

Pode chegar um momento em que você descobrirá que pode usar um pouco do que leu, mas não conseguiu "entender" no começo, ou talvez não.

    
por 11.04.2018 / 00:53
fonte
8

Como sua pergunta está cobrindo muito terreno, com muitas suposições, vou destacar o assunto da sua pergunta:

Why do we need so many classes in design patterns

Nós não. Não há uma regra geralmente aceita que diga que deve haver muitas classes nos padrões de design.

Existem dois guias principais para decidir onde colocar o código e como cortar suas tarefas em diferentes unidades de código:

  • Coesão: qualquer unidade de código (seja um pacote, um arquivo, uma classe ou um método) deve pertencer . Ou seja, qualquer método específico deve ter uma tarefa e fazer isso bem. Qualquer classe deve ser responsável por um tópico maior (o que quer que seja). Nós queremos alta coesão.
  • Acoplamento: quaisquer duas unidades de código devem depender um do outro o mínimo possível - não deve haver nenhuma dependência circular. Nós queremos baixo acoplamento.

Por que esses dois devem ser importantes?

  • Coesão: um método que faz muitas coisas (por exemplo, um antigo script CGI que faz GUI, lógica, acesso ao banco de dados, etc., tudo em uma longa confusão de códigos) torna-se pesado. No momento da escrita, é tentador apenas colocar sua linha de pensamento em um método longo. Isso funciona, é fácil apresentar e tal, e você pode ser feito com isso. O problema surge depois: depois de alguns meses, você pode esquecer o que fez. Uma linha de código no topo pode estar a algumas telas de distância de uma linha na parte inferior; é fácil esquecer todos os detalhes. Qualquer alteração em qualquer parte do método pode quebrar qualquer quantidade de comportamentos da coisa complexa. Será muito fácil refatorar ou reutilizar partes do método. E assim por diante.
  • Acoplamento: sempre que você alterar uma unidade de código, você potencialmente quebrará todas as outras unidades que dependem dela. Em linguagens estritas como Java, você pode obter dicas durante o tempo de compilação (ou seja, sobre parâmetros, exceções declaradas e assim por diante). Mas muitas mudanças não estão provocando tais (ou seja, mudanças comportamentais), e outras linguagens mais dinâmicas, não têm tais possibilidades. Quanto mais alto o acoplamento, mais difícil ele muda qualquer coisa, e você pode parar, onde uma reescrita completa é necessária para atingir algum objetivo.

Esses dois aspectos são os "drivers" básicos para qualquer escolha de "onde colocar o quê" em qualquer linguagem de programação e qualquer paradigma (não apenas OO). Nem todo mundo está totalmente ciente deles, e leva tempo, muitas vezes anos, para ter um sentimento realmente arraigado e automático sobre como eles influenciam o software.

Obviamente, esses dois conceitos não dizem nada sobre o que realmente faz . Algumas pessoas erram do lado de muito, outras do lado de muito pouco. Algumas linguagens (olhando para você aqui, Java) tendem a favorecer muitas classes por causa da natureza extremamente estática e pedante da própria linguagem (isso não é uma declaração de valor, mas é o que é). Isso se torna especialmente perceptível quando comparado a linguagens dinâmicas e mais expressivas, por exemplo, Ruby.

Outro aspecto é que algumas pessoas assinam a abordagem ágil de apenas escrever código que é necessário agora , e refatorar mais tarde, quando necessário. Nesse estilo de desenvolvimento, você não criaria um interface quando tiver apenas uma classe de implementação. Você simplesmente implementaria a classe concreta. Se, mais tarde, você precisar de uma segunda aula, refataria.

Algumas pessoas simplesmente não funcionam assim. Eles criam interfaces (ou, mais geralmente, classes base abstratas) para qualquer coisa que possa ser usada de maneira mais geral; isso leva a uma explosão de classe rapidamente.

Novamente, há argumentos a favor e contra, e não importa qual eu, ou você, prefira. Você vai, em sua vida como desenvolvedor de software, encontrar todos os extremos, desde métodos longos de spaghetti, passando por projetos de classe iluminados, apenas grandes o suficiente, até esquemas de classe incrivelmente ampliados que são muito overengineered. À medida que você for mais experiente, crescerá mais em papéis "arquitetônicos" e poderá começar a influenciá-lo na direção desejada. Você descobrirá um meio de ouro para si mesmo e ainda descobrirá que muitas pessoas vão discordar de você, seja o que for que você faça.

Portanto, manter uma mente aberta é a parte mais importante, aqui, e esse seria o meu principal conselho para você, visto que você parece estar muito preocupado com a questão, a julgar pelo resto da sua pergunta ...

    
por 11.04.2018 / 19:22
fonte
7

Codificadores experientes aprenderam:

  • Porque esses pequenos programas e clasess começam a crescer, especialmente os bem-sucedidos. Os padrões simples que funcionaram em um nível simples não são dimensionáveis.
  • Porque pode parecer complicado ter de adicionar / alterar vários artefatos para cada adição / alteração, mas você sabe o que adicionar e é fácil fazê-lo. 3 minutos de digitação batem 3 horas de codificação inteligente.
  • Várias turmas pequenas não são "bagunçadas" só porque você não entende por que a sobrecarga é realmente uma boa ideia
  • Sem o conhecimento do domínio adicionado, o código "óbvio para mim" é muitas vezes misterioso para os meus companheiros de equipe ... além do futuro eu.
  • O conhecimento tribal pode tornar os projetos incrivelmente difíceis de adicionar aos membros da equipe de forma fácil e difícil para que eles sejam produtivos rapidamente.
  • A nomeação é um dos dois problemas difíceis na computação ainda é verdade e muitas classes e métodos são frequentemente um exercício intenso de nomeação, que muitos acham ser um grande benefício.
por 11.04.2018 / 04:06
fonte
2

As respostas até agora, todas elas boas, tendo começado com a razoável suposição de que o solicitante está perdendo alguma coisa, o que o consulente também reconhece. Também é possível que o consulente esteja basicamente certo, e vale a pena discutir como esta situação pode acontecer.

Experiência e prática são poderosas, e se os seniores ganharam experiência em projetos grandes e complexos onde a única maneira de manter as coisas sob controle é com muito EntityTransformationServiceImpl , então eles se tornam rápidos e confortáveis com padrões de design e adesão estreita ao DDD. Eles seriam muito menos eficazes usando uma abordagem leve, mesmo para programas pequenos. Como o estranho, você deve se adaptar e será uma ótima experiência de aprendizado.

Ao mesmo tempo em que você se adapta, você deve tomar como uma lição do equilíbrio entre aprender profundamente uma única abordagem, até o ponto de fazer com que ela funcione em qualquer lugar, versus permanecer versátil e saber quais ferramentas estão disponíveis sem necessariamente ser um especialista deles. Há vantagens para ambos e ambos são necessários no mundo.

    
por 13.04.2018 / 08:22
fonte
-4

Criar mais classe e função conforme o uso será uma excelente abordagem para resolver problemas de uma maneira específica que ajudará no futuro a resolver quaisquer problemas.

Várias classes ajudam a identificar como seu trabalho básico e qualquer classe pode ser chamada a qualquer momento.

No entanto, se você tiver muitas classes e funções a partir do nome que pode obter, elas serão fáceis de chamar e gerenciar. Isso é chamado de código limpo.

    
por 13.04.2018 / 17:16
fonte