¿Cómo puede una clase tener múltiples métodos sin romper el principio de responsabilidad única?

64

El principio de responsabilidad única se define en wikipedia como

El principio de responsabilidad única es un principio de programación de computadoras que establece que cada módulo, clase o función debe tener responsabilidad sobre una sola parte de la funcionalidad proporcionada por el software, y esa responsabilidad debe estar completamente encapsulada por la clase

Si una clase solo debe tener una única responsabilidad, ¿cómo puede tener más de 1 método? ¿No tendría cada método una responsabilidad diferente, lo que significaría que la clase tendría más de 1 responsabilidad?

Todos los ejemplos que he visto que demuestran el principio de responsabilidad única utilizan una clase de ejemplo que solo tiene un método. Puede ser útil ver un ejemplo o tener una explicación de una clase con múltiples métodos que aún pueden considerarse como una responsabilidad.

ganso
fuente
11
¿Por qué un voto negativo? Parece una pregunta ideal para SE.SE; la persona investigó el tema e hizo un esfuerzo para dejar la pregunta muy clara. Merece votos a favor en su lugar.
Arseni Mourzenko
19
El voto negativo probablemente se debió a que esta es una pregunta que ya se ha formulado y respondido varias veces, por ejemplo, consulte softwareengineering.stackexchange.com/questions/345018/… . En mi opinión, no agrega aspectos nuevos sustanciales.
Hans-Martin Mosner
99
Esto es simplemente reductio ad absurdum. Si a cada clase se le permitiera literalmente un solo método, entonces literalmente no hay forma de que ningún programa pueda hacer más de una cosa.
Darrel Hoffman
66
@DarrelHoffman Eso no es cierto. Si cada clase fuera un funtor con solo un método "call ()", entonces básicamente has emulado una programación procesal simple con programación orientada a objetos. Aún puede hacer cualquier cosa que podría haber hecho de otra manera, ya que el método "call ()" de una clase puede llamar a los métodos "call ()" de muchas otras clases.
Vaelus

Respuestas:

29

La responsabilidad única podría no ser algo que una sola función pueda cumplir.

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

Esta clase puede romper el principio de responsabilidad única. No porque tenga dos funciones, sino por el código getX()y getY()para satisfacer a diferentes partes interesadas que pueden exigir un cambio. Si el Vicepresidente Sr. X envía un memorando de que todos los números se expresarán como números de coma flotante y la Directora de Contabilidad, la Sra. Y insiste en que todos los números que revisa su departamento permanecerán enteros independientemente de lo que el Sr. X piense bien, entonces esta clase debería tener Una sola idea de quién es responsable porque las cosas están a punto de volverse confusas.

Si se hubiera seguido SRP, estaría claro si la clase de ubicación contribuye a las cosas a las que están expuestos el Sr. X y su grupo. Deje en claro de qué es responsable la clase y sepa qué directiva impacta esta clase. Si ambos impactan en esta clase, entonces estaba mal diseñada para minimizar el impacto del cambio. "Una clase solo debe tener una razón para cambiar" no significa que toda la clase solo pueda hacer una pequeña cosa. Significa que no debería poder mirar la clase y decir que tanto el Sr. X como la Sra. Y tienen interés en esta clase.

Aparte de cosas como esas. No, múltiples métodos están bien. Simplemente dele un nombre que aclare qué métodos pertenecen a la clase y cuáles no.

El SRP del tío Bob tiene más que ver con la ley de Conway que con la ley de Curly . El tío Bob aboga por aplicar la Ley de Curly (hacer una cosa) a funciones, no a clases. SRP advierte contra los motivos de mezcla para cambiar juntos. La Ley de Conway dice que el sistema seguirá cómo fluye la información de una organización. Eso lleva a seguir SRP porque no te importa lo que nunca escuchas.

"Un módulo debe ser responsable ante un solo actor"

Robert C Martin - Arquitectura limpia

La gente sigue queriendo que SRP sea sobre todas las razones para limitar el alcance. Hay más razones para limitar el alcance que SRP. Limito aún más el alcance al insistir en que la clase sea una abstracción que puede tomar un nombre que garantice que mirar dentro no te sorprenda .

Puedes aplicar la Ley de Curly a las clases. Estás fuera de lo que habla el tío Bob, pero puedes hacerlo. Cuando te equivocas es cuando comienzas a pensar que eso significa una función. Es como pensar que una familia solo debe tener un hijo. Tener más de un hijo no impide que sea una familia.

Si aplica la ley de Curly a una clase, todo en la clase debe tratarse de una sola idea unificadora. Esa idea puede ser amplia. La idea podría ser la persistencia. Si algunas funciones de utilidad de registro están allí, entonces están claramente fuera de lugar. No importa si el Sr. X es el único a quien le importa este código.

El principio clásico para aplicar aquí se llama Separación de preocupaciones . Si separa todas sus preocupaciones, se podría argumentar que lo que queda en cualquier lugar es una preocupación. Así llamamos a esta idea antes de que la película de 1991 City Slickers nos presentara al personaje Curly.

Esto esta bien. Es solo que lo que el tío Bob llama responsabilidad no es una preocupación. Una responsabilidad hacia él no es algo en lo que te concentres. Es algo que puede obligarte a cambiar. Puede concentrarse en una preocupación y aún así crear código que sea responsable ante diferentes grupos de personas con diferentes agendas.

Tal vez no te importe eso. Multa. Pensar que sostener "hacer una cosa" resolverá todos sus problemas de diseño muestra una falta de imaginación de lo que "una cosa" puede llegar a ser. Otra razón para limitar el alcance es la organización. Puede anidar muchas "una cosa" dentro de otras "una cosa" hasta que tenga un cajón de basura lleno de todo. He hablado de eso antes

Por supuesto, la razón clásica de OOP para limitar el alcance es que la clase tiene campos privados y, en lugar de usar getters para compartir esos datos, colocamos todos los métodos que necesitan esos datos en la clase donde pueden usar los datos en privado. Muchos consideran que esto es demasiado restrictivo para usarlo como limitador de alcance porque no todos los métodos que pertenecen juntos usan exactamente los mismos campos. Me gusta asegurarme de que cualquier idea que reunió los datos sea la misma idea que unió los métodos.

La forma funcional de ver esto es eso a.f(x)y a.g(x)son simplemente f a (x) y g a (x). No dos funciones, sino un continuo de pares de funciones que varían juntas. El ani siquiera tiene que tener los datos en ella. Podría ser simplemente cómo sabes qué implementación fy gqué vas a usar. Las funciones que cambian juntas pertenecen juntas. Eso es buen viejo polimorfismo.

SRP es solo una de las muchas razones para limitar el alcance. Es una buena. Pero no el único.

naranja confitada
fuente
25
Creo que esta respuesta es confusa para alguien que intenta descifrar el SRP. La batalla entre el señor presidente y la señora directora no se resuelve por medios técnicos y usarla para justificar una decisión de ingeniería no tiene sentido. La ley de Conway en acción.
cuál es el
8
@whatsisname Por el contrario. El SRP tenía el propósito explícito de aplicarse a las partes interesadas. No tiene nada que ver con el diseño técnico. Puede que no esté de acuerdo con ese enfoque, pero así es como SRP ha sido originalmente definido por el tío Bob, y tuvo que reiterarlo una y otra vez porque, por alguna razón, las personas no parecen ser capaces de entender esta simple noción (tenga en cuenta si es realmente útil es una pregunta completamente ortogonal).
Luaan
La Ley de Curly, según lo descrito por Tim Ottinger, enfatiza que una variable debe significar consistentemente una cosa. Para mí, SRP es un poco más fuerte que eso; una clase puede representar conceptualmente "una cosa", pero violar SRP si dos impulsores externos del cambio tratan algún aspecto de esa "una cosa" de diferentes maneras, o si están preocupados por dos aspectos diferentes. El problema es uno de modelado; ha elegido modelar algo como una sola clase, pero hay algo en el dominio que hace que esa elección sea problemática (las cosas comienzan a interponerse en su camino a medida que evoluciona la base de código).
Filip Milovanović
2
@ FilipMilovanović La similitud que veo entre la Ley de Conway y SRP en la forma en que el tío Bob explicó SRP en su libro de Arquitectura limpia proviene de la suposición de que la organización tiene un organigrama acíclico limpio. Esta es una vieja idea. Incluso la Biblia tiene una cita aquí: "Ningún hombre puede servir a dos señores".
candied_orange
1
@TKK lo relaciono (no lo equiparo) con la ley de Conways, no con la ley de Curly. Estoy refutando la idea de que SRP es la ley de Curly principalmente porque el tío Bob lo dijo en su libro de Arquitectura limpia.
candied_orange
48

La clave aquí es el alcance o, si lo prefiere, la granularidad . Una parte de la funcionalidad representada por una clase se puede separar en partes de la funcionalidad, cada parte es un método.

Aquí hay un ejemplo. Imagine que necesita crear un CSV a partir de una secuencia. Si desea cumplir con RFC 4180, tomaría bastante tiempo implementar el algoritmo y manejar todos los casos límite.

Hacerlo en un solo método daría como resultado un código que no será particularmente legible, y especialmente, el método haría varias cosas a la vez. Por lo tanto, lo dividirá en varios métodos; por ejemplo, uno de ellos puede estar a cargo de generar el encabezado, es decir, la primera línea del CSV, mientras que otro método convertiría un valor de cualquier tipo a su representación de cadena adecuada para el formato CSV, mientras que otro determinaría si un el valor debe estar entre comillas dobles.

Esos métodos tienen su propia responsabilidad. El método que verifica si es necesario agregar comillas dobles o no tiene el suyo, y el método que genera el encabezado tiene uno. Este es SRP aplicado a los métodos .

Ahora, todos esos métodos tienen un objetivo en común, es decir, tomar una secuencia y generar el CSV. Esta es la responsabilidad única de la clase .


Pablo H comentó:

Buen ejemplo, pero creo que aún no responde por qué SRP permite que una clase tenga más de un método público.

En efecto. El ejemplo CSV que di tiene idealmente un método público y todos los demás métodos son privados. Un mejor ejemplo sería una cola, implementada por una Queueclase. Esta clase contendría, básicamente, dos métodos: push(también llamado enqueue) y pop(también llamado dequeue).

  • La responsabilidad de Queue.pushes agregar un objeto a la cola de la cola.

  • La responsabilidad de Queue.popes eliminar un objeto de la cabeza de la cola y manejar el caso donde la cola está vacía.

  • La responsabilidad de la Queueclase es proporcionar una lógica de cola.

Arseni Mourzenko
fuente
1
Buen ejemplo, pero creo que aún no responde por qué SRP permite que una clase tenga más de un método público .
Pablo H
1
@PabloH: justo. Agregué otro ejemplo en el que una clase tiene dos métodos.
Arseni Mourzenko
30

Una función es una función.

Una responsabilidad es una responsabilidad.

Un mecánico tiene la responsabilidad de reparar automóviles, lo que implicará diagnósticos, algunas tareas de mantenimiento simples, algunos trabajos de reparación reales, algunas delegaciones de tareas a otros, etc.

Una clase de contenedor (lista, matriz, diccionario, mapa, etc.) tiene la responsabilidad de almacenar objetos, lo que implica almacenarlos, permitir su inserción, proporcionar acceso, algún tipo de orden, etc.

Una sola responsabilidad no significa que haya muy poco código / funcionalidad, significa que cualquier funcionalidad que haya "pertenece" bajo la misma responsabilidad.

Peter
fuente
2
Concurrir. @Aulis Ronkainen: para unir las dos respuestas. Y para las responsabilidades anidadas, utilizando su analogía mecánica, un garaje tiene la responsabilidad del mantenimiento de los vehículos. diferentes mecánicos en el garaje tienen la responsabilidad de diferentes partes del automóvil, pero cada una de estas mecánicas trabaja juntas en cohesión
wolfsshield
2
@wolfsshield, de acuerdo. El mecánico que solo hace una cosa es inútil, pero el mecánico que tiene una sola responsabilidad no lo es (al menos necesariamente). Aunque las analogías de la vida real no siempre son las mejores para describir conceptos abstractos de OOP, es importante distinguir estas diferencias. Creo que no entender la diferencia es lo que crea la confusión en primer lugar.
Aulis Ronkainen
3
@AulisRonkainen Si bien se ve, huele y se siente como una analogía, tenía la intención de utilizar la mecánica para resaltar el significado específico del término Responsabilidad en SRP. Estoy completamente de acuerdo con tu respuesta.
Peter
20

La responsabilidad individual no necesariamente significa que solo hace una cosa.

Tomemos, por ejemplo, una clase de servicio de usuario:

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 clase tiene múltiples métodos, pero su responsabilidad es clara. Proporciona acceso a los registros de usuario en el almacén de datos. Sus únicas dependencias son el modelo de usuario y el almacén de datos. Está ligeramente acoplado y es altamente cohesivo, que realmente es lo que SRP está tratando de hacer que pienses.

SRP no debe confundirse con el "principio de segregación de interfaz" (ver SÓLIDO ). El principio de segregación de interfaz (ISP) dice que las interfaces más pequeñas y livianas son preferibles que las interfaces más grandes y más generalizadas. Go hace un uso intensivo de ISP en toda su biblioteca estándar:

// 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 están ciertamente relacionados, pero uno no implica el otro. ISP está en el nivel de interfaz y SRP está en el nivel de clase. Si una clase implementa varias interfaces simples, es posible que ya no tenga una sola responsabilidad.

Gracias a Luaan por señalar la diferencia entre ISP y SRP.

Jesse
fuente
3
En realidad, está describiendo el principio de segregación de la interfaz (la "I" en SÓLIDO). SRP es una bestia bastante diferente.
Luaan
Además, ¿qué convención de codificación estás usando aquí? Yo esperaría que los objetos UserService y Userpara ser UpperCamelCase, pero los métodos Create , Existsy Updatehabría hecho lowerCamelCase.
KlaymenDK
1
@KlaymenDK Tienes razón, la mayúscula es solo un hábito del uso de Go (mayúscula = exportado / público, minúscula = privado)
Jesse
@Luaan Gracias por señalar eso, aclararé mi respuesta
Jesse
1
@KlaymenDK Muchos idiomas usan PascalCase para métodos y clases. C # por ejemplo.
Omegastick
15

Hay un chef en un restaurante. Su única responsabilidad es cocinar. Sin embargo, puede cocinar filetes, papas, brócoli y cientos de otras cosas. ¿Contrataría a un chef por plato en su menú? ¿O un chef para cada componente de cada plato? O un chef que puede cumplir con su única responsabilidad: ¿cocinar?

Si le pide a ese chef que haga la nómina también, es cuando viola SRP.

gnasher729
fuente
4

Contraejemplo: almacenamiento de estado mutable.

Supongamos que tiene la clase más simple, cuyo único trabajo es almacenar un int.

public class State {
    private int i;


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

Si se limitara a solo 1 método, podría tener un setState(), o un getState(), a menos que rompa la encapsulación y haga ipúblico.

  • Un setter es inútil sin un getter (nunca podrías leer la información)
  • Un getter es inútil sin un setter (nunca puedes mutar la información).

Claramente, esta responsabilidad única requiere tener al menos 2 métodos en esta clase. QED

Alejandro
fuente
4

Estás malinterpretando el principio de responsabilidad única.

La responsabilidad individual no equivale a un solo método. Significan cosas diferentes. En el desarrollo de software hablamos de cohesión . Las funciones (métodos) que tienen una alta cohesión "pertenecen" juntas y pueden considerarse como una responsabilidad única.

Depende del desarrollador diseñar el sistema para que se cumpla el principio de responsabilidad única. Uno puede ver esto como una técnica de abstracción y, por lo tanto, a veces es una cuestión de opinión. La implementación del principio de responsabilidad única hace que el código sea más fácil de probar y de comprender su arquitectura y diseño.

Aulis Ronkainen
fuente
2

A menudo es útil (en cualquier idioma, pero especialmente en los idiomas OO) mirar las cosas y organizarlas desde el punto de vista de los datos en lugar de las funciones.

Por lo tanto, considere que la responsabilidad de una clase es mantener la integridad y proporcionar ayuda para utilizar correctamente los datos que posee. Claramente, esto es más fácil de hacer si todo el código está en una clase, en lugar de distribuirse en varias clases. Agregar dos puntos se hace de manera más confiable, y el código se mantiene más fácilmente, con un Point add(Point p)método en la Pointclase que tenerlo en otro lugar.

Y en particular, la clase no debe exponer nada que pueda resultar en datos inconsistentes o incorrectos. Por ejemplo, si un Pointdebe estar dentro de un plano (0,0) a (127,127), el constructor y cualquier método que modifique o produzca uno nuevo Pointtienen la responsabilidad de verificar los valores que se les dan y rechazar cualquier cambio que pueda violar esto requisito. (A menudo, algo como a Pointsería inmutable, y garantizar que no haya formas de modificar un Pointdespués de que se construya también sería una responsabilidad de la clase)

Tenga en cuenta que las capas aquí son perfectamente aceptables. Puede tener una Pointclase para tratar puntos individuales y una Polygonclase para tratar un conjunto de Points; estos todavía tienen responsabilidades separadas porque Polygondelega toda la responsabilidad de tratar cualquier cosa que tenga que ver únicamente con un Point(como garantizar que un punto tenga tanto un valor xcomo un yvalor) para la Pointclase.

Curt J. Sampson
fuente