Aclarar el principio de responsabilidad única

64

El Principio de Responsabilidad Única establece que una clase debe hacer una y solo una cosa. Algunos casos son bastante claros. Otros, sin embargo, son difíciles porque lo que parece "una cosa" cuando se ve en un nivel dado de abstracción puede ser varias cosas cuando se ve en un nivel inferior. También temo que si se respeta el Principio de Responsabilidad Única en los niveles más bajos, puede resultar un código de ravioles excesivamente desacoplado y detallado, en el que se gastan más líneas creando clases pequeñas para todo y sondeando información en lugar de resolver el problema en cuestión.

¿Cómo describirías lo que significa "una cosa"? ¿Cuáles son algunos signos concretos de que una clase realmente hace más que "una cosa"?

dsimcha
fuente
66
+1 para el "código de ravioles" superior. Al principio de mi carrera, fui una de esas personas que lo llevó demasiado lejos. No solo con clases, sino también con la modularización de métodos. Mi código estaba salpicado de toneladas de pequeños métodos que hacían algo simple, solo por el hecho de dividir un problema en pequeños trozos que podrían caber en la pantalla sin desplazarse. Obviamente, esto a menudo iba demasiado lejos.
Bobby Tables

Respuestas:

50

Me gusta mucho la forma en que Robert C. Martin (tío Bob) reitera el Principio de responsabilidad única (vinculado a PDF) :

Nunca debería haber más de una razón para que una clase cambie

Es sutilmente diferente de la definición tradicional de "debería hacer una sola cosa", y me gusta porque te obliga a cambiar la forma en que piensas sobre tu clase. En lugar de pensar en "¿esto está haciendo una cosa?", Piensas qué puede cambiar y cómo esos cambios afectan a tu clase. Entonces, por ejemplo, si la base de datos cambia, ¿su clase necesita cambiar? ¿Qué pasa si el dispositivo de salida cambia (por ejemplo, una pantalla, un dispositivo móvil o una impresora)? Si su clase necesita cambiar debido a cambios de muchas otras direcciones, entonces esa es una señal de que su clase tiene demasiadas responsabilidades.

En el artículo vinculado, el tío Bob concluye:

El SRP es uno de los principios más simples y uno de los más difíciles de acertar. Unir responsabilidades es algo que hacemos naturalmente. Encontrar y separar esas responsabilidades entre sí es en gran parte de lo que se trata realmente el diseño de software.

Paddyslacker
fuente
2
También me gusta la forma en que lo dice, parece más fácil verificarlo cuando se relaciona con el cambio que con la "responsabilidad" abstracta.
Matthieu M.
Esa es realmente una excelente manera de decirlo. Eso me encanta Como nota, generalmente tiendo a pensar que el SRP se aplica con mucha más fuerza a los métodos. A veces, una clase solo tiene que hacer dos cosas (tal vez es la clase que une dos dominios), pero un método casi nunca debería estar haciendo más de lo que puede describir sucintamente por su firma de tipo.
CodexArcanum
1
Acabo de mostrárselo a mi Grad: una gran lectura y un maldito buen recordatorio para mí.
Martijn Verburg
1
Ok, esto tiene mucho sentido cuando lo combinas con la idea de que el código sin ingeniería excesiva solo debe planificar el cambio que es probable en el futuro previsible, no para cada cambio posible. Replantearía esto, entonces, ligeramente como "solo debería haber una razón para que una clase cambie que es probable que ocurra en el futuro previsible". Esto anima a favorecer la simplicidad en partes del diseño que es poco probable que cambien y desacoplarse en partes que probablemente cambien.
dsimcha 01 de
18

Sigo preguntándome qué problema está tratando de resolver SRP. ¿Cuándo me ayuda SRP? Esto es lo que se me ocurrió:

Debe refactorizar la responsabilidad / funcionalidad fuera de una clase cuando:

1) Has duplicado la funcionalidad (DRY)

2) Descubres que tu código necesita otro nivel de abstracción para ayudarte a entenderlo (KISS)

3) Usted encuentra que los expertos en dominio entienden que las funciones son parte de un componente diferente (lenguaje ubicuo)

NO DEBE refactorizar la responsabilidad fuera de una clase cuando:

1) No hay ninguna funcionalidad duplicada.

2) La funcionalidad no tiene sentido fuera del contexto de su clase. Dicho de otra manera, su clase proporciona un contexto en el que es más fácil entender la funcionalidad.

3) Sus expertos en dominios no tienen un concepto de esa responsabilidad.

Se me ocurre que si SRP se aplica de manera amplia, intercambiamos un tipo de complejidad (tratando de hacer cara o cruz de una clase con demasiadas cosas dentro) con otro tipo (tratando de mantener a todos los colaboradores / niveles de abstracción directa para descubrir qué hacen realmente estas clases).

En caso de duda, ¡déjalo afuera! Siempre puede refactorizar más tarde, cuando haya un caso claro para ello.

¿Qué piensas?

Brandon
fuente
Si bien estas pautas pueden o no ser útiles, en realidad no tiene nada que ver con el SRP como se define en SOLID, ¿verdad?
sara
Gracias. No puedo creer algo de la locura involucrada en los llamados principios SÓLIDOS, donde hacen que el código muy simple sea cien veces más complejo sin una buena razón . Los puntos que da describen las razones del mundo real para implementar SRP. Creo que los principios que mencionó anteriormente deberían convertirse en su propio acrónimo, y descartar "SÓLIDO", hace más daño que bien. "Astronautas de la arquitectura" de hecho, como usted señaló en el hilo que me condujo aquí.
Nicholas Petersen
4

No sé si hay una escala objetiva, pero lo que lo delataría serían los métodos, no tanto el número de ellos sino la variedad de su función. Estoy de acuerdo en que puede llevar la descomposición demasiado lejos, pero no seguiría ninguna regla dura y rápida al respecto.

Jeremy
fuente
4

La respuesta está en la definición.

Lo que define la responsabilidad de ser, en última instancia, le da el límite.

Ejemplo:

Tengo un componente que tiene la responsabilidad de mostrar facturas -> en este caso, si comencé a agregar algo más, entonces estoy rompiendo el principio.

Por otro lado, si dije responsabilidad de manejar las facturas -> agregar múltiples funciones más pequeñas (por ejemplo, imprimir Factura , actualizar Factura ) todas están dentro de ese límite.

Sin embargo, si ese módulo comenzó a manejar cualquier funcionalidad fuera de las facturas , entonces estaría fuera de ese límite.

Noche oscura
fuente
Supongo que el principal problema aquí es la palabra "manejo". Es demasiado genérico para definir una sola responsabilidad. Creo que sería mejor tener un componente responsable de la impresión de la factura y otro para la actualización de la factura en lugar de un solo componente que maneje {imprimir, actualizar y, ¿por qué no? - mostrar, lo que sea} factura .
Machado
1
El OP esencialmente pregunta "¿Cómo define una responsabilidad?" así que cuando dices que la responsabilidad es lo que definas, parece que solo se trata de repetir la pregunta.
Despertar
2

Siempre lo veo en dos niveles:

  • Me aseguro de que mis métodos solo hagan una cosa y lo hagan bien
  • Veo una clase como una agrupación lógica (OO) de esos métodos que representa una cosa bien

Entonces, algo así como un objeto de dominio llamado Perro:

Doges mi clase pero los perros pueden hacer muchas cosas! Podría tener métodos como walk(), run()y bite(DotNetDeveloper spawnOfBill)(lo siento, no pude resistir; p).

Si Dogse vuelve difícil de manejar a continuación, que lo pensaría cómo los grupos de esos métodos se pueden modelar juntos en otra clase, como una Movementclase, lo que podría contener mis walk()y run()métodos.

No existe una regla estricta y rápida, su diseño OO evolucionará con el tiempo. Intento buscar una interfaz clara / API pública, así como métodos simples que hagan una cosa y una cosa bien.

Martijn Verburg
fuente
Bite realmente debería tomar una instancia de Object, y un DotNetDeveloper debería ser una subclase de Person (¡generalmente, de todos modos!)
Alan Pearce
@Alan - No - fijo que para usted :-)
Martijn Verburg
1

Lo miro más a lo largo de las líneas de una clase solo debería representar una cosa. Apropiarse @ ejemplo de Karianna, tengo a mi Dogclase, que tiene métodos para walk(), run()y bark(). No voy a añadir métodos para meaow(), squeak(), slither()o fly()porque esas no son cosas que hacen los perros. Son cosas que hacen otros animales, y esos otros animales tendrían sus propias clases para representarlos.

(Por cierto, si su perro no vuela, entonces probablemente debería dejar de arrojándolo por la ventana).

JohnL
fuente
+1 para "si tu perro vuela, entonces probablemente deberías dejar de tirarlo por la ventana". :)
Bobby Tables
Más allá de la pregunta de qué debería representar la clase , ¿qué representa una instancia ? Si se considera SeesFoodcomo una característica de DogEyes, Barkcomo algo hecho por a DogVoice, y Eatcomo algo hecho por a DogMouth, entonces if (dog.SeesFood) dog.Eat(); else dog.Bark();se convertirá en una lógica similar if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, perdiendo cualquier sentido de identidad de que los ojos, la boca y la voz están todos conectados a un solo derecho.
Supercat
@supercat es un punto justo, aunque el contexto es importante. Si el código que menciona está dentro de la Dogclase, entonces probablemente esté Dogrelacionado. Si no, entonces probablemente terminarías con algo así en myDog.Eyes.SeesFoodlugar de solo eyes.SeesFood. Otra posibilidad es que Dogexpone la ISeeinterfaz que exige la Dog.Eyespropiedad y el SeesFoodmétodo.
JohnL 01 de
@JohnL: Si la mecánica real de ver es manejada por los ojos de un perro, esencialmente de la misma manera que la de un gato o una cebra, entonces puede tener sentido que una Eyeclase maneje la mecánica , pero un perro debería "ver" usando sus ojos, en lugar de simplemente tener ojos que puedan ver. Un perro no es un ojo, pero tampoco es simplemente un sostenedor de ojos. Es una "cosa que puede [al menos intentar] ver", y debe describirse a través de la interfaz como tal. Incluso a un perro ciego se le puede preguntar si ve comida; no será muy útil, ya que el perro siempre dirá "no", pero no tiene nada de malo preguntar.
supercat
Entonces usarías la interfaz ISee como describo en mi comentario.
JohnL
1

Una clase debe hacer una cosa cuando se ve en su propio nivel de abstracción. Sin duda hará muchas cosas en un nivel menos abstracto. Así es como funcionan las clases para hacer que los programas sean más fáciles de mantener: ocultan detalles de implementación si no necesita examinarlos de cerca.

Yo uso los nombres de clase como prueba para esto. Si no puedo dar a una clase un nombre descriptivo bastante corto, o si ese nombre tiene una palabra como "Y", probablemente esté violando el Principio de Responsabilidad Única.

En mi experiencia, es más fácil mantener este principio en los niveles inferiores, donde las cosas son más concretas.

David Thornley
fuente
0

Se trata de tener un rol único .

Cada clase debe resumirse con un nombre de rol. Un rol es de hecho un (conjunto de) verbo (s) asociado (s) con un contexto.

Por ejemplo :

Archivo proporciona acceso a un archivo. FileManager gestiona objetos File.

Datos de retención de recursos para un recurso de un archivo. ResourceManager mantiene y proporciona todos los recursos.

Aquí puede ver que algunos verbos como "administrar" implican un conjunto de otros verbos. Los verbos solos se consideran mejor como funciones que las clases, la mayoría de las veces. Si el verbo implica demasiadas acciones que tienen su propio contexto común, entonces debería ser una clase en sí misma.

Por lo tanto, la idea es solo permitirle tener una idea simple de lo que hace la clase al definir un rol único, que podría ser el agregado de varios sub-roles (realizados por objetos miembros u otros objetos).

A menudo construyo clases de Manager que tienen varias otras clases diferentes. Como una Fábrica, un Registro, etc. Vea una clase de Gerente como una especie de jefe de grupo, un jefe de orquesta que guía a otras personas a trabajar juntas para lograr una idea de alto nivel. Él tiene un rol, pero implica trabajar con otros roles únicos en su interior. También se puede ver cómo se organiza una empresa: un CEO no es productivo en el nivel de productividad pura, pero si no está allí, entonces nada puede funcionar correctamente juntos. Ese es su papel.

Cuando diseñe, identifique roles únicos. Y para cada rol, nuevamente vea si no se puede cortar en varios otros roles. De esa manera, si necesita cambiar fácilmente la forma en que su Gerente construye objetos, simplemente cambie la Fábrica y vaya con tranquilidad.

Klaim
fuente
-1

El SRP no se trata solo de dividir las clases, sino también de delegar la funcionalidad.

En el ejemplo de Dog utilizado anteriormente, no use SRP como justificación para tener 3 clases separadas como DogBarker, DogWalker, etc. (baja cohesión). En cambio, mire la implementación de los métodos de una clase y decida si "saben demasiado". Todavía puede tener dog.walk (), pero probablemente el método walk () debería delegar a otra clase los detalles de cómo se logra caminar.

En efecto, estamos permitiendo que la clase Perro tenga una razón para cambiar: porque los Perros cambian. Por supuesto, combinando esto con otros principios SÓLIDOS, Extendería Dog para una nueva funcionalidad en lugar de cambiar Dog (abierto / cerrado). E inyectaría sus dependencias como IMove e IEat. Y, por supuesto, haría estas interfaces separadas (segregación de interfaz). El perro solo cambiaría si encontramos un error o si los perros cambian fundamentalmente (Liskov Sub, no extienda y luego elimine el comportamiento).

El efecto neto de SOLID es que podemos escribir código nuevo con más frecuencia de la que tenemos que modificar el código existente, y eso es una gran victoria.

usuario1488779
fuente
-1

Todo depende de la definición de responsabilidad y de cómo esa definición va a afectar el mantenimiento de su código. Todo se reduce a una cosa y así es como su diseño lo ayudará a mantener su código.

Y como alguien dijo: "Caminar sobre el agua y diseñar software a partir de especificaciones específicas es fácil, siempre que ambos estén congelados".

Entonces, si estamos definiendo la responsabilidad de una manera más concreta, entonces no necesitamos cambiarla.

A veces las responsabilidades serán evidentemente obvias, pero a veces puede ser sutil y debemos decidir juiciosamente.

Supongamos que agregamos otra responsabilidad a la clase Dog llamada catchThief (). Ahora podría estar conduciendo a una responsabilidad diferente adicional. Mañana, si el Departamento de Policía debe cambiar la forma en que el Perro atrapa al ladrón, entonces la clase del Perro tendrá que cambiar. Sería mejor crear otra subclase en este caso y llamarla ThiefCathcerDog. Pero desde una perspectiva diferente, si estamos seguros de que no va a cambiar en ninguna circunstancia, o la forma en que se ha implementado catchThief depende de algún parámetro externo, entonces está perfectamente bien tener esta responsabilidad. Si las responsabilidades no son sorprendentemente extrañas, entonces tenemos que decidirlo juiciosamente en función del caso de uso.

AKS
fuente
-1

"Una razón para cambiar" depende de quién está usando el sistema. Asegúrese de tener casos de uso para cada actor y haga una lista de los cambios más probables; para cada posible cambio de caso de uso, asegúrese de que solo una clase se vea afectada por ese cambio. Si está agregando un caso de uso completamente nuevo, asegúrese de que solo necesite extender una clase para hacerlo.

kiwicomb123
fuente
1
Una razón para cambiar no tiene nada que ver con la cantidad de casos de uso, actores o la probabilidad de un cambio. No debería estar haciendo una lista de posibles cambios. Es correcto que un cambio solo afecte a una clase. Poder extender una clase para acomodar ese cambio es bueno, pero ese es el principio abierto y cerrado, no SRP.
candied_orange
¿Por qué no deberíamos estar haciendo una lista de los cambios más probables? Diseño especulativo?
kiwicomb123
porque no ayudará, no estará completo, y hay formas más efectivas de lidiar con el cambio que tratar de predecirlo. Solo esperalo. Aislar las decisiones y el impacto será mínimo.
candied_orange
Bien, entiendo, viola el principio de "no lo necesitarás".
kiwicomb123
En realidad, yagni puede ser empujado demasiado lejos también. Realmente está destinado a evitar que implemente casos de uso especulativo. No para evitar aislar un caso de uso implementado.
candied_orange