Como uma classe pode ter vários métodos sem violar o princípio da responsabilidade única


64

O princípio da responsabilidade única é definido na wikipedia como

O princípio de responsabilidade única é um princípio de programação de computadores que afirma que todo módulo, classe ou função deve ter responsabilidade sobre uma única parte da funcionalidade fornecida pelo software e que a responsabilidade deve ser totalmente encapsulada pela classe

Se uma classe deve ter apenas uma responsabilidade, como pode ter mais de um método? Cada método não teria uma responsabilidade diferente, o que significaria que a classe teria mais de uma responsabilidade.

Todos os exemplos que vi demonstrando o princípio de responsabilidade única usam uma classe de exemplo que possui apenas um método. Pode ser útil ver um exemplo ou ter uma explicação de uma classe com vários métodos que ainda podem ser considerados como tendo uma responsabilidade.


11
Por que um voto negativo? Parece uma pergunta ideal para o SE.SE; a pessoa pesquisou o tópico e fez um esforço para tornar a pergunta muito clara. Ele merece votos positivos.
Arseni Mourzenko

19
O voto negativo provavelmente se deve a essa pergunta já ter sido feita e respondida várias vezes, por exemplo, consulte softwareengineering.stackexchange.com/questions/345018/… . Na minha opinião, não adiciona novos aspectos substanciais.
Hans-Martin Mosner


9
Isso é simplesmente reductio ad absurdum. Se toda classe tivesse literalmente apenas um método, então literalmente não há como nenhum programa conseguir fazer mais do que uma coisa.
Darrel Hoffman

6
@DarrelHoffman Isso não é verdade. Se toda classe fosse um functor com apenas um método "call ()", você basicamente emularia a simples programação procedural com programação orientada a objetos. Você ainda pode fazer qualquer coisa que poderia ter feito de outra forma, já que o método "call ()" de uma classe pode chamar os métodos "call ()" de muitas outras classes.
Vaelus

Respostas:


29

A responsabilidade única pode não ser algo que uma única função possa cumprir.

 class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }

Esta classe pode violar o princípio da responsabilidade única. Não porque tem duas funções, mas se o código getX()e getY()precisa satisfazer diferentes partes interessadas que podem exigir mudanças. Se o vice-presidente Sr. X enviar um memorando de que todos os números serão expressos como números de ponto flutuante e a diretora de contabilidade, a Sra. Y, insistir em que todos os números que suas revisões de departamento permanecerão inteiros, independentemente do que o Sr. X pense bem, então é melhor que essa classe tenha uma única idéia de quem é responsável, porque as coisas estão prestes a ficar confusas.

Se o SRP tivesse sido seguido, seria claro se a classe Location contribui para as coisas às quais o Sr. X e seu grupo estão expostos. Deixe claro o que a classe é responsável e você sabe qual diretiva afeta essa classe. Se ambos impactam essa classe, ela foi mal projetada para minimizar o impacto da mudança. "Uma turma deve ter apenas um motivo para mudar" não significa que toda a turma possa fazer apenas uma coisinha. Isso significa que eu não deveria poder olhar para a classe e dizer que o Sr. X e a Sra. Y têm interesse nessa classe.

Além de coisas assim. Não, vários métodos estão bem. Apenas dê um nome que deixe claro quais métodos pertencem à classe e quais não.

O SRP do tio Bob é mais sobre a lei de Conway do que sobre a lei de Curly . O tio Bob defende a aplicação da lei de Curly (faça uma coisa) a funções e não a classes. O SRP adverte contra os motivos de mistura para alterar juntos. A lei de Conway diz que o sistema seguirá como as informações de uma organização fluem. Isso leva a seguir o SRP porque você não se importa com o que nunca ouve.

"Um módulo deve ser responsável por um e apenas um ator"

Robert C Martin - Arquitetura Limpa

As pessoas continuam querendo que o SRP tenha todos os motivos para limitar o escopo. Há mais motivos para limitar o escopo do que o SRP. Limito ainda mais o escopo, insistindo que a classe seja uma abstração que pode ter um nome que garante que olhar para dentro não o surpreenda .

Você pode aplicar a Lei de Curly às aulas. Você está fora do que o tio Bob fala, mas pode fazê-lo. O erro é quando você começa a pensar que isso significa uma função. É como pensar que uma família deveria ter apenas um filho. Ter mais de um filho não impede que seja uma família.

Se você aplicar a lei de Curly a uma classe, tudo na classe deve ser sobre uma única idéia unificadora. Essa ideia pode ser ampla. A ideia pode ser persistência. Se algumas funções do utilitário de registro estiverem lá, elas estarão claramente fora do lugar. Não importa se o Sr. X é o único que se importa com esse código.

O princípio clássico a ser aplicado aqui é chamado de Separação de Preocupações . Se você separar todas as suas preocupações, pode-se argumentar que o que resta em qualquer lugar é uma preocupação. Foi assim que chamamos essa idéia antes do filme de 1991, City Slickers, que nos apresentou o personagem Curly.

Isto é bom. Só que o que o tio Bob chama de responsabilidade não é uma preocupação. Uma responsabilidade para ele não é algo em que você se concentra. É algo que pode forçá-lo a mudar. Você pode se concentrar em uma preocupação e ainda criar um código responsável por diferentes grupos de pessoas com diferentes agendas.

Talvez você não se importe com isso. Bem. Pensar que segurar para "fazer uma coisa" resolverá todos os problemas de seu design mostra uma falta de imaginação do que "uma coisa" pode acabar sendo. Outro motivo para limitar o escopo é a organização. Você pode aninhar muitas "uma coisa" dentro de outras "uma coisa" até ter uma gaveta de lixo cheia de tudo. Eu já falei sobre isso antes

É claro que o motivo clássico da OOP para limitar o escopo é que a classe possui campos particulares e, em vez de usar getters para compartilhar esses dados, colocamos todos os métodos que precisam desses dados na classe, onde eles podem usar os dados em particular. Muitos acham isso muito restritivo para usar como um limitador de escopo, porque nem todo método que pertence junto usa exatamente os mesmos campos. Eu gosto de garantir que qualquer ideia que reuniu os dados seja a mesma que reuniu os métodos.

A maneira funcional de analisar isso é isso a.f(x)e a.g(x)são simplesmente f a (x) e g a (x). Não são duas funções, mas um continuum de pares de funções que variam juntas. O amesmo não tem de ter dados nela. Pode ser simplesmente como você sabe qual fe gimplementação você usará. Funções que mudam juntas pertencem juntas. Esse é o bom e velho polimorfismo.

O SRP é apenas uma das muitas razões para limitar o escopo. É um bom. Mas não é o único.


25
Eu acho que essa resposta é confusa para alguém tentando descobrir o SRP. A batalha entre o Sr. Presidente e a Sra. Diretora não é resolvida por meios técnicos, e usá-lo para justificar uma decisão de engenharia não faz sentido. A lei de Conway em ação.
whatsisname

8
@whatsisname Pelo contrário. O SRP foi explicitamente destinado a ser aplicado às partes interessadas. Não tem nada a ver com design técnico. Você pode discordar dessa abordagem, mas é assim que o SRP foi originalmente definido pelo tio Bob, e ele teve que reiterá-lo repetidamente porque, por algum motivo, as pessoas parecem não ser capazes de entender essa noção simples (mente, seja é realmente útil é uma questão completamente ortogonal).
Luaan

A lei de Curly, conforme descrito por Tim Ottinger, enfatiza que uma variável deve consistentemente significar uma coisa. Para mim, o SRP é um pouco mais forte que isso; uma classe pode representar conceitualmente "uma coisa", mas violar o SRP se dois fatores externos de mudança tratam algum aspecto dessa "única coisa" de maneiras diferentes ou se preocupam com dois aspectos diferentes. O problema é de modelagem; você optou por modelar algo como uma única classe, mas há algo no domínio que torna essa escolha problemática (as coisas começam a ficar no seu caminho à medida que a base de código evolui).
Filip Milovanović

2
@ FilipMilovanović A semelhança que vejo entre a Lei de Conway e o SRP da maneira como o tio Bob explicou o SRP em seu livro Arquitetura Limpa vem do pressuposto de que a organização possui um organograma acíclico limpo. Esta é uma ideia antiga. Até a Bíblia tem uma citação aqui: "Ninguém pode servir a dois senhores".
candied_orange

11
A @TKK o relaciona (não a iguala) à lei de Conways e não à lei de Curly. Estou refutando a idéia de que o SRP é a lei de Curly principalmente porque o tio Bob disse isso em seu livro Clean Architecture.
candied_orange

48

A chave aqui é o escopo ou, se você preferir, a granularidade . Uma parte da funcionalidade representada por uma classe pode ser ainda mais separada em partes da funcionalidade, cada parte sendo um método.

Aqui está um exemplo. Imagine que você precisa criar um CSV a partir de uma sequência. Se você deseja estar em conformidade com a RFC 4180, levaria algum tempo para implementar o algoritmo e lidar com todos os casos extremos.

Fazer isso em um único método resultaria em um código que não será particularmente legível e, especialmente, o método faria várias coisas ao mesmo tempo. Portanto, você o dividirá em vários métodos; por exemplo, um deles pode estar encarregado de gerar o cabeçalho, ou seja, a primeira linha do CSV, enquanto outro método converteria um valor de qualquer tipo em sua representação de string adequada ao formato CSV, enquanto outro determinaria se um o valor precisa ser colocado entre aspas duplas.

Esses métodos têm sua própria responsabilidade. O método que verifica se é necessário adicionar aspas duplas ou não, tem o seu, e o método que gera o cabeçalho, um. Esse é o SRP aplicado aos métodos .

Agora, todos esses métodos têm um objetivo em comum, ou seja, tomar uma sequência e gerar o CSV. Essa é a responsabilidade única da classe .


Pablo H comentou:

Bom exemplo, mas acho que ainda não responde por que o SRP permite que uma classe tenha mais de um método público.

De fato. O exemplo de CSV que dei foi idealmente um método público e todos os outros métodos são privados. Um exemplo melhor seria de uma fila, implementada por uma Queueclasse. Essa classe conteria basicamente dois métodos: push(também chamado enqueue) e pop(também chamado dequeue).

  • A responsabilidade de Queue.pushé adicionar um objeto à cauda da fila.

  • A responsabilidade Queue.popé remover um objeto da cabeça da fila e lidar com o caso em que a fila está vazia.

  • A responsabilidade da Queueclasse é fornecer uma lógica de fila.


11
Bom exemplo, mas acho que ainda não responde por que o SRP permite que uma classe tenha mais de um método público .
Pablo H

11
@ PabloH: justo. Adicionei outro exemplo no qual uma classe tem dois métodos.
Arseni Mourzenko

30

Uma função é uma função.

Uma responsabilidade é uma responsabilidade.

Um mecânico tem a responsabilidade de consertar carros, o que envolverá diagnósticos, algumas tarefas simples de manutenção, algum trabalho de reparo real, alguma delegação de tarefas a outros etc.

Uma classe de contêiner (lista, matriz, dicionário, mapa etc.) tem a responsabilidade de armazenar objetos, o que envolve armazená-los, permitir a inserção, fornecer acesso, algum tipo de pedido, etc.

Uma única responsabilidade não significa que há muito pouco código / funcionalidade, significa qualquer funcionalidade que "pertença" sob a mesma responsabilidade.


2
Concordar. @Aulis Ronkainen - para amarrar as duas respostas. E para responsabilidades aninhadas, usando sua analogia mecânica, uma garagem é responsável pela manutenção dos veículos. diferentes mecânicos na garagem são responsáveis ​​por diferentes partes do carro, mas cada um desses mecânicos trabalha juntos em coesão
wolfsshield

2
@wolfsshield, concordou. Mecânico que apenas faz uma coisa é inútil, mas mecânico que tem uma única responsabilidade não é (pelo menos necessariamente). Embora as analogias da vida real nem sempre sejam as melhores para descrever conceitos abstratos de POO, é importante distinguir essas diferenças. Eu acredito que não entender a diferença é o que cria a confusão em primeiro lugar.
Aulis Ronkainen

3
@AulisRonkainen Embora pareça, cheire e pareça uma analogia, pretendi usar o mecânico para destacar o significado específico do termo Responsabilidade no SRP. Concordo plenamente com a sua resposta.
Peter

20

Responsabilidade única não significa necessariamente que apenas faz uma coisa.

Tomemos, por exemplo, uma classe de serviço do usuário:

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

Esta classe possui vários métodos, mas sua responsabilidade é clara. Ele fornece acesso aos registros do usuário no armazenamento de dados. Suas únicas dependências são o modelo de usuário e o armazenamento de dados. É fracamente acoplada e altamente coesa, que é realmente o que o SRP está tentando fazer você pensar.

O SRP não deve ser confundido com o "princípio de segregação da interface" (consulte SOLID ). O princípio de segregação de interface (ISP) diz que interfaces menores e leves são preferíveis a interfaces maiores e mais generalizadas. O Go faz uso pesado do ISP em toda a sua biblioteca padrão:

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRP e ISP certamente estão relacionados, mas um não implica o outro. O ISP está no nível da interface e o SRP está no nível da classe. Se uma classe implementa várias interfaces simples, pode não ter mais apenas uma responsabilidade.

Agradecemos a Luaan por apontar a diferença entre ISP e SRP.


3
Na verdade, você está descrevendo o princípio de segregação da interface (o "I" no SOLID). SRP é um animal completamente diferente.
Luaan

Como um aparte, que convenção de codificação você está usando aqui? Eu esperaria que os objetos UserService e Usera ser UpperCamelCase, mas os métodos Create , Existse Updateeu teria feito lowerCamelCase.
KlaymenDK

11
@KlaymenDK Você está certo, as letras maiúsculas são apenas um hábito de usar Go (maiúsculas = exportadas / públicas, minúsculas = privadas)
Jesse

@Luaan Obrigado por apontar isso, vou esclarecer minha resposta
Jesse

11
@KlaymenDK Muitos idiomas usam o PascalCase para métodos e classes. C # por exemplo.
Omegastick

15

Há um chef em um restaurante. Sua única responsabilidade é cozinhar. No entanto, ele pode cozinhar bifes, batatas, brócolis e centenas de outras coisas. Você contrataria um chef por prato no seu menu? Ou um chef para cada componente de cada prato? Ou um chef que pode cumprir sua única responsabilidade: cozinhar?

Se você pedir que o chef faça a folha de pagamento também, é quando você viola o SRP.


4

Contra-exemplo: armazenando estado mutável.

Suponha que você tenha a classe mais simples de todas, cujo único trabalho é armazenar um int.

public class State {
    private int i;


    public State(int i) { this.i = i; }
}

Se você estivesse limitado a apenas 1 método, poderia ter um setState(), ou a getState(), a menos que quebre o encapsulamento e torne ipúblico.

  • Um setter é inútil sem um getter (você nunca pode ler as informações)
  • Um getter é inútil sem um setter (você nunca pode alterar a informação).

Então, claramente, essa responsabilidade única exige ter pelo menos 2 métodos nessa classe. QED.


4

Você está interpretando mal o princípio da responsabilidade única.

Responsabilidade única não é igual a um único método. Eles significam coisas diferentes. No desenvolvimento de software, falamos sobre coesão . Funções (métodos) que possuem alta coesão "pertencem" juntas e podem ser contadas como desempenhando uma única responsabilidade.

Cabe ao desenvolvedor projetar o sistema para que o princípio de responsabilidade única seja cumprido. Pode-se ver isso como uma técnica de abstração e, portanto, às vezes é uma questão de opinião. A implementação do princípio de responsabilidade única torna o código mais fácil de testar e mais fácil de entender sua arquitetura e design.


2

Muitas vezes, é útil (em qualquer idioma, mas especialmente nos idiomas OO) examinar as coisas e organizá-las do ponto de vista dos dados e não das funções.

Portanto, considere a responsabilidade de uma classe de manter a integridade e fornecer ajuda para usar corretamente os dados que possui. Claramente, isso é mais fácil de fazer se todo o código estiver em uma classe, em vez de se espalhar por várias classes. A adição de dois pontos é mais confiável e o código é mantido com mais facilidade, com um Point add(Point p)método noPoint classe do que em outro lugar.

E, em particular, a classe não deve expor nada que possa resultar em dados inconsistentes ou incorretos. Por exemplo, se um Pointdeve estar dentro de um plano (0,0) a (127,127), o construtor e quaisquer métodos que modifiquem ou produzam um novo Pointterão a responsabilidade de verificar os valores dados e rejeitar quaisquer alterações que violem isso. requerimento. (Geralmente, algo como a Pointseria imutável, e garantir que não haja maneiras de modificar um Pointdepois que ele foi construído também seria uma responsabilidade da classe)

Observe que as camadas aqui são perfeitamente aceitáveis. Você pode ter uma Pointaula para lidar com pontos individuais e uma Polygonaula para lidar com um conjunto de Points; elas ainda têm responsabilidades separadas, porque Polygondelega toda a responsabilidade de lidar com qualquer coisa relacionada exclusivamente a Point(como garantir que um ponto tenha xum yvalor e um ) para a Pointclasse.

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.