Gerenciando e organizando o número massivamente aumentado de classes após a mudança para o SOLID?


50

Nos últimos anos, passamos lentamente a mudar para um código escrito progressivamente melhor, alguns pequenos passos de cada vez. Finalmente estamos começando a mudar para algo que pelo menos se assemelha ao SOLID, mas ainda não chegamos lá. Desde a mudança, uma das maiores reclamações dos desenvolvedores é que eles não suportam a revisão por pares e a passagem de dezenas e dezenas de arquivos, onde anteriormente todas as tarefas exigiam apenas que o desenvolvedor tocasse de 5 a 10 arquivos.

Antes de começar a fazer a troca, nossa arquitetura era organizada da seguinte maneira (concedida, com uma ou duas ordens de magnitude a mais de arquivos):

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

Em termos de arquivo, tudo era incrivelmente linear e compacto. Obviamente, havia muita duplicação de código, acoplamento rígido e dores de cabeça, no entanto, todos podiam atravessar e descobrir. Iniciantes completos, pessoas que nunca abriram o Visual Studio, conseguiram descobrir isso em apenas algumas semanas. A falta de complexidade geral de arquivos torna relativamente simples para desenvolvedores iniciantes e novos contratados começarem a contribuir sem muito tempo de aceleração. Mas é aqui que praticamente qualquer benefício do estilo de código sai pela janela.

Eu sinceramente apoio todas as tentativas que fazemos para melhorar nossa base de código, mas é muito comum receber alguma reação do resto da equipe em mudanças de paradigma massivas como essa. Atualmente, alguns dos maiores pontos de discórdia são:

  • Testes unitários
  • Contagem de turmas
  • Complexidade da revisão por pares

Os testes de unidade têm sido incrivelmente difíceis de vender para a equipe, pois todos acreditam que são uma perda de tempo e que podem testar seu código com muito mais rapidez como um todo do que cada peça individualmente. O uso de testes de unidade como um endosso ao SOLID tem sido inútil e tornou-se uma piada neste momento.

A contagem de turmas é provavelmente o maior obstáculo a superar. As tarefas que costumavam levar de 5 a 10 arquivos agora podem demorar de 70 a 100! Embora cada um desses arquivos tenha uma finalidade distinta, o grande volume de arquivos pode ser esmagador. A resposta da equipe tem sido principalmente gemidos e coçar a cabeça. Anteriormente, uma tarefa pode ter exigido um ou dois repositórios, um modelo ou dois, uma camada lógica e um método de controlador.

Agora, para criar um aplicativo simples para salvar arquivos, você tem uma classe para verificar se o arquivo já existe, uma classe para gravar os metadados, uma classe para abstrair, DateTime.Nowpara que você possa injetar horários para testes de unidade, interfaces para cada arquivo que contém lógica, arquivos para conter testes de unidade para cada classe existente e um ou mais arquivos para adicionar tudo ao seu contêiner de DI.

Para aplicações de pequeno e médio porte, o SOLID é uma venda super fácil. Todo mundo vê o benefício e a facilidade de manutenção. No entanto, eles não veem uma boa proposta de valor para o SOLID em aplicativos de grande escala. Por isso, estou tentando encontrar maneiras de melhorar a organização e o gerenciamento para superar as dores do crescimento.


Eu achei que daria um exemplo um pouco mais forte do volume do arquivo com base em uma tarefa concluída recentemente. Foi-me dada a tarefa de implementar algumas funcionalidades em um de nossos microsserviços mais recentes para receber uma solicitação de sincronização de arquivos. Quando a solicitação é recebida, o serviço executa uma série de pesquisas e verificações e, finalmente, salva o documento em uma unidade de rede, além de 2 tabelas de banco de dados separadas.

Para salvar o documento na unidade de rede, eu precisava de algumas classes específicas:

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

Portanto, são 15 classes (excluindo POCOs e andaimes) para realizar um salvamento bastante direto. Esse número aumentou significativamente quando eu precisei criar POCOs para representar entidades em alguns sistemas, criei alguns repositórios para se comunicar com sistemas de terceiros incompatíveis com nossos outros ORMs e criei métodos lógicos para lidar com os meandros de certas operações.


52
"As tarefas que costumavam levar de 5 a 10 arquivos agora podem levar de 70 a 100!" Como diabos? Isso não é de forma alguma normal. Que tipo de mudanças você está fazendo e exige a alteração de tantos arquivos?
Eufórico

43
O fato de você precisar alterar mais arquivos por tarefa (significativamente mais!) Significa que você está fazendo o SOLID errado. O ponto principal é organizar seu código (ao longo do tempo) de uma maneira que reflita os padrões de alterações observados, simplificando as alterações. Todos os princípios do SOLID vêm com um certo raciocínio (quando e por que deve ser aplicado); parece que vocês se envolveram nessa situação aplicando-os às cegas. A mesma coisa com o teste de unidade (TDD); se você está fazendo isso sem ter uma boa noção de como fazer design / arquitetura, vai se aprofundar.
Filip Milovanović 9/07

60
Você adotou claramente o SOLID como uma religião e não como uma ferramenta pragmática para ajudar a realizar o trabalho. Se algo no SOLID estiver tornando mais trabalho ou dificultando as coisas, não faça isso.
whatsisname 9/07

25
@Euphoric: O problema pode ocorrer nos dois sentidos. Eu suspeito que você esteja respondendo à possibilidade de que 70-100 classes sejam um exagero. Mas não é impossível que isso aconteça como um projeto massivo, repleto de 5 a 10 arquivos (já trabalhei em arquivos 20KLOC antes ...) e 70-100 é na verdade a quantidade certa de arquivos.
Flater

18
Há uma desordem de pensamento que chamo de "doença da felicidade do objeto", que é a crença de que as técnicas de OO são um fim em si mesmas, em vez de apenas uma das muitas técnicas possíveis para diminuir os custos de trabalhar em uma grande base de código. Você tem uma forma particularmente avançada, "doença da felicidade sólida". O SOLID não é o objetivo. Reduzir o custo de manutenção da base de código é o objetivo. Avalie suas propostas nesse contexto, não se é doutrinário o SOLID. (Que suas propostas provavelmente também não são realmente doutrinárias SOLID também é um bom ponto a considerar.)
Eric Lippert

Respostas:


104

Agora, para criar um aplicativo simples de salvar arquivos, você tem uma classe para verificar se o arquivo já existe, uma classe para gravar os metadados, uma classe para abstrair o DateTime. Agora, você pode injetar horários para testes de unidade, interfaces para cada arquivo que contém lógica, arquivos para conter testes de unidade para cada classe existente e um ou mais arquivos para adicionar tudo ao seu contêiner de DI.

Eu acho que você não entendeu a idéia de uma única responsabilidade. A única responsabilidade de uma classe pode ser "salvar um arquivo". Para fazer isso, ele pode dividir essa responsabilidade em um método que verifica se existe um arquivo, um método que grava metadados etc. Cada um desses métodos tem uma única responsabilidade, que faz parte da responsabilidade geral da classe.

Uma aula para abstrair DateTime.Nowparece boa. Mas você só precisa de um deles e pode ser agrupado com outros recursos do ambiente em uma única classe, com a responsabilidade de abstrair os recursos ambientais. Novamente, uma única responsabilidade com múltiplas sub-responsabilidades.

Você não precisa de "interfaces para todos os arquivos que contenham lógica", mas de interfaces para classes com efeitos colaterais, por exemplo, aquelas classes que lêem / gravam em arquivos ou bancos de dados; e mesmo assim, eles são necessários apenas para as partes públicas dessa funcionalidade. Assim, por exemplo AccountRepo, talvez você não precise de nenhuma interface, apenas uma interface para o acesso real ao banco de dados que é injetado nesse repositório.

Os testes de unidade têm sido incrivelmente difíceis de vender para a equipe, pois todos acreditam que são uma perda de tempo e que podem testar seu código com muito mais rapidez como um todo do que cada peça individualmente. O uso de testes de unidade como um endosso ao SOLID tem sido inútil e tornou-se uma piada neste momento.

Isso sugere que você também não entendeu os testes de unidade. A "unidade" de um teste de unidade não é uma unidade de código. O que é uma unidade de código? Uma aula? Um método? Uma variável? Uma única instrução de máquina? Não, a "unidade" refere-se a uma unidade de isolamento, ou seja, código que pode ser executado isoladamente de outras partes do código. Um teste simples para determinar se um teste automatizado é um teste de unidade ou não é se você pode executá-lo em paralelo com todos os outros testes de unidade sem afetar o resultado. Existem mais algumas regras práticas nos testes de unidade, mas essa é a sua medida principal.

Portanto, se partes do seu código puderem ser testadas como um todo sem afetar outras, faça isso.

Seja sempre pragmático e lembre-se de que tudo é um compromisso. Quanto mais você aderir ao DRY, mais forte será o seu código. Quanto mais você introduz abstrações, mais fácil o código é testar, mas mais difícil é entender. Evite a ideologia e encontre um bom equilíbrio entre o ideal e mantenha-o simples. Aí reside o ponto ideal da máxima eficiência, tanto para o desenvolvimento quanto para a manutenção.


27
Eu gostaria de acrescentar que uma dor de cabeça semelhante surge quando as pessoas tentam aderir ao mantra repetido com frequência de "métodos devem fazer apenas uma coisa" e acabam com vários métodos de uma linha apenas porque tecnicamente pode ser transformada em método .
Logarr

8
Re "Seja sempre pragmático e lembre-se de que tudo é um compromisso" : os discípulos do tio Bob não são conhecidos por isso (não importa a intenção original).
Peter Mortensen

13
Para resumir a primeira parte, você geralmente tem um estagiário de café, e não um conjunto completo de plug-in-coador, chave seletora, cheque se o açúcar precisa de recarga, geladeira aberta, leite extraído, - estagiários, copos de despejo, despeje café, adicione açúcar, adicione leite, misture copos e estagiários de entrega de copos. ; P
Justin Time 2 Restabelecer Monica

12
A causa raiz do problema do OP parece estar entendendo mal a diferença entre as funções que devem executar uma única tarefa e as classes que devem ter uma única responsabilidade.
alephzero 9/07

6
"As regras são para a orientação dos sábios e a obediência dos tolos." - Douglas Bader
Calanus

29

As tarefas que costumavam levar de 5 a 10 arquivos agora podem demorar de 70 a 100!

Isso é o oposto do princípio de responsabilidade única (SRP). Para chegar a esse ponto, você deve ter dividido sua funcionalidade de maneira muito refinada, mas não é disso que trata o SRP - isso ignora a ideia principal de coesão .

De acordo com o SRP, o software deve ser dividido em módulos ao longo das linhas definidas por suas possíveis razões para alterar, de modo que uma única alteração no projeto possa ser aplicada em apenas um módulo sem exigir modificações em outros lugares. Um único "módulo" nesse sentido pode corresponder a mais de uma classe, mas se uma alteração exigir que você toque em dezenas de arquivos, serão realmente várias alterações ou você estará fazendo o SRP errado.

Bob Martin, que originalmente formulou o SRP, escreveu um post no blog alguns anos atrás para tentar esclarecer a situação. Ele discute detalhadamente o que é um "motivo para mudar" para os propósitos do SRP. Vale a pena ler na íntegra, mas entre as coisas que merecem atenção especial está esta redação alternativa do SRP:

Reúna as coisas que mudam pelas mesmas razões . Separe as coisas que mudam por diferentes motivos.

(ênfase minha). O SRP não é sobre dividir as coisas nas menores partes possíveis. Isso não é um bom design, e sua equipe está certa em resistir. Isso torna sua base de código mais difícil de atualizar e manter. Parece que você pode estar tentando vender sua equipe com base em considerações de teste de unidade, mas isso colocaria o carrinho à frente do cavalo.

Da mesma forma, o princípio de segregação da interface não deve ser tomado como absoluto. Não é mais um motivo para dividir o seu código tão finamente do que o SRP, e geralmente ele se alinha muito bem com o SRP. O fato de uma interface conter alguns métodos que alguns clientes não usam não é um motivo para quebrá-la. Você está novamente procurando por coesão.

Além disso, peço que você não tome o princípio de aberto-fechado ou o princípio de substituição de Liskov como uma razão para favorecer hierarquias profundas de herança. Não há acoplamento mais apertado do que uma subclasse com suas superclasses, e o acoplamento apertado é um problema de design. Em vez disso, favorece a composição sobre a herança, sempre que fizer sentido. Isso reduzirá seu acoplamento e, portanto, o número de arquivos que uma alteração específica pode precisar tocar, e se alinha perfeitamente à inversão de dependência.


11
Acho que estou apenas tentando descobrir onde está a linha. Em uma tarefa recente, tive que executar uma operação bastante simples, mas estava em uma base de código sem muitos andaimes ou funcionalidades existentes. Como tal, tudo o que eu precisava fazer era muito simples, mas tudo muito único e não parecia se encaixar em classes compartilhadas. No meu caso, eu precisava salvar um documento em uma unidade de rede e registrá-lo em duas tabelas de banco de dados separadas. As regras em torno de cada etapa eram bastante particulares. Até a geração do nome do arquivo (um guia simples) tinha algumas classes para tornar o teste mais conveniente.
JD Davis

3
Mais uma vez, o @JDDavis, escolher várias classes em vez de uma única para fins de testabilidade é colocar o carrinho à frente do cavalo e vai diretamente contra o SRP, que exige o agrupamento de funcionalidades coesas. Não posso aconselhá-lo em detalhes, mas o problema de que as alterações funcionais individuais exigem a modificação de muitos arquivos é um problema que você deve abordar (e tentar evitar), e não aquele que deveria tentar justificar.
John Bollinger

Concordo, eu adiciono isso. Para citar a Wikipedia, "Martin define uma responsabilidade como uma razão para mudar e conclui que uma classe ou módulo deve ter uma e apenas uma razão para ser alterada (ou seja, reescrita)". e "ele declarou mais recentemente" Este princípio é sobre as pessoas "." Na verdade, acredito que isso significa que a "responsabilidade" no SRP refere-se às partes interessadas, não à funcionalidade. Uma classe deve ser responsável pelas mudanças exigidas por apenas uma parte interessada (pessoa que exige que você altere seu programa), para que você mude o menor número possível de coisas em resposta a diferentes partes interessadas que exigem mudanças.
Corrodias 11/07

12

As tarefas que costumavam levar de 5 a 10 arquivos agora podem demorar de 70 a 100!

Isso é uma mentira. As tarefas nunca levaram apenas 5 a 10 arquivos.

Você não está resolvendo nenhuma tarefa com menos de 10 arquivos. Por quê? Porque você está usando c #. C # é uma linguagem de alto nível. Você está usando mais de 10 arquivos apenas para criar um olá mundo.

Ah, claro que você não os percebe porque não os escreveu. Então você não olha neles. Você confia neles.

O problema não é o número de arquivos. Agora você tem tanta coisa que não confia.

Então, descubra como fazer esses testes funcionarem a tal ponto que, uma vez aprovados, você confia nesses arquivos da mesma maneira que confia nos arquivos do .NET. Fazer isso é o ponto do teste de unidade. Ninguém se importa com o número de arquivos. Eles se preocupam com o número de coisas em que não podem confiar.

Para aplicações de pequeno e médio porte, o SOLID é uma venda super fácil. Todo mundo vê o benefício e a facilidade de manutenção. No entanto, eles não veem uma boa proposta de valor para o SOLID em aplicativos de grande escala.

A mudança é difícil em aplicações de larga escala, não importa o que você faça. A melhor sabedoria a aplicar aqui não vem do tio Bob. Vem de Michael Feathers em seu livro Working Effective with Legacy Code.

Não inicie um fest de reescrita. O código antigo representa um conhecimento adquirido com dificuldade. Jogá-lo fora porque tem problemas e não é expresso no novo e aprimorado paradigma X está apenas pedindo um novo conjunto de problemas e nenhum conhecimento adquirido com força.

Em vez disso, encontre maneiras de tornar seu código antigo e não testável (código legado no Feathers speak). Nesta metáfora, o código é como uma camisa. Peças grandes são unidas em costuras naturais que podem ser desfeitas para separar o código da maneira que você remove as costuras. Faça isso para anexar "mangas" de teste que permitem isolar o restante do código. Agora, quando você cria as mangas de teste, confia nas mangas porque fez isso com uma camisa de trabalho. (ow, essa metáfora está começando a doer).

Essa idéia parte da suposição de que, como na maioria das lojas, os únicos requisitos atualizados estão no código de trabalho. Isso permite que você troque isso em testes que permitem fazer alterações no código de trabalho comprovado sem que ele perca todos os seus status de trabalho comprovado. Agora, com essa primeira onda de testes, você pode começar a fazer alterações que tornam o código "legado" (não testável) testável. Você pode ser ousado, porque os testes de costuras estão apoiando você dizendo que isso é o que sempre fazia e os novos testes mostram que seu código realmente faz o que você pensa.

O que isso tem a ver com:

Gerenciando e organizando o número massivamente aumentado de classes após a mudança para o SOLID?

Abstração.

Você pode me fazer odiar qualquer base de código com más abstrações. Uma má abstração é algo que me faz olhar para dentro. Não me surpreenda quando olho para dentro. Seja praticamente o que eu esperava.

Dê-me um bom nome, testes legíveis (exemplos) que mostram como usar a interface e organizá-la para que eu possa encontrar coisas e não me importo se usamos 10, 100 ou 1000 arquivos.

Você me ajuda a encontrar coisas com bons nomes descritivos. Coloque coisas com bons nomes em coisas com bons nomes.

Se você fizer tudo isso corretamente, você abstrairá os arquivos para onde a tarefa está concluída, dependendo de 3 a 5 outros arquivos. Os arquivos 70-100 ainda estão lá. Mas eles estão se escondendo atrás dos 3 a 5. Isso só funciona se você confiar nos 3 a 5 para fazer isso direito.

Então, o que você realmente precisa é o vocabulário para criar bons nomes para todas essas coisas e testes nas quais as pessoas confiam, para que parem de percorrer tudo. Sem isso, você também estaria me deixando louco.

@Delioth faz um bom argumento sobre dores de crescimento. Quando você está acostumado com a louça no armário acima da lava-louça, é preciso algum tempo para se acostumar a estar acima da barra de café da manhã. Torna algumas coisas mais difíceis. Facilita algumas coisas. Mas isso causa todos os tipos de pesadelos se as pessoas não concordam onde os pratos vão. Em uma grande base de código, o problema é que você só pode mover alguns pratos de cada vez. Então agora você tem pratos em dois lugares. É confuso. Torna difícil confiar que os pratos estejam onde deveriam estar. Se você quiser superar isso, a única coisa a fazer é continuar mexendo na louça.

O problema é que você realmente gostaria de saber se vale a pena lavar os pratos na barra de café da manhã antes de passar por toda essa bobagem. Bem, para isso tudo o que posso recomendar é acampar.

Ao experimentar um novo paradigma pela primeira vez, o último local que você deve aplicá-lo é em uma grande base de código. Isso vale para todos os membros da equipe. Ninguém deve acreditar que o SOLID funciona, que OOP funciona ou que a programação funcional funciona. Todo membro da equipe deve ter a chance de brincar com a nova idéia, seja ela qual for, em um projeto de brinquedo. Isso permite que eles vejam pelo menos como funciona. Isso permite que eles vejam o que não faz bem. Isso permite que eles aprendam a fazer isso antes de fazer uma grande bagunça.

Dar às pessoas um lugar seguro para brincar as ajudará a adotar novas idéias e a confiar que os pratos realmente podem funcionar em seu novo lar.


3
Vale a pena mencionar que parte da dor da pergunta provavelmente também está aumentando - enquanto, sim, eles podem precisar fazer 15 arquivos para uma coisa ... agora eles nunca precisam escrever um GUIDProvider novamente ou um BasePathProvider , ou um ExtensionProvider etc. É o mesmo tipo de obstáculo que você obtém ao iniciar um novo projeto greenfield - vários recursos de suporte que são na maioria triviais, estúpidos de escrever e ainda precisam ser escritos. É péssimo construí-los, mas quando eles estão lá, você não precisa pensar neles ... nunca.
Delioth 10/07

@ Delioth Estou incrivelmente inclinado a acreditar que este é o caso. Anteriormente, se precisávamos de algum subconjunto de funcionalidades (digamos que queríamos apenas um URL hospedado no AppSettings), simplesmente tínhamos uma classe massiva que era distribuída e usada. Com a nova abordagem, não há motivo para repassar a totalidade AppSettingsapenas para obter um caminho de URL ou arquivo.
JD Davis

11
Não inicie um fest de reescrita. O código antigo representa um conhecimento adquirido com dificuldade. Jogá-lo fora porque tem problemas e não é expresso no novo e aprimorado paradigma X está apenas pedindo um novo conjunto de problemas e nenhum conhecimento adquirido com dificuldade. Este. Absolutamente.
Flot2011

10

Parece que seu código não está muito bem dissociado e / ou o tamanho das suas tarefas é muito grande.

As alterações de código devem ter entre 5 e 10 arquivos, a menos que você esteja executando um refecho de código ou em larga escala. Se uma única alteração tocar muitos arquivos, provavelmente significa que suas alterações estão em cascata. Algumas abstrações aprimoradas (responsabilidade mais única, segregação de interface, inversão de dependência) devem ajudar. Também é possível que você tenha assumido uma responsabilidade única e possa usar um pouco mais de pragmatismo - hierarquias mais curtas e mais finas. Isso também deve facilitar a compreensão do código, já que você não precisa entender dezenas de arquivos para saber o que o código está fazendo.

Também pode ser um sinal de que seu trabalho é muito grande. Em vez de "ei, adicione esse recurso" (que requer alterações na interface do usuário e alterações de API e alterações de acesso a dados e alterações de segurança e alterações de teste e ...), divida-as em partes mais úteis. Isso fica mais fácil de revisar e mais fácil de entender, porque exige que você estabeleça contratos decentes entre os bits.

E, é claro, os testes de unidade ajudam tudo isso. Eles forçam você a fazer interfaces decentes. Eles o forçam a tornar seu código flexível o suficiente para injetar os bits necessários para testar (se for difícil testar, será difícil reutilizar). E eles afastam as pessoas de coisas com excesso de engenharia, porque quanto mais você projeta, mais precisa testar.


2
Os arquivos de 5 a 10 a 70 a 100 são um pouco mais do que hipotéticos. Minha última tarefa foi criar algumas funcionalidades em um dos nossos microsserviços mais recentes. O novo serviço deveria receber uma solicitação e salvar um documento. Ao fazer isso, eu precisava de classes para representar as entidades do usuário em 2 bancos de dados e repositórios separados para cada um. Repos para representar outras tabelas nas quais eu precisava escrever. Classes dedicadas para lidar com a verificação de dados de arquivos e a geração de nomes. E a lista continua. Sem mencionar, todas as classes que continham lógica eram representadas por uma interface para que pudessem ser simuladas para testes de unidade.
JD Davis

11
No que diz respeito às nossas bases de código mais antigas, elas são todas fortemente acopladas e incrivelmente monolíticas. Com a abordagem do SOLID, o único acoplamento entre as classes ocorreu no caso dos POCOs; todo o resto é passado através do DI e das interfaces.
JD Davis

3
@JDDavis - espere, por que um microsserviço está trabalhando diretamente com vários bancos de dados?
Telastyn

11
Foi um compromisso com nosso gerente de desenvolvimento. Ele prefere maciçamente software monolítico e processual. Como tal, nossos microsserviços são muito mais macro do que deveriam ser. À medida que nossa infraestrutura melhora, lentamente as coisas passam para seus próprios microsserviços. Por enquanto, estamos seguindo um pouco a abordagem do estrangulador para mover determinadas funcionalidades para os microsserviços. Como vários serviços precisam acessar um recurso específico, também os estamos transferindo para seus próprios microsserviços.
JD Davis

4

Eu gostaria de expor algumas das coisas já mencionadas aqui, mas mais de uma perspectiva de onde os limites dos objetos são traçados. Se você está seguindo algo parecido com o Design Orientado a Domínio, é provável que seus objetos representem aspectos de seus negócios. Customere Order, por exemplo, seriam objetos. Agora, se eu fizesse um palpite com base nos nomes das classes que você tinha como ponto de partida, sua AccountLogicclasse tinha um código que seria executado em qualquer conta. No OO, no entanto, cada classe deve ter contexto e identidade. Você não deve obter um Accountobjeto e depois passá-lo para uma AccountLogicclasse e fazer com que essa classe faça alterações no Accountobjeto. Isso é chamado de modelo anêmico e não representa muito bem o OO. Em vez disso, seuAccountA classe deve ter um comportamento, como Account.Close()ou Account.UpdateEmail(), e esses comportamentos afetariam apenas a instância da conta.

Agora, COMO esses comportamentos são tratados pode (e em muitos casos deve) ser descarregado para dependências representadas por abstrações (ou seja, interfaces). Account.UpdateEmail, por exemplo, pode querer atualizar um banco de dados ou um arquivo ou enviar uma mensagem para um barramento de serviço etc. E isso pode mudar no futuro. Portanto, sua Accountclasse pode ter uma dependência, por exemplo, de um IEmailUpdate, que poderia ser uma das muitas interfaces implementadas por um AccountRepositoryobjeto. Você não gostaria de passar uma IAccountRepositoryinterface inteira para o Accountobjeto, porque provavelmente faria muito, como pesquisar e encontrar outras (quaisquer) contas, às quais você talvez não queira que o Accountobjeto tenha acesso, mas mesmo que AccountRepositorypossa implementar as duas IAccountRepositorye IEmailUpdateinterfaces, oAccountO objeto teria acesso apenas às pequenas porções necessárias. Isso ajuda a manter o Princípio de Segregação de Interface .

Realisticamente, como outras pessoas mencionaram, se você está lidando com uma explosão de classes, é provável que esteja usando o princípio SOLID (e, por extensão, OO) da maneira errada. O SOLID deve ajudar você a simplificar seu código, não complicá-lo. Mas leva tempo para realmente entender o que significam coisas como o SRP. O mais importante, no entanto, é que o funcionamento do SOLID dependerá muito do seu domínio e contextos limitados (outro termo DDD). Não há bala de prata ou tamanho único.

Mais uma coisa que gosto de enfatizar para as pessoas com quem trabalho: novamente, um objeto OOP deve ter comportamento e é, de fato, definido por seu comportamento, não por seus dados. Se o seu objeto não tiver nada além de propriedades e campos, ele ainda terá comportamento, embora provavelmente não seja o comportamento que você pretendia. Uma propriedade gravável publicamente / configurável sem outra lógica de conjunto implica que o comportamento para sua classe que contém é que qualquer pessoa em qualquer lugar, por qualquer motivo e a qualquer momento, possa modificar o valor dessa propriedade sem nenhuma lógica ou validação comercial necessária. Geralmente, esse não é o comportamento que as pessoas pretendem, mas se você tem um modelo anêmico, geralmente é o comportamento que suas classes estão anunciando para quem as usa.


2

Portanto, são 15 classes (excluindo POCOs e andaimes) para realizar um salvamento bastante direto.

Isso é loucura ... mas essas aulas parecem algo que eu mesmo escreveria. Então, vamos dar uma olhada neles. Vamos ignorar as interfaces e testes por enquanto.

  • BasePathProvider- IMHO qualquer projeto não trivial trabalhando com arquivos precisa dele. Então, eu diria que já existe e você pode usá-lo como está.
  • UniqueFilenameProvider - Claro, você já tem, não é?
  • NewGuidProvider - O mesmo caso, a menos que você esteja apenas olhando para usar o GUID.
  • FileExtensionCombiner - O mesmo caso.
  • PatientFileWriter - Acho que essa é a classe principal da tarefa atual.

Para mim, parece bom: você precisa escrever uma nova classe que precise de quatro classes auxiliares. Todas as quatro classes auxiliares parecem bastante reutilizáveis, então eu aposto que elas já estão em algum lugar na sua base de código. Caso contrário, é azar (você é realmente a pessoa da sua equipe para gravar arquivos e usar GUIDs ???) ou algum outro problema.


Com relação às classes de teste, com certeza, quando você cria uma nova classe ou atualiza-a, ela deve ser testada. Portanto, escrever cinco classes significa escrever cinco classes de teste também. Mas isso não torna o design mais complicado:

  • Você nunca usará as classes de teste em outros lugares, pois elas serão executadas automaticamente e isso é tudo.
  • Você deseja vê-los novamente, a menos que você atualize as classes em teste ou a menos que as utilize como documentação (idealmente, os testes mostram claramente como uma classe deve ser usada).

Com relação às interfaces, elas são necessárias apenas quando sua estrutura de DI ou sua estrutura de teste não pode lidar com classes. Você pode vê-los como um pedágio para ferramentas imperfeitas. Ou você pode vê-las como uma abstração útil, permitindo esquecer que existem coisas mais complicadas - ler a fonte de uma interface leva muito menos tempo do que ler a fonte de sua implementação.


Sou grato por esse ponto de vista. Nesse caso específico, eu estava escrevendo a funcionalidade em um microsserviço relativamente novo. Infelizmente, mesmo em nossa principal base de código, embora tenhamos algumas das opções acima em uso, nenhuma delas é realmente de maneira reutilizável remotamente. Tudo o que precisa ser reutilizável acabou em alguma classe estática ou é apenas copiar e colar o código. Acho que ainda estou indo um pouco longe, mas concordo que nem tudo precisa ser completamente dissecado e dissociado.
JD Davis

@JDDavis Eu estava tentando escrever algo diferente das outras respostas (com as quais concordo principalmente). Sempre que você copia e cola algo, evita a reutilização; em vez de generalizar algo, cria outro pedaço de código não reutilizável, que o forçará a copiar e colar mais um dia. IMHO é o segundo maior pecado, logo após seguir cegamente as regras. Você precisa encontrar o seu ponto ideal, onde as regras a seguir o tornam mais produtivo (principalmente as mudanças futuras) e ocasionalmente quebrá-las um pouco ajuda nos casos em que o esforço não seria inadequado. É tudo relativo.
maaartinus 11/07

@JDDavis E tudo depende da qualidade de suas ferramentas. Exemplo: existem pessoas que afirmam que a DI é empreendedora e complicada, enquanto eu afirmo que a maioria é gratuita . +++Com relação à quebra das regras: são necessárias quatro classes em alguns lugares, onde eu só poderia injetá-las após uma grande refatoração, tornando o código mais feio (pelo menos para os meus olhos), então decidi fazê-las em singletons (um programador melhor) pode encontrar uma maneira melhor, mas estou feliz com isso; o número desses singletons não muda desde as idades).
maaartinus 11/07

Essa resposta expressa praticamente o que eu estava pensando quando o OP adicionou o exemplo à pergunta. @JDDavis Deixe-me acrescentar que você pode salvar alguns códigos / classes padrão usando ferramentas funcionais para casos simples. Um provedor de GUI, por exemplo - em vez de invadir uma nova interface, uma nova classe para isso, por que não apenas utilizá Func<Guid>-lo e injetar um método anônimo como ()=>Guid.NewGuid()no construtor? E não há necessidade de testar essa função de estrutura .Net, isso é algo que a Microsoft fez por você. No total, você economizará 4 aulas.
Doc Brown

... e verifique se os outros casos que você apresentou podem ser simplificados da mesma maneira (provavelmente nem todos).
Doc Brown

2

Dependendo das abstrações, criar classes de responsabilidade única e escrever testes de unidade não são ciências exatas. É perfeitamente normal ir muito longe em uma direção ao aprender, ir ao extremo e depois encontrar uma norma que faça sentido. Parece que o seu pêndulo ficou muito longe e pode até ficar preso.

Aqui é onde eu suspeito que isso está saindo dos trilhos:

Os testes de unidade têm sido incrivelmente difíceis de vender para a equipe, pois todos acreditam que são uma perda de tempo e que podem testar seu código com muito mais rapidez como um todo do que cada peça individualmente. O uso de testes de unidade como um endosso ao SOLID tem sido inútil e tornou-se uma piada neste momento.

Um dos benefícios que advém da maioria dos princípios do SOLID (certamente não é o único benefício) é que facilita a gravação de testes de unidade para o nosso código. Se uma classe depende de abstrações, podemos zombar das abstrações. Abstrações que são segregadas são mais fáceis de zombar. Se uma classe faz uma coisa, é provável que tenha menor complexidade, o que significa que é mais fácil conhecer e testar todos os caminhos possíveis.

Se sua equipe não está escrevendo testes de unidade, duas coisas relacionadas estão acontecendo:

Primeiro, eles estão fazendo muito trabalho extra para criar todas essas interfaces e classes sem perceber todos os benefícios. Leva um pouco de tempo e prática para ver como escrever testes de unidade facilita nossa vida. Há razões pelas quais as pessoas que aprendem a escrever testes de unidade cumprem isso, mas você precisa persistir o tempo suficiente para descobri-las por si mesmo. Se sua equipe não está tentando fazer isso, eles sentirão que o resto do trabalho extra que eles estão fazendo é inútil.

Por exemplo, o que acontece quando eles precisam refatorar? Se eles tiverem centenas de turmas pequenas, mas não tiverem testes para dizer se suas alterações funcionarão ou não, essas classes e interfaces extras parecerão um fardo, não uma melhoria.

Segundo, escrever testes de unidade pode ajudá-lo a entender quanta abstração seu código realmente precisa. Como eu disse, não é uma ciência. Começamos mal, virando por todo o lado e melhorando. Os testes de unidade têm uma maneira peculiar de complementar o SOLID. Como você sabe quando precisa adicionar uma abstração ou separar algo? Em outras palavras, como você sabe quando está "suficientemente sólido"? Muitas vezes, a resposta é quando você não pode testar alguma coisa.

Talvez seu código possa ser testado sem criar tantas abstrações e classes minúsculas. Mas se você não está escrevendo os testes, como pode saber? Até onde vamos? Podemos ficar obcecados em dividir as coisas cada vez menos. É uma toca de coelho. A capacidade de escrever testes para o nosso código nos ajuda a ver quando cumprimos nosso objetivo, para que possamos parar de ficar obcecados, seguir em frente e nos divertir escrevendo mais código.

Os testes de unidade não são uma bala de prata que resolve tudo, mas são uma bala realmente impressionante que melhora a vida dos desenvolvedores. Não somos perfeitos, nem nossos testes. Mas os testes nos dão confiança. Esperamos que nosso código esteja correto e ficamos surpresos quando está errado, e não o contrário. Não somos perfeitos e nem nossos testes. Mas quando nosso código é testado, temos confiança. Estamos menos propensos a roer as unhas quando nosso código é implantado e nos perguntamos o que vai quebrar dessa vez e se será nossa culpa.

Além disso, assim que pegamos o jeito, escrever testes de unidade torna o desenvolvimento do código mais rápido, não mais lento. Passamos menos tempo revisitando o código antigo ou depurando para encontrar problemas que são como agulhas no palheiro.

Os erros diminuem, fazemos mais e substituímos a ansiedade pela confiança. Não é uma moda passageira ou óleo de cobra. É real. Muitos desenvolvedores atestam isso. Se sua equipe ainda não experimentou isso, eles precisam avançar nessa curva de aprendizado e superar o problema. Dê uma chance, percebendo que eles não obterão resultados instantaneamente. Mas quando isso acontecer, eles ficarão felizes em ter feito e nunca olharão para trás. (Ou eles se tornarão párias isolados e escreverão posts irritados no blog sobre como os testes de unidade e a maioria dos outros conhecimentos acumulados de programação são uma perda de tempo.)

Desde a mudança, uma das maiores reclamações dos desenvolvedores é que eles não suportam a revisão por pares e a passagem de dezenas e dezenas de arquivos, onde anteriormente todas as tarefas exigiam apenas que o desenvolvedor tocasse de 5 a 10 arquivos.

A revisão por pares é muito mais fácil quando todos os testes de unidade são aprovados e grande parte dessa revisão é apenas garantir que os testes sejam significativos.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.