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

54

Estou trabalhando em uma equipe onde o líder da equipe é um virulento defensor dos princípios de desenvolvimento da SOLID. No entanto, ele não tem muita experiência na obtenção de software complexo para 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 altamente fragmentada e difícil de entender e depurar.

Agora temos um problema não apenas com a fragmentação de 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 uma "razão para mudar" e foram extraídos para classes públicas ou internas e interfaces que não estão de acordo com os objetivos de encapsulamento do aplicativo.

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

Eu quero saber se existe uma abordagem de 'refatoração fora do SRP' que poderíamos usar para ajudar a corrigir alguns desses problemas. Eu li que ele não viola o SOLID se eu criar um número de classes vazias e grosseiras que 'envolvam' um número de classes intimamente relacionadas para fornecer um único ponto de acesso à soma de sua funcionalidade (ou seja, imitando uma excessivamente implementação de classe 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?

    
por Dean Chalk 29.05.2012 / 12:19
fonte

7 respostas

80

Se sua classe tiver 20 parâmetros no construtor, não parece que sua equipe saiba exatamente o que é SRP. Se você tem uma classe que faz apenas uma coisa, como ela tem 20 dependências? Isso é como ir em uma viagem de pesca e trazendo uma vara de pesca, caixa de equipamento, suprimentos de quilting, bola de boliche, nunchucks, lança-chamas, etc .... Se você precisa de tudo isso para ir pescar, você não está indo apenas pescar.

Dito isto, o SRP, como a maioria dos princípios existentes, pode ser aplicado em excesso. Se você criar uma nova classe para incrementar inteiros, então sim, isso pode ser uma responsabilidade única, mas vamos lá. Isso é ridículo. Nós 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 é manutenibilidade . Se você pretende obter essa granularidade com o Princípio da Responsabilidade Única, é um indicador de que o zelo pelo SOLID cegou a equipe para o objetivo do SOLID.

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

EDITAR

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

EDIT 2

Se você está procurando dicas sobre refatoração, pode começar a fazer o que sugeriu - crie classes mais granuladas para envolver várias outras. Certifique-se de que a classe mais granulada ainda esteja aderindo ao SRP , mas em um nível mais alto. Então você tem duas alternativas:

  1. Se as classes mais refinadas não forem mais usadas em outras partes do sistema, você poderá gradualmente extrair sua implementação para a classe com granulação mais grossa e excluí-las.
  2. Deixe as classes mais refinadas sozinhas. Talvez eles foram bem concebidos e você só precisava do invólucro para torná-los mais fáceis de usar. Eu suspeito que este é o caso de grande parte do seu projeto.

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

    
por 30.05.2012 / 05:28
fonte
29

Acho que é na Refatoração de Martin Fowler que uma vez li uma regra contrária ao SRP, definindo para onde está indo longe demais. Há uma segunda questão, 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 questão for, em todos os casos, "sim", mas a segunda pergunta for "nem perto", então você precisa verificar novamente como está implementando o SRP.

Por exemplo, se adicionar um campo a uma tabela significa que você precisa alterar uma classe DTO e um validador, uma classe de persistência e um objeto de modelo de visualização, e assim por diante, 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 modo que adicionar um campo STILL requer uma alteração no objeto CustomerPersisitence, qual foi o objetivo? Você ainda tem um objeto com dois motivos para mudar - não é mais o cliente.

No entanto, se você introduzir um ORM, é bem possível que você faça as classes funcionarem de tal forma que, se você adicionar um campo ao DTO, ele mudará automaticamente o SQL usado para ler esses dados. Então você tem uma boa razão para separar as duas preocupações.

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

    
por 30.05.2012 / 05:58
fonte
21

Só porque um sistema é um complexo não significa que você tenha que complicar . Se você tem uma classe que tem muitas dependências (ou Colaboradores) assim:

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

... então ficou complicado demais e você não está realmente seguindo o SRP , está? Eu aposto que se você escreveu o que o MyAwesomeClass faz em um cartão CRC ele não caberia em um cartão de índice ou você tem que escrever em letras ilegíveis realmente minúsculas.

O que você tem aqui é que seu pessoal só seguiu o Princípio da Segregação de Interface e pode ter levado isso ao extremo mas essa é uma outra história. Você poderia argumentar que as dependências são objetos de domínio (o que acontece), no entanto, ter uma classe que lide com 20 objetos de domínio ao mesmo tempo é um pouco longe demais.

O TDD fornecerá um bom indicador do quanto uma turma faz. Sem rodeios; Se um método de teste tiver código de configuração que demora uma eternidade para ser gravado (mesmo se você refatorar os testes), então o MyAwesomeClass provavelmente tem muitas coisas a fazer.

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

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

Um exemplo abstrato de responsabilidades de refatoração

Deixa C ser uma classe que tem várias dependências D1 , D2 , D3 , D4 que você precisa refatorar para usar menos. Quando identificamos os métodos que C chama nas 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 que D1 e D2 estão relacionados uns com os outros, pois a classe precisa deles juntos de alguma forma. Também podemos ver que D4 precisa de D3 . Então nós temos dois grupos:

  • Group 1 - D1 < - > %código%
  • D2 - Group 2 - > %código%

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

  1. D4 - Um para lidar com a chamada de dois objetos que precisam um do outro. Talvez você possa permitir que sua classe D3 elimine a necessidade de lidar com as duas dependências e deixe uma delas manipulando essas chamadas. Neste agrupamento, é óbvio que Group 1 poderia ter uma referência a C .
  2. D1 - A outra responsabilidade precisa de um objeto para chamar outro. Não é possível D2 manipular Group 2 em vez de sua turma? Então, provavelmente, podemos eliminar D4 da classe D3 deixando D3 fazer as chamadas.

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

Editar:

Entre os comentários @Emmad Karem diz:

"If your class has 20 parameters in the constructor, it doesn't sound like your team quite knows what SRP is. If you have a class that does only one thing, how does it have 20 dependencies? " - I think that If you have a Customer class, it is not strange to have 20 parameters in the constructor.

É verdade que os objetos DAO tendem a ter muitos parâmetros, que você deve definir em seu construtor, e os parâmetros geralmente são tipos simples, como string. No entanto, no exemplo de uma classe C , você ainda pode agrupar suas propriedades dentro de outras classes para simplificar as coisas. Como ter uma classe D4 com ruas e uma classe Customer que contenha o CEP e lide com a lógica de negócios, como 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
    }
}

Essa coisa é discutida mais adiante no post do blog "Nunca, nunca, nunca use String em Java (ou pelo menos com freqüência)" . Como alternativa ao uso de construtores ou métodos estáticos para facilitar a criação de sub objetos, você pode usar um padrão de gerador de fluidos .

    
por 30.05.2012 / 13:43
fonte
3

Concordo com todas as respostas sobre o SRP e como ele pode ser levado longe demais. Em seu post você menciona que, devido à "refatoração excessiva" para aderir ao SRP, você descobriu que o encapsulamento quebra ou é modificado. A única coisa que funcionou para mim é sempre manter o básico e fazer exatamente o que é necessário para alcançar um fim.

Quando se trabalha com sistemas Legacy, o "entusiasmo" de consertar tudo para torná-lo melhor é geralmente muito alto em Team Leads, especialmente aqueles que são novos nessa função. Sólido, só não tem SRP - Isso é apenas o S. Certifique-se de que, se você está seguindo o SOLID, você não se esqueça do OLID também.

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 para aproveitar ao máximo os dois mundos - SOLID e K.I.S.S (Keep It Simple Stupid). Discutimos coletivamente grandes mudanças na estrutura do código e aplicamos o senso comum ao aplicar vários princípios de desenvolvimento. Eles são ótimos como diretrizes não "Leis do Desenvolvimento de 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 é reunir todos em uma sala e elaborar um conjunto de diretrizes compartilhadas que toda a sua equipe concorda em seguir.

Com relação a como corrigir sua situação atual, se você usar um VCS e não tiver adicionado muitos novos recursos ao seu aplicativo, poderá sempre voltar para uma versão do código que toda a equipe considera compreensível, legível e sustentável. Sim! Eu estou pedindo para você jogar fora o trabalho e começar do zero. Isso é melhor do que tentar "consertar" algo que foi quebrado e movê-lo de volta para algo que já existia.

    
por 01.06.2012 / 10:38
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 eu 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.

No topo do SRP, existem muitos outros estilos de desenvolvimento. No seu caso, parece que seu som como YAGNI está definitivamente faltando.

    
por 08.06.2012 / 22:21
fonte
3

Muitas das respostas aqui são realmente boas, mas estão focadas no lado técnico desta questão. Vou simplesmente acrescentar que soa como tentativas do desenvolvedor de seguir o som do SRP como se eles realmente violassem o SRP.

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

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

Agora, há o segundo problema - o "virulento defensor do desenvolvimento do SOLID". Com certeza não parece que você tem um ótimo relacionamento com esse desenvolvedor, então qualquer tentativa de convencê-lo dos problemas na base de código está perdida. Você precisará reparar o relacionamento para poder ter uma discussão real dos problemas. O que eu recomendaria é cerveja.

Não seriamente - se você não beber de cabeça para um café. Saia do escritório e relaxe em algum lugar, onde você pode conversar informalmente sobre essas coisas. Ao invés de tentar ganhar uma discussão em uma reunião, você não terá uma discussão em algum lugar divertido. Tente reconhecer que esse desenvolvedor, que está enlouquecendo, é um humano em bom estado de funcionamento que está tentando fazer com que o software "saia da porta" e não queira enviar porcaria. Como você provavelmente compartilha esse ponto comum, pode começar a discutir como melhorar o design enquanto ainda estiver em conformidade com o SRP.

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

    
por 19.01.2016 / 14:27
fonte
-1

Concordo com sua decisão de liderar a equipe [update = 2012.05.31] de que o SRP geralmente é uma boa ideia. Mas eu concordo totalmente com @ Spoike -s comentando que um construtor com 20 argumentos de interface é muito longe. [/ Update]:

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

  • mais fácil testability de unidade / tdd (testando uma classe srp em isolamento 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
    • fragmentation (= distribuição de código em vários arquivos / diretórios)

Eu temo que você não possa reduzir a codefragmentação sem sacrificar srp.

Mas você pode "aliviar a dor" da iniciação de código implementando uma classe de açúcar sintático 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(), ....)
      }
   }
    
por 29.05.2012 / 14:08
fonte