Esse design de classe viola o princípio da responsabilidade única?

63

Hoje tive uma discussão com alguém.

Eu estava explicando os benefícios de ter um modelo de domínio rico em oposição a um modelo de domínio anêmico. E eu demonstrei meu ponto com uma classe simples parecida com isso:

public class Employee
{
    public Employee(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastname;
    }

    public string FirstName { get private set; }
    public string LastName { get; private set;}
    public int CountPaidDaysOffGranted { get; private set;}

    public void AddPaidDaysOffGranted(int numberOfdays)
    {
        // Do stuff
    }
}

Como ele defendeu sua abordagem de modelo anêmico, um de seus argumentos foi: "Acredito em SOLID . Você está violando o princípio de responsabilidade única (SRP), pois ambos representam dados e executam lógica no mesma classe. "

Eu achei essa afirmação realmente surpreendente, pois seguindo este raciocínio, qualquer classe tendo uma propriedade e um método viola o SRP e, portanto, a OOP em geral não é SOLID e programação funcional é o único caminho para o céu.

Eu decidi não responder aos seus muitos argumentos, mas estou curioso sobre o que a comunidade pensa sobre essa questão.

Se eu tivesse respondido, eu teria começado apontando para o paradoxo mencionado acima e, em seguida, indicaria que o SRP é altamente dependente do nível de granularidade que você deseja considerar e que, se você o levar longe o suficiente, qualquer classe contendo mais de uma propriedade ou um método viola-lo.

O que você teria dito?

Atualização: O exemplo foi generosamente atualizado por guntbert para tornar o método mais realista e nos ajudar a focar na discussão subjacente.

    
por tobiak777 07.01.2016 / 22:35
fonte

11 respostas

68

A responsabilidade única deve ser entendida como uma abstração de tarefas lógicas em seu sistema. Uma turma deve ter a responsabilidade única de (fazer tudo o que for necessário para realizar) uma única tarefa específica. Isso pode realmente trazer muito em uma classe bem projetada, dependendo de qual é a responsabilidade. A classe que executa seu mecanismo de script, por exemplo, pode ter muitos métodos e dados envolvidos no processamento de scripts.

Seu colega de trabalho está se concentrando na coisa errada. A questão não é "que membros tem essa classe?" mas "que operação útil esta classe realiza dentro do programa?" Assim que isso for compreendido, seu modelo de domínio ficará bem.

    
por 07.01.2016 / 22:41
fonte
41

O Single Responsibility Principle se preocupa apenas com o fato de uma parte do código (em OOP, normalmente estamos falando de classes) ter responsabilidade sobre uma parte da funcionalidade . Acho que seu amigo dizendo que funções e dados não podem se misturar realmente não entendeu essa ideia. Se Employee também contivesse informações sobre seu local de trabalho, a velocidade de seu carro e o tipo de comida que seu cachorro come, teríamos um problema.

Como essa classe lida apenas com um Employee , acho justo dizer que ela não viola o SRP descaradamente, mas as pessoas sempre terão suas próprias opiniões.

Um lugar onde podemos melhorar é separar as informações dos funcionários (como nome, número de telefone, e-mail) de suas férias. Na minha opinião, isso não significa que métodos e dados não possam ser combinados , isso significa apenas que a funcionalidade de férias pode estar em um lugar separado.

    
por 07.01.2016 / 22:38
fonte
20

Em minha opinião, esta classe poderia potencialmente violar o SRP se continuasse a representar um Employee e EmployeeHolidays .

Como é, e se me ocorreu pela Peer Review, eu provavelmente deixaria passar. Se mais propriedades e métodos específicos de funcionários forem adicionados e mais propriedades específicas de feriado forem adicionadas, eu provavelmente recomendaria uma divisão, citando tanto o SRP quanto o ISP.

    
por 07.01.2016 / 23:06
fonte
20

Já existem ótimas respostas apontando que o SRP é um conceito abstrato sobre funcionalidade lógica, mas há pontos mais sutis que eu acho que valem a pena adicionar.

As duas primeiras letras em SOLID, SRP e OCP são sobre como o código é alterado em resposta a uma alteração nos requisitos. Minha definição favorita do SRP é: "um módulo / classe / função deve ter apenas um motivo para mudar." Argumentar sobre as razões prováveis para o seu código mudar é muito mais produtivo do que discutir se o seu código é SOLID ou não.

Quantas razões sua turma de funcionários precisa mudar? Eu não sei, porque não conheço o contexto em que você está usando, e também não consigo ver o futuro. O que eu posso fazer é debater possíveis mudanças com base no que eu vi no passado, e você pode avaliar subjetivamente o quão provável elas são. Se houver mais de uma pontuação entre "razoavelmente provável" e "meu código já foi alterado por esse motivo exato", você estará violando o SRP contra esse tipo de alteração. Aqui está uma: alguém com mais de dois nomes se junta à sua empresa (ou um arquiteto lê este excelente artigo do W3C ). Aqui está outra: sua empresa muda a forma como aloca dias de feriado.

Observe que esses motivos são igualmente válidos, mesmo se você remover o método AddHolidays. Muitos modelos de domínio anêmico violam o SRP. Muitos deles são apenas tabelas de banco de dados em código, e é muito comum que as tabelas de banco de dados tenham mais de 20 razões para mudar.

Aqui está algo para mastigar: a sua turma de Funcionários mudaria se o seu sistema precisasse rastrear os salários dos funcionários? Endereços Informações de contato de emergência? Se você disse "sim" (e "provável que aconteça") para dois deles, então sua classe estaria violando o SRP, mesmo que ainda não tivesse nenhum código! O SOLID é sobre processos e arquitetura, tanto quanto sobre código.

    
por 08.01.2016 / 04:04
fonte
9

Que a classe representa dados não é da responsabilidade da classe, é um detalhe de implementação privada.

A turma tem uma responsabilidade para representar um funcionário. Neste contexto, isso significa que apresenta uma API pública que fornece a funcionalidade que você precisa para lidar com os funcionários (se o AddHolidays é um bom exemplo é discutível).

A implementação é interna; Acontece que isso precisa de algumas variáveis privadas e alguma lógica. Isso não significa que a turma agora tenha múltiplas responsabilidades.

    
por 08.01.2016 / 09:47
fonte
5

A ideia de que misturar lógica e dados de qualquer maneira está sempre errada, é tão ridícula que nem sequer merece discussão. No entanto, há uma clara violação da responsabilidade única no exemplo, mas não é porque há uma propriedade DaysOfHolidays e uma função AddHolidays(int) .

É porque a identidade do funcionário está misturada com a gestão de férias, o que é ruim. A identidade do funcionário é uma tarefa complexa necessária para acompanhar férias, salário, horas extras, representar quem está em qual equipe, vincular-se a relatórios de desempenho, etc. Um funcionário também pode alterar seu nome, sobrenome ou ambos e permanecer o mesmo empregado. Os funcionários podem até ter várias grafias de seus nomes, como um ASCII e uma ortografia unicode. As pessoas podem ter 0 a n primeiros e / ou sobrenomes. Eles podem ter nomes diferentes em jurisdições diferentes. O acompanhamento da identidade de um funcionário é responsabilidade suficiente para que o gerenciamento de férias ou de férias não possa ser adicionado ao topo sem que seja considerada uma segunda responsabilidade.

    
por 09.01.2016 / 15:14
fonte
3

"I am a believer in SOLID. You are violating the single responsibility principle (SRP) as you are both representing data and performing logic in the same class."

Como os outros, eu não concordo com isso.

Eu diria que o SRP é violado se você estiver executando mais de uma parte da lógica na classe. Quantos dados precisam ser armazenados dentro da classe para conseguir essa única parte da lógica é irrelevante.

    
por 08.01.2016 / 02:41
fonte
2

Eu não acho útil hoje em dia debater sobre o que constitui ou não uma única responsabilidade ou uma única razão para mudar. Eu proporia um princípio do luto mínimo em seu lugar:

Minimum Grief Principle: code should either seek to minimize its probability of requiring changes or maximize the ease of being changed.

Como é isso? Não deve levar um cientista de foguetes para descobrir por que isso pode ajudar a reduzir os custos de manutenção e, esperamos, não deve ser um ponto de debate interminável, mas, como acontece com o SOLID em geral, não é algo que se aplique cegamente em todos os lugares. É algo a considerar ao equilibrar trade-offs.

Quanto à probabilidade de exigir mudanças, isso fica abaixo de:

  1. Bom teste (maior confiabilidade).
  2. Envolvendo apenas o código mínimo necessário para fazer algo específico (isso pode incluir a redução de acoplamentos aferentes).
  3. Apenas tornando o código foda no que ele faz (veja Princípio Make Badass).

Quanto à dificuldade de fazer mudanças, aumenta com acoplamentos eferentes. O teste introduz acoplamentos eferentes, mas melhora a confiabilidade. Bem feito, geralmente faz mais bem do que mal e é totalmente aceitável e promovido pelo Princípio Mínimo do Sofrimento.

Make Badass Principle: classes that are used in many places should be awesome. They should be reliable, efficient if that ties to their quality, etc.

E o Princípio do Make Badass está ligado ao Princípio do Sofrimento Mínimo, uma vez que as coisas ruins encontrarão uma probabilidade menor de exigir mudanças do que as que sugam o que fazem.

I would have started by pointing to the paradox mentioned above, and then indicate that the SRP is highly dependent on the level of granularity you want to consider and that if you take it far enough, any class containing more than one property or one method violates it.

Do ponto de vista do SRP, uma classe que mal faz qualquer coisa certamente teria apenas um (às vezes zero) motivos para mudar:

class Float
{
public:
    explicit Float(float val);
    float get() const;
    void set(float new_val);
};

Isso praticamente não tem motivos para mudar! É melhor que o SRP. É ZRP!

A não ser que eu sugira que esteja em flagrante violação do Princípio Make Badass. Também é absolutamente inútil. Algo que faz tão pouco não pode esperar ser durão. Tem pouca informação (TLI). E naturalmente quando você tem algo que é TLI, ele não pode fazer nada realmente significativo, nem mesmo com a informação que ele encapsula, então ele tem que vazar para o mundo externo na esperança de que alguém realmente faça algo significativo e foda. E esse vazamento é bom para algo que é apenas para agregar dados e nada mais, mas esse limite é a diferença como eu vejo entre "dados" e "objetos".

É claro que algo que é TMI também é ruim. Podemos colocar todo o nosso software em uma classe. Pode até ter apenas um método run . E alguém pode até argumentar que agora tem uma razão muito ampla para mudar: "Essa classe só precisará ser alterada se o software precisar de melhorias". Eu estou sendo bobo, mas é claro que podemos imaginar todos os problemas de manutenção com isso.

Portanto, há um equilíbrio entre a granularidade ou a grossura dos objetos que você projeta. Eu sempre julgo isso pela quantidade de informação que você tem que vazar para o mundo externo e quanta funcionalidade significativa ele pode executar. Muitas vezes eu acho o Princípio do Make Badass útil para encontrar o equilíbrio, combinando-o com o Princípio do Mago Mínimo.

    
por 08.01.2016 / 00:50
fonte
1

Pelo contrário, para mim, o modelo de domínio anêmico quebra alguns dos principais conceitos da OOP (que associam atributos e comportamento), mas pode ser inevitável com base em escolhas arquitetônicas. Domínios anêmicos são mais fáceis de pensar, menos orgânicos e mais sequenciais.

Muitos sistemas tendem a fazer isso quando várias camadas devem ser reproduzidas com os mesmos dados (camada de serviço, camada da Web, camada do cliente, agentes ...).

É mais fácil definir a estrutura de dados em um lugar e o comportamento em outras classes de serviço. Se a mesma classe foi usada em múltiplas camadas, esta pode crescer, e pergunta qual camada é responsável por definir o comportamento de que necessita, e quem é capaz de chamar os métodos.

Por exemplo, pode não ser uma boa ideia que um processo do agente que calcule estatísticas sobre todos os seus funcionários possa chamar uma computação para dias pagos. E a GUI da lista de funcionários certamente não precisa de todo o novo cálculo de ID agregado usado neste agente de estatística (e os dados técnicos que o acompanham). Quando você separa os métodos dessa maneira, geralmente termina com uma classe apenas com estruturas de dados.

Você pode facilmente serializar / desserializar os dados de "objeto", ou apenas alguns deles, ou para outro formato (json) ... sem se preocupar com qualquer conceito / responsabilidade de objeto. São apenas dados passando embora. Você sempre pode fazer o mapeamento de dados entre duas classes (Employee, EmployeeVO, EmployeeStatistic…), mas o que o Employee realmente significa aqui?

Então, sim, ele separa completamente dados em classes de domínio e manipulação de dados em classes de serviço, mas é necessário aqui. Esse sistema é ao mesmo tempo funcional para agregar valor comercial e também técnico para propagar os dados em todos os lugares necessários, mantendo um escopo de responsabilidade adequado (e o objeto distribuído também não resolve isso).

Se você não precisar separar os escopos de comportamento, será mais fácil colocar os métodos em classes de serviço ou em classes de domínio, dependendo de como você vê seu objeto. Eu costumo ver um objeto como o conceito "real", isso naturalmente ajuda a manter o SRP. Então, em seu exemplo, é mais realista do que o Employee's Boss adicionar payday off concedido à sua PayDayAccount. Um funcionário é contratado pela empresa, Works, pode ser doente ou ser solicitado por um conselho, e ele tem uma conta do dia de pagamento (o chefe pode recuperá-lo diretamente dele ou de um registro PayDayAccount ...) Mas você pode fazer um atalho agregado aqui para simplificar, se você não quer muita complexidade para um software simples.

    
por 13.01.2016 / 23:35
fonte
0

You are violating the single responsibility principle (SRP) as you are both representing data and performing logic in the same class.

Parece muito razoável para mim. O modelo pode não ter propriedades públicas se expõe ações. É basicamente uma ideia de Separação de Consulta de Comando. Por favor, note que o Comando terá estado privado com certeza.

    
por 25.01.2016 / 13:55
fonte
0

Você não pode violar o Princípio da Responsabilidade Única porque é apenas um critério estético, não uma regra da natureza. Não se deixe enganar pelo nome científico e pelas letras maiúsculas.

    
por 25.01.2016 / 18:13
fonte