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

63

Hoje eu 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 argumento com uma classe simples assim:

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: "Eu acredito no SOLID . Você está violando o princípio da responsabilidade única (SRP), pois representa os dados e executa a lógica na mesma classe".

Achei essa afirmação realmente surpreendente, pois, seguindo esse raciocínio, qualquer classe com uma propriedade e um método viola o SRP e, portanto, OOP em geral não é SÓLIDO, e a programação funcional é o único caminho para o céu.

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

Se eu tivesse respondido, começaria apontando o paradoxo mencionado acima e depois indicaria que o SRP é altamente dependente do nível de granularidade que você deseja considerar e que, se você for suficientemente longe, qualquer classe que contenha mais de um propriedade ou um método a viola.

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.

tobiak777
fonte
19
esta classe viola SRP não porque mistura de dados com lógica, mas porque tem baixa coesão - potencial de Deus objeto
mosquito
11
Qual o objetivo de adicionar feriados ao funcionário? Talvez deva haver uma aula de calendário ou algo que tenha férias. Eu acho que seu amigo está certo.
James Black
9
Nunca ouça alguém que diga "eu acredito em X".
Stig Hemmer
29
Não é tanto se isso é uma violação do SRP, mas se é uma boa modelagem. Suponha que eu seja um empregado. Quando pergunto ao meu gerente se está tudo bem com ele se eu demorar um longo fim de semana para esquiar, meu gerente não adiciona férias a mim . O código aqui não corresponde à realidade que pretende modelar e, portanto, é suspeito.
Eric Lippert
2
+1 para programação funcional sendo o único caminho para o céu.
erip

Respostas:

68

A responsabilidade única deve ser entendida como uma abstração de tarefas lógicas no seu sistema. Uma classe deve ter a única responsabilidade de (fazer todo o necessário para) executar uma única tarefa específica. Isso pode realmente trazer muito para uma classe bem projetada, dependendo da 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 essa classe tem?" mas "que operações úteis essa classe executa dentro do programa?" Uma vez que isso seja entendido, seu modelo de domínio ficará bem.

Mason Wheeler
fonte
Se a Programação Orientada a Objetos foi criada e pretende modelar os objetos e as classes da vida real (ações + atributos), por que não escrever a classe de código com várias responsabilidades (ações)? Um objeto do mundo real pode ter múltiplas responsabilidades. Por exemplo, um jornalista escreve editoriais em jornais e entrevista políticos em um programa de TV. Duas responsabilidades por um objeto da vida real! E se eu for escrever uma classe Jornalista?
user1451111
41

O Princípio de Responsabilidade Única se preocupa apenas se um pedaço de código (no OOP, normalmente estamos falando de classes) tem responsabilidade sobre um pedaço de funcionalidade . Acho que seu amigo dizendo que funções e dados não podem se misturar realmente não entendeu essa ideia. Se Employeetambém contivéssemos informações sobre o local de trabalho, a velocidade do carro e que tipo de comida o cachorro come, teríamos um problema.

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

Um lugar em que podemos melhorar é separar as informações do funcionário (como nome, número de telefone, email) das férias dele. Na minha opinião, isso não significa que métodos e dados não possam ser combinados , apenas significa que talvez a funcionalidade de férias possa estar em um local separado.

Frank Bryce
fonte
20

Para minha mente esta classe poderia violar SRP se continuasse a representar tanto um Employeee EmployeeHolidays.

Do jeito que está, e se fosse para a Revisão por pares, eu provavelmente deixaria passar. Se mais propriedades e métodos específicos de Funcionários forem adicionados, e mais propriedades específicas de feriados forem adicionadas, eu provavelmente recomendaria uma divisão, citando o SRP e o ISP.

NikolaiDante
fonte
Concordo. Se o código for tão simples quanto o fornecido aqui, eu provavelmente o deixaria deslizar. Mas, em minha opinião, não devo ser responsabilidade do Funcionário em lidar com suas próprias férias. Pode não parecer muito importante para onde a responsabilidade está colocada, mas veja da seguinte maneira: se você era novo na base de código e precisava trabalhar na funcionalidade específica do feriado - onde você procuraria primeiro? Para a lógica do feriado, eu pessoalmente não examinaria a entidade Employee para começar.
Niklas H
11
@NiklasH Concordou. Tho pessoalmente eu não olharia de forma aleatória e tentar adivinhar a classe I iria procurar com o estúdio para a palavra "férias" e veja o que as classes que surgiu em :).
NikolaiDante
4
Verdadeiro. Mas e se não for chamado "Feriado" neste novo sistema, mas for "Férias" ou "Tempo Livre". Mas eu concordo, com o fato de que você normalmente tem a capacidade de procurá-lo ou pode pedir a um colega de trabalho. Meu comentário foi principalmente para fazer com que o OP modelasse mentalmente a responsabilidade e onde seria o lugar mais óbvio para a lógica :-) #
1113 Niklas H
Eu concordo com sua primeira declaração. No entanto, se se tratasse de revisão por pares, acho que não, porque violar o SRP é uma ladeira escorregadia e essa pode ser a primeira de muitas janelas quebradas. Felicidades.
Jim Speaker
20

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

As duas primeiras letras no SOLID, SRP e OCP são sobre como o seu código muda 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 os motivos prováveis ​​para a alteração do seu código é muito mais produtivo do que discutir se o seu código é SOLID ou não.

Quantas razões a sua classe Employee precisa mudar? Não sei, porque não conheço o contexto em que você o está usando e também não consigo ver o futuro. O que posso fazer é debater possíveis alterações com base no que vi no passado, e você pode avaliar subjetivamente a probabilidade delas. Se mais de uma pontuação entre "razoavelmente provável" e "meu código já tiver sido alterado exatamente por esse motivo", você estará violando o SRP contra esse tipo de alteração. Aqui está um: alguém com mais de dois nomes entra na sua empresa (ou um arquiteto lê este excelente artigo do W3C ). Aqui está outra: sua empresa muda a maneira como aloca dias de férias.

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 motivos para mudar.

Aqui está algo para se pensar: sua classe Employee mudaria se o seu sistema precisasse acompanhar os salários dos funcionários? Endereços? Informações de contato de emergência? Se você disser "sim" (e "provavelmente acontecerá") a dois deles, sua classe estará violando o SRP, mesmo que ainda não tenha um código! O SOLID é sobre processos e arquitetura, tanto quanto sobre código.

Carl Leth
fonte
9

O fato de a classe representar dados não é de responsabilidade da classe, é um detalhe de implementação particular.

A classe tem uma responsabilidade, representar um funcionário. Nesse contexto, isso significa que ela apresenta alguma API pública que fornece a funcionalidade necessária para você lidar com funcionários (se 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 classe agora tenha múltiplas responsabilidades.

RemcoGerlich
fonte
Interessante linha de pensamento, muito obrigado por compartilhar
tobiak777
Nice - Boa maneira de expressar os objetivos pretendidos do OOP.
user949300
5

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

É porque a identidade do funcionário está misturada com o gerenciamento de férias, o que é ruim. A identidade do funcionário é algo complexo que é necessário para rastrear férias, salário, horas extras, para representar quem está em qual equipe, para vincular a relatórios de desempenho etc. Um funcionário também pode alterar o nome, o sobrenome ou ambos, e permanecer o mesmo empregado. Os funcionários podem até ter várias grafias de nome, como ASCII e unicode. As pessoas podem ter 0 a n primeiro e / ou sobrenome. Eles podem ter nomes diferentes em jurisdições diferentes. O rastreamento da identidade de um funcionário é uma responsabilidade suficiente para que o gerenciamento de férias ou de férias não possa ser acrescentado sem chamá-lo de uma segunda responsabilidade.

Peter
fonte
"Rastrear a identidade de um funcionário é uma responsabilidade suficiente para que o gerenciamento de férias ou de férias não possa ser acrescentado sem chamá-lo de uma segunda responsabilidade". + O funcionário pode ter vários nomes, etc ... O objetivo de um modelo é focar nos fatos relevantes do mundo real para o problema em questão. Existem requisitos para os quais este modelo é ideal. Nesses requisitos, os funcionários são interessantes apenas na medida em que podemos modificar suas férias e não estamos muito interessados ​​em gerenciar outros aspectos de suas especificidades da vida real.
precisa saber é o seguinte
@reddy "Os funcionários são interessantes apenas na medida em que podemos modificar as férias" - O que significa que você precisa identificá-los corretamente. Assim que você tiver um funcionário, ele poderá alterar o sobrenome a qualquer momento devido a casamento ou divórcio. Eles também podem mudar seu nome e gênero. Você demitirá funcionários se o sobrenome mudar, de forma que o nome corresponda ao de outro funcionário? Você não adicionará toda essa funcionalidade no momento. Em vez disso, você o adicionará quando precisar, o que é bom. Independentemente de quanto é implementado, a responsabilidade de identificação permanece a mesma.
Peter
3

"Acredito no SOLID. Você está violando o princípio da responsabilidade única (SRP), pois representa dados e executa lógica na mesma classe."

Como os outros, eu discordo disso.

Eu diria que o SRP é violado se você estiver executando mais de uma peça de lógica na classe. A quantidade de dados que precisa ser armazenada na classe para alcançar essa única peça de lógica é irrelevante.

Corridas de leveza com Monica
fonte
Não! O SRP não é violado por várias partes da lógica, várias partes de dados ou qualquer combinação das duas. O único requisito é que o objeto cumpra sua finalidade. Seu objetivo pode envolver muitas operações.
Martin Maat
@ MartinMaat: Muitas operações, sim. Uma parte da lógica como resultado. Acho que estamos dizendo a mesma coisa, mas com termos diferentes (e eu estou feliz em assumir que os seus são os corretos como eu não estudar essas coisas muitas vezes)
Leveza raças com Monica
2

Atualmente, não acho útil debater sobre o que constitui e o que não constitui uma única responsabilidade ou uma única razão para mudar. Eu proporia um Princípio Mínimo de Luto em seu lugar:

Princípio Mínimo de Luto: o código deve procurar minimizar sua probabilidade de exigir alterações ou maximizar a facilidade de alteração.

Como é isso? Não é necessário que um cientista de foguetes descubra por que isso pode ajudar a reduzir os custos de manutenção e, esperançosamente, não deve ser um ponto de debate sem fim, mas, como no SOLID em geral, não é algo para aplicar às cegas em todos os lugares. É algo a considerar ao equilibrar trade-offs.

Quanto à probabilidade de exigir alterações, isso diminui com:

  1. Bom teste (confiabilidade aprimorada).
  2. Envolver apenas o código mínimo necessário para fazer algo específico (isso pode incluir a redução de acoplamentos aferentes).
  3. Apenas torne o código durão no que faz (consulte Tornar o princípio durão).

Quanto à dificuldade de fazer alterações, ela aumenta com os acoplamentos eferentes. O teste introduz acoplamentos eferentes, mas melhora a confiabilidade. Feito bem, geralmente faz mais bem do que mal e é totalmente aceitável e promovido pelo Princípio Mínimo de Luto.

Faça o princípio de Badass: as classes usadas em muitos lugares devem ser incríveis. Eles devem ser confiáveis, eficientes se isso estiver relacionado à sua qualidade, etc.

E o princípio Make Badass está vinculado ao princípio de luto mínimo, uma vez que as coisas mais duras encontrarão uma menor probabilidade de exigir mudanças do que as que são péssimas no que fazem.

Eu começaria apontando o paradoxo mencionado acima e depois indicaria que o SRP é altamente dependente do nível de granularidade que você deseja considerar e que, se você for suficientemente longe, qualquer classe que contenha mais de uma propriedade ou um método viola isto.

Do ponto de vista do SRP, uma classe que quase nada faz 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. É o ZRP!

Exceto que eu sugeriria uma violação flagrante do princípio Make Badass. Também é absolutamente inútil. Algo que faz tão pouco não pode esperar ser durão. Tem muito pouca informação (TLI). E, naturalmente, quando você tem algo que é TLI, ele não pode fazer nada realmente significativo, nem mesmo com as informações que encapsula; portanto, deve vazá-las para o mundo exterior, na esperança de que outra pessoa faça algo significativo e durão. E esse vazamento é bom para algo que apenas agrega dados e nada mais, mas esse limite é a diferença que eu vejo entre "dados" e "objetos".

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

Portanto, há um ato de equilíbrio quanto à granularidade ou grosseria dos objetos que você cria. Costumo julgá-lo pela quantidade de informações que você deve vazar para o mundo exterior e pela funcionalidade significativa que ela pode executar. Costumo achar o princípio Make Badass útil para encontrar o equilíbrio ao combiná-lo com o princípio do luto mínimo.


fonte
1

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

Muitos sistemas tendem a fazer isso quando várias camadas precisam jogar 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 local e o comportamento em outras classes de serviço. Se a mesma classe foi usada em várias camadas, essa pode aumentar e faz a pergunta de qual camada é responsável por definir o comportamento de que precisa 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 de todos os seus funcionários possa chamar uma computação por dias pagos. E a GUI da lista de funcionários certamente não precisa de todo o novo cálculo de ID agregado usado nesse agente estatístico (e os dados técnicos que o acompanham). Quando você separa os métodos dessa maneira, geralmente termina com uma classe com apenas estruturas de dados.

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

Portanto, sim, ele separa completamente os dados nas classes de domínio e o tratamento de dados nas classes de serviço, mas é necessário aqui. Esse sistema é ao mesmo tempo funcional para gerar 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, estará mais livre para colocar os métodos nas classes de serviço ou nas classes de domínio, dependendo de como vê seu objeto. Tendo a ver um objeto como o conceito "real", isso naturalmente ajuda a manter o SRP. Assim, no seu exemplo, é mais realista do que o salário do chefe do funcionário acrescente o dia de pagamento concedido à sua PayDayAccount. Um funcionário é contratado pela empresa, Works, pode ficar doente ou pedir um conselho, e ele tem uma conta no Payday (o chefe pode recuperá-la diretamente dele ou de um registro do PayDayAccount ...) Mas você pode criar um atalho agregado aqui por simplicidade, se você não quiser muita complexidade para um software simples.

Vince
fonte
Obrigado pela sua contribuição Vince. O ponto é que você não precisa de uma camada de serviço quando possui um domínio rico. Há apenas uma classe responsável pelo comportamento e é a sua entidade de domínio. As outras camadas (camada da web, interface do usuário etc ...) normalmente lidam com DTOs e ViewModels, e isso é bom. O modelo de domínio é modelar o domínio, não executar trabalhos de interface do usuário ou enviar mensagens pela Internet. Sua mensagem reflete esse equívoco comum, que deriva do fato de as pessoas simplesmente não saberem como ajustar o POO em seu design. E eu acho isso muito triste - para eles.
precisa saber é o seguinte
Eu não acho que tenho um conceito errado de OOP de domínio rico, eu projetei muitos sotwares dessa maneira e é verdade que é realmente bom para manutenção / evolução. Mas lamento dizer que isso não é uma bala de prata.
Vince
Eu não estou dizendo que é. Para escrever um compilador, provavelmente não é, mas para a maioria dos aplicativos de linha de negócios / SaaS, acho que é muito menos arte / ciência do que você sugere. A necessidade de modelo de domínio pode ser provado matematicamente, seus exemplos me fazem pensar em projetos discutíveis em vez de falhas de limitações em OOP
tobiak777
0

Você está violando o princípio de responsabilidade única (SRP), pois representa os dados e executa a lógica na mesma classe.

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

Dmitry Nogin
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 que soa cientificamente e pelas letras maiúsculas.

ZunTzu
fonte
11
Essa dificilmente é uma resposta real, mas teria sido ótimo como um comentário para a pergunta.
Jay Elston