Princípio de responsabilidade única - como posso evitar a fragmentação de código?

57

Estou trabalhando em uma equipe em que o líder da equipe é um defensor virulento dos princípios de desenvolvimento do SOLID. No entanto, ele não tem muita experiência em obter software complexo fora da porta.

Temos uma situação em que ele aplicou o SRP ao que já era uma base de código bastante complexa, que agora se tornou muito fragmentada e difícil de entender e depurar.

Agora, temos um problema não apenas com a fragmentação do código, mas também com o encapsulamento, pois os métodos dentro de uma classe que podem ter sido privados ou protegidos foram julgados como representando um 'motivo para mudar' e foram extraídos para classes e interfaces públicas ou internas que não está de acordo com os objetivos de encapsulamento do aplicativo.

Como temos alguns construtores de classe que assumem mais de 20 parâmetros de interface, nosso registro e resolução de IoC estão se tornando um monstro por si só.

Quero saber se existe alguma abordagem de 'refatorar longe do SRP' que possamos usar para ajudar a corrigir alguns desses problemas. Li que não viola o SOLID se eu criar várias classes de granulação mais grossa e vazias que 'agrupam' várias classes estreitamente relacionadas para fornecer um ponto único de acesso à soma de sua funcionalidade (por exemplo, imitando uma implementação de classe excessivamente SRP).

Além disso, não consigo pensar em uma solução que nos permita continuar pragmaticamente com nossos esforços de desenvolvimento, mantendo todos felizes.

Alguma sugestão ?

Dean Chalk
fonte
18
Essa é apenas a minha opinião, mas acho que há mais uma regra, que é facilmente esquecida sob a pilha de várias siglas - "Princípio do senso comum". Quando uma 'solução' cria mais problemas do que realmente resolve, algo está errado. Minha opinião é que, se um problema é complexo, mas está incluído em uma classe que cuida de suas complexidades e ainda é relativamente fácil de depurar - eu estou deixando isso em paz. Geralmente, sua idéia de 'invólucro' me parece boa, mas deixarei a resposta para alguém mais experiente.
Patryk Ćwiek
6
Quanto à "razão para mudar" - não há necessidade de especular todas as razões prematuramente. Espere até que você realmente precise mudar isso e veja o que pode ser feito para facilitar esse tipo de mudança no futuro.
62
Uma classe com 20 parâmetros de construtor não me parece muito SRP!
MattDavey
11
Você escreve "... registro e resolução de IoC ..."; isso soa como se você (ou o líder da sua equipe) pensasse que "IoC" e "injeção de dependência" (DI) são a mesma coisa, o que não é verdade. O DI é um meio de atingir a IoC, mas certamente não é o único. Você deve analisar cuidadosamente por que deseja fazer IoC; se for porque você deseja escrever testes de unidade, você também pode tentar usar o padrão de localizador de serviço ou simplesmente interface de classes ( ISomething). IMHO, essas abordagens são muito mais fáceis de manipular do que a injeção de dependência e resultam em código mais legível.
2
qualquer resposta dada aqui estaria no vácuo; teríamos que ver o código para dar uma resposta específica. 20 parâmetros em um construtor? bem, você pode estar perdendo um objeto ... ou todos podem ser válidos; ou eles podem pertencer a um arquivo de configuração, ou podem pertencer a uma classe DI, ou ... Os sintomas certamente parecem suspeitos, mas como a maioria das coisas no CS, "depende" ...
Steven A. Lowe

Respostas:

85

Se sua classe tem 20 parâmetros no construtor, não parece que sua equipe sabe o que é SRP. Se você tem uma classe que faz apenas uma coisa, como tem 20 dependências? É como ir em uma viagem de pesca e levar uma vara de pescar, caixa de equipamento, suprimentos para acolchoados, bola de boliche, nunchucks, lança-chamas, etc.

Dito isto, o SRP, como a maioria dos princípios existentes, pode ser aplicado em excesso. Se você criar uma nova classe para incrementar números inteiros, sim, isso pode ser uma responsabilidade única, mas vamos lá. Isso é ridículo. Tendemos a esquecer que coisas como os princípios do SOLID existem para um propósito. O SOLID é um meio para um fim, não um fim em si mesmo. O fim é a manutenção . Se você deseja obter essa granularidade com o Princípio de responsabilidade única, é um indicador de que o zelo pelo SOLID cegou a equipe ao objetivo do SOLID.

Então, acho que o que estou dizendo é ... O SRP não é seu problema. É um mal-entendido do SRP ou uma aplicação incrivelmente granular. Tente fazer com que sua equipe mantenha a coisa principal a principal. E o principal é a manutenção.

EDITAR

Faça com que as pessoas projetem módulos de uma maneira que incentive a facilidade de uso. Pense em cada classe como uma mini API. Pense primeiro em "Como eu gostaria de usar essa classe" e depois implemente-a. Não pense apenas "O que essa classe precisa fazer". O SRP tem uma grande tendência a tornar as aulas mais difíceis de usar, se você não pensar muito em usabilidade.

EDIT 2

Se você está procurando dicas sobre refatoração, pode começar a fazer o que sugeriu - criar classes de granulação mais grossa para agrupar várias outras. Verifique se a classe de granulação mais grossa ainda está aderindo ao SRP , mas em um nível superior. Então você tem duas alternativas:

  1. Se as classes de granulação mais fina não forem mais usadas em outras partes do sistema, você poderá gradualmente puxar sua implementação para a classe de granulação mais grossa e excluí-las.
  2. Deixe as classes mais refinadas em paz. Talvez eles tenham sido bem projetados e você só precise do invólucro para torná-los mais fáceis de usar. Suspeito que esse seja o caso de grande parte do seu projeto.

Quando você terminar a refatoração (mas antes de se comprometer com o repositório), revise seu trabalho e pergunte a si mesmo se a refatoração foi realmente uma melhoria da capacidade de manutenção e facilidade de uso.

Phil
fonte
2
Uma maneira alternativa de levar as pessoas a pensar em criar classes: deixe-as escrever cartões CRC (nome da classe, responsabilidade, colaboradores) . Se uma classe tem muitos colaboradores ou responsabilidades, provavelmente não é suficiente o SRP. Em outras palavras, todo o texto deve caber no cartão de índice ou está fazendo muito.
Spoike
18
Eu sei para que serve o lança-chamas, mas como diabos você pesca com uma vara?
R. Martinho Fernandes
13
+1 SOLID é um meio para um fim, não um fim em si mesmo.
B Sete
11
+1: Argumentei antes que coisas como "A Lei de Demeter" são nomeadas incorretamente, deve ser "A linha guia de Demeter". Essas coisas devem funcionar para você, você não deveria estar trabalhando para elas.
Preocupante binário
2
@EmmadKareem: É verdade que os objetos DAO devem ter várias propriedades. Mas, novamente, há várias coisas que você pode agrupar em algo tão simples quanto uma Customerclasse e ter um código mais sustentável. Veja exemplos aqui: codemonkeyism.com/...
Spoike
33

Acho que é na refatoração de Martin Fowler que li uma contra-regra para o SRP, definindo para onde está indo longe demais. Há uma segunda pergunta, tão importante quanto "toda classe tem apenas um motivo para mudar?" e isso é "toda mudança afeta apenas uma classe?"

Se a resposta para a primeira pergunta for, em todos os casos, "yes", mas a segunda questão for "nem mesmo perto", será necessário examinar novamente como você está implementando o SRP.

Por exemplo, se adicionar um campo a uma tabela significa que você precisa alterar um DTO e uma classe de validador e uma classe de persistência e um objeto de modelo de exibição e assim por diante, então você criou um problema. Talvez você deva repensar como implementou o SRP.

Talvez você tenha dito que adicionar um campo é o motivo para alterar o objeto Customer, mas alterar a camada de persistência (digamos, de um arquivo XML para um banco de dados) é outro motivo para alterar o objeto Customer. Então você decide criar um objeto CustomerPersistence também. Mas se você fizer isso de forma que adicionar um campo AINDA exija uma alteração no objeto CustomerPersisitence, qual era o objetivo? Você ainda tem um objeto com dois motivos para mudar - ele não é mais o Cliente.

No entanto, se você introduzir um ORM, é bem possível que as classes funcionem de forma que, se você adicionar um campo ao DTO, ele alterará automaticamente o SQL usado para ler esses dados. Então você tem um bom motivo para separar as duas preocupações.

Em resumo, eis o que eu costumo fazer: se houver um equilíbrio aproximado entre o número de vezes que digo "não, há mais de um motivo para mudar esse objeto" e o número de vezes que digo "não, essa alteração será afetam mais de um objeto ", acho que tenho o equilíbrio certo entre SRP e fragmentação. Mas se os dois ainda estão altos, começo a me perguntar se há uma maneira diferente de separar as preocupações.

pdr
fonte
+1 em "todas as alterações afetam apenas uma classe?"
dj18
Uma questão relacionada que não vi discutida é que, se as tarefas vinculadas a uma entidade lógica forem fragmentadas entre classes diferentes, talvez seja necessário que o código mantenha referências a vários objetos distintos, todos vinculados à mesma entidade. Considere, por exemplo, um forno com as funções "SetHeaterOutput" e "MeasureTemperature". Se o forno fosse representado pelos objetos HeaterControl e TemperatureSensor independentes, nada impediria um objeto TemperatureFeedbackSystem de manter uma referência ao aquecedor de um forno e a um sensor de temperatura diferente do forno.
Supercat 11/14
11
Se, em vez disso, essas funções fossem combinadas em uma interface IKiln, implementada por um objeto Kiln, o TemperatureFeedbackSystem precisaria manter apenas uma única referência IKiln. Se fosse necessário usar um forno com um sensor de temperatura independente para o mercado de reposição, seria possível usar um objeto CompositeKiln cujo construtor aceitasse um IHeaterControl e ITemperatureSensor e os usasse para implementar o IKiln, mas essa composição solta deliberada seria facilmente reconhecível no código.
Supercat 11/14
24

Só porque um sistema é complexo, não significa que você precise complicá-lo . Se você tem uma classe que possui muitas dependências (ou Colaboradores) como esta:

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

... então ficou muito complicado e você realmente não está seguindo o SRP , está? Aposto que, se você anotasse o que MyAwesomeClassfaz em um cartão CRC, ele não caberia em um cartão de índice ou você teria que escrever em letras minúsculas ilegíveis.

O que você tem aqui é que vocês seguiram apenas o Princípio de Segregação de Interface e podem ter levado ao extremo, mas essa é outra história. Você pode argumentar que as dependências são objetos de domínio (o que acontece), no entanto, ter uma classe que lida com 20 objetos de domínio ao mesmo tempo está estendendo um pouco demais.

O TDD fornecerá um bom indicador de quanto uma classe faz. Sem rodeios; se um método de teste possui um código de configuração que leva uma eternidade para ser gravado (mesmo se você refatorar os testes), MyAwesomeClassprovavelmente você tem muitas coisas a fazer.

Então, como você resolve esse enigma? Você move as responsabilidades para outras classes. Existem algumas etapas que você pode executar em uma classe com esse problema:

  1. Identifique todas as ações (ou responsabilidades) que sua classe realiza com suas dependências.
  2. Agrupe as ações de acordo com dependências estreitamente relacionadas.
  3. Redelegate! Ou seja, refatorar cada uma das ações identificadas para novas ou (mais importante) outras classes.

Um exemplo abstrato sobre responsabilidades de refatoração

Vamos Cser uma classe que tem várias dependências D1, D2, D3, D4que você precisa para refatorar para usar menos. Quando identificamos quais métodos Cchamam as dependências, podemos fazer uma lista simples:

  • D1- performA(D2),performB()
  • D2 - performD(D1)
  • D3 - performE()
  • D4 - performF(D3)

Olhando para a lista, podemos ver isso D1e D2estamos relacionados entre si, pois a classe precisa deles de alguma forma. Também podemos ver essas D4necessidades D3. Portanto, temos dois agrupamentos:

  • Group 1- D1<->D2
  • Group 2- D4->D3

Os agrupamentos são um indicador de que a classe agora tem duas responsabilidades.

  1. Group 1- Um para manipular a chamada de dois objetos que precisam um do outro. Talvez você possa deixar sua classe Celiminar a necessidade de lidar com ambas as dependências e deixar um deles cuidar dessas chamadas. Nesse agrupamento, é óbvio que D1poderia ter uma referência a D2.
  2. Group 2- A outra responsabilidade precisa de um objeto para chamar outro. Não consegue D4lidar D3com a classe? Provavelmente, podemos eliminar D3da classe Cdeixando D4fazer as chamadas.

Não tome minha resposta como definida, pois o exemplo é muito abstrato e faz muitas suposições. Tenho certeza de que existem mais maneiras de refatorar isso, mas pelo menos as etapas podem ajudá-lo a obter algum tipo de processo para mover responsabilidades, em vez de dividir as classes.


Editar:

Entre os comentários, @Emmad Karem diz:

"Se sua classe possui 20 parâmetros no construtor, não parece que sua equipe sabe o que é SRP. Se você tem uma classe que faz apenas uma coisa, como tem 20 dependências?" - Eu acho que se você tem uma classe Customer, não é estranho ter 20 parâmetros no construtor.

É verdade que os objetos DAO tendem a ter muitos parâmetros, que você precisa definir em seu construtor, e os parâmetros geralmente são tipos simples, como string. No entanto, no exemplo de uma Customerclasse, você ainda pode agrupar suas propriedades dentro de outras classes para simplificar as coisas. Como ter uma Addressturma com ruas e uma Zipcodeturma que contenha o CEP e também lide com a lógica de negócios, como a validação de dados:

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

Isso é discutido mais adiante na postagem do blog "Nunca, nunca, nunca use String em Java (ou pelo menos com frequência)" . Como alternativa ao uso de construtores ou métodos estáticos para facilitar a criação dos subobjetos, você pode usar um padrão de construtor de fluidos .

Spoike
fonte
+1: Ótima resposta! O agrupamento é um mecanismo muito poderoso para a IMO, porque você pode aplicar o agrupamento recursivamente. Em termos gerais, com n camadas de abstração, você pode organizar 2 ^ n itens.
Giorgio
+1: Seus primeiros parágrafos resumem exatamente o que minha equipe está enfrentando. "Objetos de negócios" que são realmente objetos de serviço e código de configuração de teste de unidade que é entorpecedor de escrever. Eu sabia que tínhamos um problema quando as chamadas da camada de serviço continham uma linha de código; uma chamada para um método da camada de negócios.
Man
3

Concordo com todas as respostas sobre SRP e como isso pode ser levado longe demais. Na sua postagem, você mencionou que, devido à "refatoração excessiva" para aderir ao SRP, você encontrou o encapsulamento quebrado ou sendo modificado. A única coisa que funcionou para mim é sempre seguir o básico e fazer exatamente o necessário para atingir um fim.

Ao trabalhar com sistemas Legacy, o "entusiasmo" de consertar tudo para torná-lo melhor geralmente é bastante alto nos líderes de equipe, especialmente aqueles que são novos nessa função. SOLID, apenas não possui SRP - são apenas os S. Certifique-se de que, se estiver seguindo o SOLID, não esqueça também o OLID.

Estou trabalhando em um sistema Legacy agora e começamos a seguir um caminho semelhante no começo. O que funcionou para nós foi uma decisão coletiva da equipe de fazer o melhor dos dois mundos - SOLID e KISS (Keep It Simple Stupid). Discutimos coletivamente as principais mudanças na estrutura do código e aplicamos o bom senso na aplicação de vários princípios de desenvolvimento. Eles são ótimos como diretrizes, não "Leis do Desenvolvimento S / W". A equipe não é apenas sobre o líder da equipe - é sobre todos os desenvolvedores da equipe. O que sempre funcionou para mim é colocar todos em uma sala e criar um conjunto compartilhado de diretrizes que sua equipe inteira concorda em seguir.

Com relação a como corrigir sua situação atual, se você usa um VCS e não adicionou muitos recursos novos ao seu aplicativo, sempre pode voltar para uma versão de código que toda a equipe considera compreensível, legível e sustentável. Sim! Estou pedindo que você jogue fora o trabalho e comece do zero. É melhor do que tentar "consertar" algo que estava quebrado e movê-lo de volta para algo que já existia.

Sharath Satish
fonte
3

A resposta é a manutenção e a clareza do código acima de tudo. Para mim, isso significa escrever menos código , não mais. Menos abstrações, menos interfaces, menos opções, menos parâmetros.

Sempre que avalio uma reestruturação de código ou adiciono um novo recurso, penso em quanto clichê será necessário em comparação com a lógica real. Se a resposta for superior a 50%, provavelmente significa que estou pensando demais.

Além do SRP, existem muitos outros estilos de desenvolvimento. No seu caso, parece que YAGNI está definitivamente ausente.

cmcginty
fonte
3

Muitas das respostas aqui são realmente boas, mas focam-se no lado técnico desta questão. Vou simplesmente acrescentar que parece que as tentativas do desenvolvedor de seguir o SRP parecem violar o SRP.

Você pode ver o blog de Bob aqui sobre essa situação, mas ele argumenta que, se uma responsabilidade é manchada em várias classes, o SRP da responsabilidade é violado porque essas classes mudam em paralelo. Suspeito que seu desenvolvedor realmente goste do design no topo do blog de Bob e talvez fique um pouco desapontado ao vê-lo destruído. Em particular porque viola o "Princípio Comum de Fechamento" - as coisas que mudam juntas permanecem juntas.

Lembre-se de que o SRP se refere a "motivo da mudança" e não a "faça uma coisa", e que você não precisa se preocupar com esse motivo até que uma mudança realmente ocorra. O segundo cara paga pela abstração.

Agora existe o segundo problema - o "advogado virulento do desenvolvimento do SOLID". Certamente não parece que você tenha um ótimo relacionamento com esse desenvolvedor, portanto, qualquer tentativa de convencê-lo dos problemas na base de código é frustrada. Você precisará reparar o relacionamento para ter uma discussão real dos problemas. O que eu recomendaria é cerveja.

Não é sério - se você não bebe a cabeça em uma cafeteria. Saia do escritório e relaxe em algum lugar, onde você pode conversar sobre isso informalmente. Em vez de tentar ganhar uma discussão em uma reunião, o que você não quer, faça uma discussão em algum lugar divertido. Tente reconhecer que esse desenvolvedor, que está deixando você louco, é um humano realmente funcional que está tentando colocar o software "fora da porta" e não quer enviar porcaria. Como você provavelmente compartilha esse terreno comum, pode começar a discutir como melhorar o design enquanto ainda está em conformidade com o SRP.

Se você pode reconhecer que o SRP é uma coisa boa, que apenas interpreta os aspectos de maneira diferente, provavelmente pode começar a ter conversas produtivas.

Eric Smith
fonte
-1

Eu concordo com a sua decisão de líder de equipe [atualização = 2012.05.31] de que o SRP é geralmente um bom pensamento. Mas eu concordo totalmente com o comentário do @ Spoike -s que um construtor com 20 argumentos de interface é longe demais.

A introdução do SRP com IoC move complexetyety de uma "classe multi-responsável" para muitas classes srp e uma inicialização muito mais complicada para o benefício de

  • testabilidade unitária / tdd mais fácil (testando uma classe srp isolada de cada vez)
  • mas ao custo de
    • uma inicialização e integração de código muito mais difícil e
    • depuração mais difícil
    • fragmentação (= distribuição de código em vários arquivos / diretórios)

Receio que você não possa reduzir a desfragmentação de código sem sacrificar o srp.

Mas você pode "aliviar a dor" da inicialização de código implementando uma classe de açúcar sintática que oculta a complexidade da inicialização em um construtor.

   class MySrpClass {
      MySrpClass(Interface1 parm1, Interface2 param2, .... Interface20 param2) {
      }
   } 

   class MySyntaxSugarClass : MySrpClass {
      MySyntaxSugarClass() {
         super(new MyInterface1Implementation(), new MyImpl2(), ....)
      }
   }
k3b
fonte
2
Acredito que 20 interfaces é um indicador de que a classe tem muito a fazer. Ou seja, existem 20 razões para mudar, o que é praticamente uma violação do SRP. Só porque o sistema é complexo, não significa que tenha que ser complicado.
Spoike