Cuando se utiliza el Principio de responsabilidad única, ¿qué constituye una "responsabilidad"?

198

Parece bastante claro que "Principio de responsabilidad única" no significa "solo hace una cosa". Para eso son los métodos.

public Interface CustomerCRUD
{
    public void Create(Customer customer);
    public Customer Read(int CustomerID);
    public void Update(Customer customer);
    public void Delete(int CustomerID);
}

Bob Martin dice que "las clases deberían tener una sola razón para cambiar". Pero eso es difícil de entender si eres un programador nuevo en SOLID.

Escribí una respuesta a otra pregunta , donde sugerí que las responsabilidades son como títulos de trabajo, y bailé alrededor del tema usando una metáfora de restaurante para ilustrar mi punto. Pero eso aún no articula un conjunto de principios que alguien podría usar para definir las responsabilidades de sus clases.

Entonces, ¿cómo lo haces? ¿Cómo determina qué responsabilidades debe tener cada clase y cómo define una responsabilidad en el contexto de SRP?

Robert Harvey
fuente
28
Publicar en Revisión de Código y ser destrozado :-D
Jörg W Mittag
8
@ JörgWMittag Hola, no
asustes a la
118
Preguntas como esta de miembros veteranos demuestran que las reglas y principios que intentamos mantener no son en absoluto sencillos o simples . Son [algo así como] contradictorios y místicos ... como debería ser cualquier buen conjunto de reglas . Y, me gustaría creer preguntas como esta, humilde el sabio, y dar esperanza a aquellos que se sienten irremediablemente estúpidos. Gracias Robert!
svidgen
41
Me pregunto si esta pregunta hubiera sido rechazada + marcada como duplicada de inmediato si hubiera sido publicada por un novato :)
Andrejs
99
@rmunn: o en otras palabras, el gran representante atrae aún más representantes, porque nadie canceló los prejuicios humanos básicos en stackexchange
Andrejs

Respuestas:

117

Una forma de entender esto es imaginar posibles cambios en los requisitos en futuros proyectos y preguntarse qué necesitará hacer para que se cumplan.

Por ejemplo:

Nuevo requisito comercial: los usuarios ubicados en California obtienen un descuento especial.

Ejemplo de cambio "bueno": necesito modificar el código en una clase que calcule los descuentos.

Ejemplo de cambios incorrectos: necesito modificar el código en la clase Usuario, y ese cambio tendrá un efecto en cascada en otras clases que usan la clase Usuario, incluidas las clases que no tienen nada que ver con descuentos, por ejemplo, inscripción, enumeración y administración.

O:

Nuevo requisito no funcional: comenzaremos a usar Oracle en lugar de SQL Server

Ejemplo de buen cambio: solo necesita modificar una sola clase en la capa de acceso a datos que determina cómo persistir los datos en los DTO.

Mal cambio: necesito modificar todas mis clases de capa empresarial porque contienen lógica específica de SQL Server.

La idea es minimizar la huella de futuros cambios potenciales, restringiendo las modificaciones de código a un área de código por área de cambio.

Como mínimo, sus clases deben separar las preocupaciones lógicas de las físicas. Un gran conjunto de ejemplos se pueden encontrar en el System.IOespacio de nombres: no podemos encontrar un varios tipos de flujos físicos (por ejemplo FileStream, MemoryStreamo NetworkStream) y varios lectores y escritores ( BinaryWriter, TextWriter) que funcionan en un nivel lógico. Por que los separa de esta manera, evitamos explosión combinatoria: en lugar de tener FileStreamTextWriter, FileStreamBinaryWriter, NetworkStreamTextWriter, NetworkStreamBinaryWriter, MemoryStreamTextWriter, y MemoryStreamBinaryWriter, que simplemente conectar el escritor y la corriente y se puede tener lo que quiere. Luego, más tarde, podemos agregar, digamos, un XmlWriter, sin necesidad de volver a implementarlo para la memoria, el archivo y la red por separado.

John Wu
fuente
34
Si bien estoy de acuerdo con pensar en el futuro, hay principios como YAGNI y metodologías como TDD que sugieren lo contrario.
Robert Harvey
87
YAGNI nos dice que no construyamos cosas que no necesitamos hoy. No le dice a no construir cosas de una manera que sea extensible. Consulte también el principio abierto / cerrado , que establece que "las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión, pero cerradas para la modificación".
John Wu
18
@JohnW: +1 solo por tu comentario de YAGNI. No puedo creer lo mucho que tengo que explicarle a la gente que YAGNI no es una excusa para construir un sistema rígido e inflexible que no pueda reaccionar al cambio, irónicamente, todo lo contrario de lo que SRP y los directores abiertos / cerrados están buscando.
Greg Burghardt
36
@ JohnWu: No estoy de acuerdo, YAGNI nos dice exactamente que no construyamos cosas que no necesitamos hoy. La legibilidad y las pruebas, por ejemplo, es algo que un programa siempre necesita "hoy", por lo que YAGNI nunca es una excusa para no agregar estructura y puntos de inyección. Sin embargo, tan pronto como la "extensibilidad" agrega un costo significativo para el cual los beneficios no son obvios "hoy", YAGNI significa evitar este tipo de extensibilidad, ya que esto lleva a una ingeniería excesiva.
Doc Brown
99
@ JohnWu Cambiamos de SQL 2008 a 2012. Hubo un total de dos consultas que debieron modificarse. ¿Y de autenticación de SQL a confiable? ¿Por qué eso incluso sería un cambio de código? cambiar el connectionString en el archivo de configuración es suficiente. De nuevo, YAGNI. A veces, YAGNI y SRP son inquietudes competitivas, y usted necesita juzgar cuál tiene el mejor costo / beneficio.
Andy
76

Hablando en términos prácticos, las responsabilidades están limitadas por aquellas cosas que probablemente cambien. Por lo tanto, desafortunadamente no existe una forma científica o formulada de llegar a lo que constituye una responsabilidad. Es una decisión judicial.

Se trata de lo que, en su experiencia , es probable que cambie.

Tendemos a aplicar el lenguaje del principio en una ira hiperbólica, literal y celosa. Tendemos a dividir las clases porque podrían cambiar, o en líneas que simplemente nos ayudan a resolver los problemas. (La última razón no es inherentemente mala). Pero, el SRP no existe por sí mismo; está en servicio para crear software mantenible.

Entonces, de nuevo, si las divisiones no son impulsadas por cambios probables , no están realmente al servicio del SRP 1 si YAGNI es más aplicable. Ambos sirven el mismo objetivo final. Y ambos son asuntos de juicio, con suerte un juicio experimentado .

Cuando el tío Bob escribe sobre esto, sugiere que pensemos en la "responsabilidad" en términos de "quién está pidiendo el cambio". En otras palabras, no queremos que la Parte A pierda sus empleos porque la Parte B pidió un cambio.

Cuando escribe un módulo de software, desea asegurarse de que cuando se soliciten cambios, esos cambios solo puedan originarse en una sola persona, o más bien, en un solo grupo de personas estrechamente acopladas que represente una única función comercial definida de forma estrecha. Desea aislar sus módulos de las complejidades de la organización en su conjunto y diseñar sus sistemas de manera que cada módulo sea responsable (responda) de las necesidades de esa única función comercial. ( Tío Bob - El principio de responsabilidad única )

Los desarrolladores buenos y experimentados tendrán una idea de los posibles cambios. Y esa lista mental variará un poco según la industria y la organización.

Lo que constituye una responsabilidad en su aplicación particular, en su organización particular, es en última instancia una cuestión de juicio experimentado . Se trata de lo que es probable que cambie. Y, en cierto sentido, se trata de quién posee la lógica interna del módulo.


1. Para ser claros, eso no significa que sean malas divisiones. Podrían ser grandes divisiones que mejoran drásticamente la legibilidad del código. Simplemente significa que no son impulsados ​​por el SRP.

svidgen
fuente
11
La mejor respuesta, y en realidad cita los pensamientos del tío Bob. En cuanto a lo que es probable que cambie, todo el mundo hace un gran problema con las E / S, "¿y si cambiamos la base de datos?" o "¿y si cambiamos de XML a JSON?" Creo que esto suele ser equivocado. La verdadera pregunta debería ser "¿y si necesitamos cambiar este int a un flotante, agregar un campo y cambiar esta cadena a una lista de cadenas?"
user949300
2
Esto es hacer trampa. La responsabilidad individual en sí misma es solo una forma propuesta de "cambio de aislamiento". Explicar que necesita aislar los cambios para mantener la responsabilidad "individual", no sugiere cómo hacerlo, solo explica el origen del requisito.
Basilevs
66
@Basilevs Estoy tratando de pulir la deficiencia que estás viendo en esta respuesta, ¡sin mencionar la respuesta del tío Bob! Pero, tal vez necesito aclarar que SRP no se trata de garantizar que "un cambio" afecte solo a 1 clase. Se trata de garantizar que cada clase responda a "un solo cambio". ... Se trata de tratar de dibujar las flechas de cada clase a un solo propietario. No de cada propietario a una sola clase.
svidgen
2
¡Gracias por brindar una respuesta pragmática! Incluso el tío Bob advierte contra la adhesión entusiasta a los principios SÓLIDOS en la arquitectura ágil . No tengo la cita a mano, pero básicamente dice que dividir las responsabilidades aumenta inherentemente el nivel de abstracción en su código y que toda abstracción tiene un costo, así que asegúrese de que el beneficio de seguir SRP (u otros principios) supere el costo de agregar más abstracción. (continuación del siguiente comentario)
Michael L.
44
Es por eso que debemos poner el producto frente al cliente tan pronto y tan a menudo como sea razonable, para que forcen cambios en nuestro diseño y podamos ver qué áreas es probable que cambien en ese producto. Además, advierte que no podemos protegernos de todo tipo de cambio. Para cualquier aplicación, será difícil realizar ciertos tipos de cambios. Necesitamos asegurarnos de que esos son los cambios que tienen menos probabilidades de suceder.
Michael L.
29

Sigo "las clases deberían tener una sola razón para cambiar".

Para mí, esto significa pensar en esquemas descabellados que el propietario de mi producto podría idear ("¡Necesitamos soporte móvil!", "¡Necesitamos ir a la nube!", "¡Necesitamos soporte chino!"). Los buenos diseños limitarán el impacto de estos esquemas a áreas más pequeñas y los harán relativamente fáciles de lograr. Los malos diseños significan ir a una gran cantidad de código y hacer un montón de cambios arriesgados.

La experiencia es lo único que he encontrado para evaluar adecuadamente la probabilidad de esos esquemas locos, porque hacer uno fácil podría hacer que otros dos sean más difíciles, y evaluar la bondad de un diseño. Los programadores experimentados pueden imaginar lo que tendrían que hacer para cambiar el código, lo que hay para morderlos en el culo y qué trucos facilitan las cosas. Los programadores experimentados tienen una buena idea de cuán jodidos están cuando el propietario del producto pide cosas locas.

Prácticamente, encuentro que las pruebas unitarias ayudan aquí. Si su código es inflexible, será difícil de probar. Si no puede inyectar simulaciones u otros datos de prueba, probablemente no podrá inyectar ese SupportChinesecódigo.

Otra métrica aproximada es la altura del ascensor. Los argumentos tradicionales de los ascensores son "si estuvieras en un ascensor con un inversor, ¿puedes venderle una idea?". Las startups deben tener descripciones breves y simples de lo que están haciendo: cuál es su enfoque. Del mismo modo, las clases (y funciones) deben tener una descripción simple de lo que hacen . No "esta clase implementa un fubar tal que puede usarlo en estos escenarios específicos". Algo que puede decirle a otro desarrollador: "Esta clase crea usuarios". Si no se puede comunicar que a otros desarrolladores, que está pasando para conseguir insectos.

Telastyn
fuente
A veces, vas a implementar lo que creías que sería un cambio desordenado, y resulta simple, o un pequeño refactor lo hace simple y agrega funcionalidades útiles al mismo tiempo. Pero sí, por lo general, puedes ver problemas llegando.
16
Soy un gran defensor de la idea del "discurso del ascensor". Si es difícil explicar lo que hace una clase en una o dos oraciones, estás en territorio arriesgado.
Ivan
1
Tocas un punto importante: la probabilidad de esos esquemas locos varía dramáticamente de un propietario de proyecto a otro. Debe confiar no solo en su experiencia general, sino en lo bien que conoce al propietario del proyecto. He trabajado para personas que querían reducir nuestros sprints a una semana, y todavía no podían evitar cambiar de dirección a mitad del sprint.
Kevin Krumwiede
1
Además de los beneficios obvios, documentar su código usando "tonos de ascensor" también sirve para ayudarlo a pensar en lo que está haciendo su código usando un lenguaje natural que encuentro útil para descubrir múltiples responsabilidades.
Alexander
1
@KevinKrumwiede ¡Para eso están las metodologías "Pollo corriendo con la cabeza cortada" y "Wild Goose Chase"!
26

Nadie sabe. O al menos, no podemos acordar una definición. Eso es lo que hace que SPR (y otros principios SÓLIDOS) sean bastante controvertidos.

Yo diría que ser capaz de descubrir qué es o no una responsabilidad es una de las habilidades que el desarrollador de software tiene que aprender a lo largo de su carrera. Cuanto más código escriba y revise, más experiencia tendrá para determinar si algo es responsabilidad única o múltiple. O si la responsabilidad individual se divide en partes separadas del código.

Yo diría que el propósito principal de SRP no es ser una regla difícil. Es para recordarnos ser conscientes de la cohesión en el código y siempre poner un esfuerzo consciente para determinar qué código es coherente y qué no.

Eufórico
fuente
20
Los nuevos programadores tienden a tratar a SOLID como si fuera un conjunto de leyes, que no lo es. Es simplemente una agrupación de buenas ideas para ayudar a las personas a mejorar en el diseño de la clase. Por desgracia, las personas tienden a tomar estos principios demasiado en serio; Recientemente vi una publicación de empleo que citaba SOLID como uno de los requisitos de trabajo.
Robert Harvey
99
+42 para el último párrafo. Como dice @RobertHarvey, cosas como SPR, SOLID y YAGNI no deben tomarse como " reglas absolutas ", sino como principios generales de "buenos consejos". Entre ellos (y otros), el consejo a veces será contradictorio, pero equilibrar ese consejo (en lugar de seguir un conjunto rígido de reglas) lo guiará (con el tiempo, a medida que su experiencia crece) para producir un mejor software. Debe haber una sola "regla absoluta" en el desarrollo de software: " No hay reglas absolutas ".
TripeHound
Esta es una muy buena aclaración sobre un aspecto de SRP. Pero, incluso si los principios SÓLIDOS no son reglas estrictas, no son terriblemente valiosos si nadie comprende lo que significan, ¡menos aún si su afirmación de que "nadie sabe" es realmente cierto! ... tiene sentido que sean difíciles de entender. Como con cualquier habilidad, ¡hay algo que distingue lo bueno de lo menos bueno! Pero ... "nadie lo sabe" lo convierte en un ritual de novatadas. (¡Y no creo que esa sea la intención de SOLID!)
svidgen
3
Por "Nadie lo sabe", espero que @Euphoric simplemente signifique que no hay una definición precisa que funcione para cada caso de uso. Es algo que requiere un cierto grado de juicio. Creo que una de las mejores formas de determinar dónde se encuentran sus responsabilidades es iterar rápidamente y dejar que su base de código le diga . Busque "olores" de que su código no es fácil de mantener. Por ejemplo, cuando un cambio en una sola regla empresarial comienza a tener efectos en cascada a través de clases aparentemente no relacionadas, es probable que tenga una violación de SRP.
Michael L.
1
Estoy totalmente de acuerdo con @TripeHound y otros que han señalado que todas estas "reglas" no existen para definir la Una Religión Verdadera del desarrollo, sino para aumentar la probabilidad de desarrollar software mantenible. Tenga mucho cuidado de seguir una "mejor práctica" si no puede explicar cómo promueve el software mantenible, mejora la calidad o aumenta la eficiencia del desarrollo ..
Michael L.
5

Creo que el término "responsabilidad" es útil como metáfora porque nos permite usar el software para investigar qué tan bien está organizado. En particular, me enfocaría en dos principios:

  • La responsabilidad es proporcional a la autoridad.
  • No hay dos entidades responsables de lo mismo.

Estos dos principios nos permiten repartir la responsabilidad de manera significativa porque se juegan entre sí. Si está habilitando un código para que haga algo por usted, debe ser responsable de lo que hace. Esto provoca la responsabilidad de que una clase tenga que crecer, ampliando su "una razón para cambiar" a ámbitos cada vez más amplios. Sin embargo, a medida que amplía las cosas, naturalmente comienza a encontrarse con situaciones en las que varias entidades son responsables de lo mismo. Esto está plagado de problemas en la responsabilidad de la vida real, por lo que seguramente también es un problema en la codificación. Como resultado, este principio hace que los ámbitos se reduzcan, ya que subdivide la responsabilidad en parcelas no duplicadas.

Además de estos dos, un tercer principio parece razonable:

  • La responsabilidad puede ser delegada

Considere un programa recién creado ... una pizarra en blanco. Al principio, solo tiene una entidad, que es el programa en su conjunto. Es responsable de ... todo. Naturalmente, en algún momento comenzará a delegar la responsabilidad en funciones o clases. En este punto, las dos primeras reglas entran en juego obligándote a equilibrar esa responsabilidad. El programa de nivel superior sigue siendo responsable de la producción general, al igual que un gerente es responsable de la productividad de su equipo, pero a cada sub-entidad se le ha delegado la responsabilidad, y con ella la autoridad para llevar a cabo esa responsabilidad.

Como una ventaja adicional, esto hace que SOLID sea particularmente compatible con cualquier desarrollo de software corporativo que uno deba hacer. Todas las empresas del planeta tienen algún concepto sobre cómo delegar responsabilidades, y no todas están de acuerdo. Si delega la responsabilidad dentro de su software de una manera que recuerda a la propia delegación de su empresa, será mucho más fácil para los futuros desarrolladores acelerar la forma en que hace las cosas en esta empresa.

Cort Ammon
fuente
No estoy 100% seguro de que esto lo explique completamente. Pero, ¡creo que explicar la "responsabilidad" con respecto a la "autoridad" es una forma perspicaz de expresarlo! (+1)
svidgen
Pirsig dijo: "Tiendes a construir tus problemas en la máquina", lo que me detiene.
@nocomprende También tiendes a desarrollar tus fortalezas en la máquina. Yo diría que cuando tus fortalezas y tus debilidades son las mismas cosas, es cuando se pone interesante.
Cort Ammon
5

En esta conferencia en Yale, el tío Bob da este divertido ejemplo:

Ingrese la descripción de la imagen aquí

Él dice que Employeetiene tres razones para cambiar, tres fuentes de requisitos de cambio, y da esta explicación humorística e irónica , pero no obstante ilustrativa:

  • Si el CalcPay()método tiene un error y le cuesta a la compañía millones de dólares, el CFO lo despedirá .

  • Si el ReportHours()método tiene un error y le cuesta a la compañía millones de dólares, el COO lo despedirá .

  • Si el WriteEmmployee(método tiene un error que causa la eliminación de muchos datos y le cuesta a la compañía millones de dólares, el CTO lo despedirá .

Por lo tanto, tener tres ejecutivos de nivel C diferentes que posiblemente lo despidan por errores costosos en la misma clase significa que la clase tiene demasiadas responsabilidades.

Da esta solución que resuelve la violación de SRP, pero aún tiene que resolver la violación de DIP que no se muestra en el video.

Ingrese la descripción de la imagen aquí

Tulains Córdova
fuente
Este ejemplo se parece más a una clase que tiene las responsabilidades equivocadas .
Robert Harvey
44
@RobertHarvey Cuando una clase tiene demasiadas responsabilidades, significa que las responsabilidades adicionales son las responsabilidades incorrectas .
Tulains Córdova
55
Escucho lo que dices, pero no me parece convincente. Hay una diferencia entre una clase que tiene demasiadas responsabilidades y una clase que hace algo que no tiene nada que ver en absoluto. Puede sonar igual, pero no lo es; contar cacahuetes no es lo mismo que llamarlos nueces. Es el principio del tío Bob y el ejemplo del tío Bob, pero si fuera suficientemente descriptivo, no necesitaríamos esta pregunta en absoluto.
Robert Harvey
@RobertHarvey, ¿cuál es la diferencia? Esas situaciones me parecen isomorfas.
Paul Draper
3

Creo que una mejor manera de subdividir las cosas que "razones para cambiar" es comenzar por pensar en términos de si tendría sentido exigir que el código que debe realizar dos (o más) acciones deba tener una referencia de objeto separada para cada acción, y si sería útil tener un objeto público que pudiera hacer una acción pero no la otra.

Si las respuestas a ambas preguntas son afirmativas, eso sugeriría que las acciones deberían realizarse por clases separadas. Si las respuestas a ambas preguntas son no, eso sugeriría que desde un punto de vista público debería haber una clase; si el código para eso fuera difícil de manejar, puede subdividirse internamente en clases privadas. Si la respuesta a la primera pregunta es no, pero la segunda es sí, debe haber una clase separada para cada acción más una clase compuesta que incluya referencias a instancias de las otras.

Si uno tiene clases separadas para el teclado, la señal acústica, la lectura numérica, la impresora de recibos y el cajón de efectivo de una caja registradora, y no tiene una clase compuesta para una caja registradora completa, entonces el código que se supone que procesa una transacción podría terminar invocando accidentalmente en un de manera que recibe la entrada del teclado de una máquina, produce ruido desde el pitido de una segunda máquina, muestra números en la pantalla de una tercera máquina, imprime un recibo en la impresora de una cuarta máquina y abre el cajón de efectivo de una quinta máquina. Cada una de esas subfunciones podría ser manejada útilmente por una clase separada, pero también debería haber una clase compuesta que las una. La clase compuesta debería delegar tanta lógica a las clases constituyentes como sea posible,

Se podría decir que la "responsabilidad" de cada clase es incorporar alguna lógica real o proporcionar un punto de conexión común para muchas otras clases que lo hacen, pero lo importante es centrarse en primer lugar en cómo el código del cliente debe ver una clase. Si tiene sentido que el código del cliente vea algo como un solo objeto, entonces el código del cliente debería verlo como un solo objeto.

Super gato
fuente
Este es un buen consejo. Vale la pena señalar que divide las responsabilidades de acuerdo con más criterios que solo el SRP.
Jørgen Fogh
1
Analogía del automóvil: no necesito saber cuánto gas hay en el tanque de otra persona, ni quiero encender los limpiaparabrisas de otra persona. (pero esa es la definición de internet) (¡Shh! arruinarás la historia)
1
@nocomprende - "No necesito saber cuánta gasolina hay en el tanque de otra persona", a menos que seas un adolescente tratando de decidir cuál de los autos de la familia "
tomarás
3

SRP es difícil de acertar. Se trata principalmente de asignar 'trabajos' a su código y asegurarse de que cada parte tenga responsabilidades claras. Al igual que en la vida real, en algunos casos dividir el trabajo entre las personas puede ser bastante natural, pero en otros casos puede ser realmente complicado, especialmente si no los conoce (o el trabajo).

Siempre recomiendo que solo escriba un código simple que funcione primero , luego refactorice un poco: tenderá a ver cómo el código se agrupa naturalmente después de un tiempo. Creo que es un error forzar responsabilidades antes de conocer el código (o las personas) y el trabajo a realizar.

Una cosa que notará es cuando el módulo comienza a hacer demasiado y es difícil de depurar / mantener. Este es el momento de refactorizar; ¿Cuál debería ser el trabajo principal y qué tareas podrían asignarse a otro módulo? Por ejemplo, ¿debería manejar los controles de seguridad y el otro trabajo, o debería hacer controles de seguridad en otro lugar primero, o esto hará que el código sea más complejo?

Usa demasiadas indirecciones y volverá a ser un desastre ... en cuanto a otros principios, este estará en conflicto con otros, como KISS, YAGNI, etc. Todo es cuestión de equilibrio.

Christophe Roussy
fuente
¿No es SRP solo la cohesión de Constantine?
Nick Keighley
Naturalmente, encontrará esos patrones si codifica el tiempo suficiente, pero puede acelerar el aprendizaje nombrándolos y ayuda con la comunicación ...
Christophe Roussy
@NickKeighley Creo que es cohesión, no tanto escrito, pero visto desde otra perspectiva.
sdenham
3

"Principio de responsabilidad única" es quizás un nombre confuso. "Solo una razón para cambiar" es una mejor descripción del principio, pero aún es fácil de entender mal. No estamos hablando de decir qué causa que los objetos cambien de estado en tiempo de ejecución. Estamos analizando qué podría hacer que los desarrolladores tengan que cambiar el código en el futuro.

A menos que estemos solucionando un error, el cambio se deberá a un requisito comercial nuevo o modificado. Tendrá que pensar fuera del código e imaginar qué factores externos pueden hacer que los requisitos cambien de forma independiente . Decir:

  • Las tasas impositivas cambian debido a una decisión política.
  • El departamento de marketing decide cambiar los nombres de todos los productos.
  • La interfaz de usuario debe ser rediseñada para ser accesible
  • La base de datos está congestionada, por lo que debe hacer algunas optimizaciones.
  • Tienes que acomodar una aplicación móvil
  • y así...

Idealmente, desea que factores independientes afecten a las diferentes clases. Por ejemplo, dado que las tasas impositivas cambian independientemente de los nombres de los productos, los cambios no deberían afectar a las mismas clases. De lo contrario, corre el riesgo de que se produzca un cambio de impuestos, un error en la denominación del producto, que es el tipo de acoplamiento estrecho que desea evitar con un sistema modular.

Por lo tanto, no solo se centre en lo que podría cambiar: cualquier cosa podría cambiar en el futuro. Concéntrese en lo que podría cambiar independientemente . Los cambios suelen ser independientes si son causados ​​por diferentes actores.

Su ejemplo con los títulos de trabajo está en el camino correcto, ¡pero debería tomarlo más literalmente! Si el marketing puede causar cambios en el código y las finanzas pueden causar otros cambios, estos cambios no deberían afectar el mismo código, ya que estos son títulos de trabajo literalmente diferentes y, por lo tanto, los cambios sucederán de forma independiente.

Para citar al tío Bob que inventó el término:

Cuando escribe un módulo de software, desea asegurarse de que cuando se soliciten cambios, esos cambios solo puedan originarse en una sola persona, o más bien, en un solo grupo de personas estrechamente acopladas que represente una única función comercial definida de forma estrecha. Desea aislar sus módulos de las complejidades de la organización en su conjunto y diseñar sus sistemas de manera que cada módulo sea responsable (responda) de las necesidades de esa única función comercial.

En resumen: una "responsabilidad" es atender a una única función empresarial. Si más de un actor puede hacer que tengas que cambiar una clase, entonces la clase probablemente rompa este principio.

JacquesB
fuente
Según su libro "Arquitectura limpia", esto es exactamente correcto. Las reglas de negocio deben provenir de una sola fuente, y solo una vez. Esto significa que RR.HH., operaciones y TI deben cooperar en la formulación de requisitos en una "responsabilidad única". Y ese es el principio. +1
Benny Skogberg
2

Un buen artículo que explica los principios de programación SOLID y da ejemplos de código que sigue y no sigue estos principios es https://scotch.io/bar-talk/solid-the-first-five-principles-of-object-oriented- de diseño .

En el ejemplo relacionado con SRP, da un ejemplo de algunas clases de formas (círculo y cuadrado) y una clase diseñada para calcular el área total de múltiples formas.

En su primer ejemplo, crea la clase de cálculo de área y hace que devuelva su salida como HTML. Más tarde, decide que quiere mostrarlo como JSON y tiene que cambiar su clase de cálculo de área.

El problema con este ejemplo es que su clase de cálculo de área es responsable de calcular el área de formas Y mostrar esa área. Luego pasa por una mejor manera de hacer esto usando otra clase diseñada específicamente para mostrar áreas.

Este es un ejemplo simple (y se entiende más fácilmente al leer el artículo ya que tiene fragmentos de código) pero demuestra la idea central de SRP.

blitz1616
fuente
0

En primer lugar, lo que tiene son en realidad dos problemas separados : el problema de qué métodos poner en sus clases y el problema de la hinchazón de la interfaz.

Interfaces

Tienes esta interfaz:

public Interface CustomerCRUD
{
  public void Create(Customer customer);
  public Customer Read(int CustomerID);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Presumiblemente, tiene varias clases que se ajustan a la CustomerCRUDinterfaz (de lo contrario, una interfaz no es necesaria), y alguna función do_crud(customer: CustomerCRUD)que tiene un objeto conforme. Pero ya ha roto el SRP: ha vinculado estas cuatro operaciones distintas.

Digamos que más adelante operarías en vistas de bases de datos. Una vista de base de datos solo tiene el Readmétodo disponible. Pero desea escribir una función do_query_stuff(customer: ???)que opere de forma transparente en tablas o vistas completas; solo usa el Readmétodo, después de todo.

Entonces crea una interfaz

Public Interface CustomerReader {public Customer Read (customerID: int)}

y factoriza tu CustomerCrudinterfaz como:

public interface CustomerCRUD extends CustomerReader
{
  public void Create(Customer customer);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Pero no hay un final a la vista. Podría haber objetos que podemos crear pero no actualizar, etc. Este agujero de conejo es demasiado profundo. La única forma sensata de adherirse al principio de responsabilidad única es hacer que todas sus interfaces contengan exactamente un método . Go en realidad sigue esta metodología de lo que he visto, con la gran mayoría de las interfaces que contienen una sola función; Si desea especificar una interfaz que contenga dos funciones, debe crear torpemente una nueva interfaz que combine las dos. Pronto obtienes una explosión combinatoria de interfaces.

La solución a este problema es utilizar subtipos estructurales (implementados, por ejemplo, en OCaml) en lugar de interfaces (que son una forma de subtipo nominal). No definimos interfaces; en cambio, simplemente podemos escribir una función

let do_customer_stuff customer = customer.read ... customer.update ...

eso llama cualquier método que nos guste. OCaml usará la inferencia de tipos para determinar que podemos pasar cualquier objeto que implemente estos métodos. En este ejemplo, se determinaría que customertiene tipo <read: int -> unit, update: int -> unit, ...>.

Clases

Esto resuelve el desorden de la interfaz ; pero aún tenemos que implementar clases que contengan múltiples métodos. Por ejemplo, ¿deberíamos crear dos clases diferentes CustomerReadery CustomerWriter? ¿Qué sucede si queremos cambiar la forma en que se leen las tablas (por ejemplo, ahora almacenamos en caché nuestras respuestas en redis antes después de recuperar los datos), pero ahora cómo se escriben? Si sigues esta cadena de razonamiento hasta su conclusión lógica, eres inextricablemente conducido a la programación funcional :)

cabeza de jardín
fuente
44
"Sin sentido" es un poco fuerte. Podría ponerme detrás de "místico" o "zen". Pero, no sin sentido!
svidgen
¿Puedes explicar un poco más por qué el subtipo estructural es una solución?
Robert Harvey
@RobertHarvey reestructuró mi respuesta significativamente
gardenhead
44
Utilizo interfaces incluso cuando solo tengo una clase que lo implementa. ¿Por qué? Burlarse de las pruebas unitarias.
Eternal21
0

En mi opinión, lo más parecido a un SRP que se me ocurre es un flujo de uso. Si no tiene un flujo de uso claro para una clase determinada, es probable que su clase tenga un olor a diseño.

Un flujo de uso sería una sucesión de llamadas a un método dado que le daría un resultado esperado (por lo tanto, comprobable). Básicamente, define una clase con los casos de uso que obtuvo en mi humilde opinión, es por eso que toda la metodología del programa se centra en las interfaces sobre la implementación.

Arthur Havlicek
fuente
0

Es para lograr que los requisitos múltiples cambien, no requiere que su componente cambie .

Pero buena suerte entendiendo eso a primera vista, cuando escuchas por primera vez sobre SOLID.


Veo muchos comentarios que dicen que SRP y YAGNI pueden contradecirse entre sí, pero YAGN que hice cumplir por TDD (GOOS, London School) me enseñó a pensar y diseñar mis componentes desde la perspectiva del cliente. Comencé a diseñar mis interfaces por lo menos que un cliente querría que hiciera, eso es lo poco que debería hacer . Y ese ejercicio se puede hacer sin ningún conocimiento de TDD.

Me gusta la técnica descrita por el tío Bob (no puedo recordar de dónde, lamentablemente), que es algo así como:

Pregúntese, ¿qué hace esta clase?

¿Su respuesta contenía And u Or

Si es así, extraiga esa parte de la respuesta, es una responsabilidad propia

Esta técnica es absoluta, y como dijo @svidgen, SRP es una decisión, pero cuando se aprende algo nuevo, lo absoluto es lo mejor, es más fácil hacer siempre algo. Asegúrese de que la razón por la que no se separa es; una estimación educada, y no porque no sepas cómo hacerlo. Este es el arte, y se necesita experiencia.


Creo que muchas de las respuestas parecen ser un argumento para desacoplar cuando se habla de SRP .

SRP no es para asegurarse de que un cambio no se propague por el gráfico de dependencia.

Teóricamente, sin SRP , no tendrías ninguna dependencia ...

Un cambio no debería causar un cambio en muchos lugares de la aplicación, pero tenemos otros principios para eso. Sin embargo, SRP mejora el Principio Abierto Cerrado . Este principio tiene más que ver con la abstracción, sin embargo, las abstracciones más pequeñas son más fáciles de reimplementar .

Entonces, cuando enseñe SOLID en su conjunto, tenga cuidado de enseñar que SRP le permite cambiar menos código cuando cambian los requisitos, cuando de hecho, le permite escribir menos código nuevo .

Chris Wohlert
fuente
3
When learning something new, absolutes are the best, it is easier to just always do something.- En mi experiencia, los nuevos programadores son demasiado dogmáticos. El absolutismo conduce a desarrolladores no pensantes y a la programación de culto a la carga. Decir "solo haz esto" está bien, siempre y cuando entiendas que la persona con la que estás hablando tendrá que desaprender más tarde lo que le has enseñado.
Robert Harvey
@RobertHarvey, completamente cierto, crea un comportamiento dogmático, y debes desaprender / volver a aprender a medida que adquieres experiencia. Sin embargo, este es mi punto. Si un nuevo programador intenta hacer juicios sin ninguna forma de razonar su decisión, parece inútil, porque no saben por qué funcionó, cuándo funcionó. Al hacer que las personas se excedan , les enseña a buscar las excepciones en lugar de hacer conjeturas sin reservas. Todo lo que dijiste sobre el absolutismo es correcto, por lo que solo debería ser un punto de partida.
Chris Wohlert
@RobertHarvey, un ejemplo rápido de la vida real : puede enseñar a sus hijos a ser siempre honestos, pero a medida que crecen, probablemente se darán cuenta de algunas excepciones en las que las personas no quieren escuchar sus pensamientos más honestos. Esperar que un niño de 5 años tome una decisión correcta sobre ser honesto es optimista en el mejor de los casos. :)
Chris Wohlert
0

No hay una respuesta clara a eso. Aunque la pregunta es estrecha, las explicaciones no lo son.

Para mí, es algo como la Navaja de Occam si quieres. Es un ideal donde trato de medir mi código actual. Es difícil precisarlo en palabras simples y simples. Otra metáfora sería »un tema« que es tan abstracto, es decir, difícil de entender, como »responsabilidad única«. Una tercera descripción sería "tratar con un nivel de abstracción".

¿Qué significa eso prácticamente?

Últimamente uso un estilo de codificación que consta principalmente de dos fases:

La fase I se describe mejor como caos creativo. En esta fase, escribo el código a medida que fluyen los pensamientos, es decir, crudo y feo.

La fase II es todo lo contrario. Es como limpiar después de un huracán. Esto requiere más trabajo y disciplina. Y luego miro el código desde la perspectiva de un diseñador.

Ahora estoy trabajando principalmente en Python, lo que me permite pensar en objetos y clases más adelante. Primera fase I : escribo solo funciones y las distribuyo casi al azar en diferentes módulos. En la Fase II , después de poner en marcha las cosas, miro más de cerca qué módulo trata con qué parte de la solución. Y mientras hojeo los módulos, los temas son emergentes para mí. Algunas funciones están relacionadas temáticamente. Estos son buenos candidatos para las clases . Y después de convertir las funciones en clases, lo que casi se hace con sangría y agregar selfa la lista de parámetros en python;), uso SRPcomo la Navaja de Occam para quitar la funcionalidad a otros módulos y clases.

Un ejemplo actual puede estar escribiendo una pequeña funcionalidad de exportación el otro día.

Existía la necesidad de csv , excel y hojas de excel combinadas en un zip.

La funcionalidad simple se realizó en tres vistas (= funciones). Cada función utilizó un método común para determinar los filtros y un segundo método para recuperar los datos. Luego, en cada función, se realizó la preparación de la exportación y se entregó como una Respuesta del servidor.

Había demasiados niveles de abstracción mezclados:

I) tratar con solicitudes / respuestas entrantes / salientes

II) determinación de filtros

III) recuperar datos

IV) transformación de datos

El paso fácil fue usar una abstracción ( exporter) para tratar las capas II-IV en un primer paso.

Lo único que quedaba era el tema relacionado con las solicitudes / respuestas . En el mismo nivel de abstracción se extraen los parámetros de solicitud, lo cual está bien. Entonces, para este punto de vista, tenía una "responsabilidad".

En segundo lugar, tuve que romper el exportador, que, como vimos, consistía en al menos otras tres capas de abstracción.

La determinación de los criterios de filtro y la recuperación real están casi en el mismo nivel de abstracción (los filtros son necesarios para obtener el subconjunto correcto de los datos). Estos niveles se pusieron en algo así como una capa de acceso a datos .

En el siguiente paso, separé los mecanismos de exportación reales: cuando se necesitaba escribir en un archivo temporal, lo dividí en dos "responsabilidades": una para la escritura real de los datos en el disco y otra parte que trataba con el formato real.

A lo largo de la formación de las clases y los módulos, las cosas se aclararon, lo que pertenecía a dónde. Y siempre la pregunta latente, si la clase hace demasiado .

¿Cómo determina qué responsabilidades debe tener cada clase y cómo define una responsabilidad en el contexto de SRP?

Es difícil dar una receta a seguir. Por supuesto, podría repetir la críptica regla de un nivel de abstracción, si eso ayuda.

Principalmente para mí es una especie de "intuición artística" que conduce al diseño actual; Modelo código como un artista puede esculpir arcilla o pintar.

Imaginame como un Coding Bob Ross ;)

Thomas Junk
fuente
0

Lo que intento hacer para escribir el código que sigue al SRP:

  • Elija un problema específico que necesite resolver;
  • Escribe el código que lo resuelve, escribe todo en un método (por ejemplo: main);
  • Analice cuidadosamente el código y, en función del negocio, intente definir las responsabilidades que son visibles en todas las operaciones que se realizan (esta es la parte subjetiva que también depende del negocio / proyecto / cliente);
  • Tenga en cuenta que toda la funcionalidad ya está implementada; lo que sigue es solo la organización del código (de ahora en adelante no se implementará ninguna característica o mecanismo adicional en este enfoque);
  • En función de las responsabilidades que definió en los pasos anteriores (que se definen en función del negocio y la idea de "una razón para cambiar"), extraiga una clase o método por separado para cada uno;
  • Tenga en cuenta que este enfoque solo se preocupa por el SPR; idealmente debería haber pasos adicionales aquí tratando de adherirse a los otros principios también.

Ejemplo:

Problema: obtenga dos números del usuario, calcule su suma y envíe el resultado al usuario:

//first step: solve the problem right away
static void Main(string[] args)
{
    Console.WriteLine("Number 1: ");
    int firstNumber = Convert.ToInt32(Console.ReadLine());

    Console.WriteLine("Number 2: ");
    int secondNumber = Convert.ToInt32(Console.ReadLine());

    int result = firstNumber + secondNumber;

    Console.WriteLine("Hi there! The result is: {0}", result);

    Console.ReadLine();
}

A continuación, intente definir las responsabilidades en función de las tareas que deben realizarse. De esto, extraiga las clases apropiadas:

//Responsible for getting two integers from the user
class Input {
    public int FirstNumber { get; set; }
    public int SecondNumber { get; set; }
    public void Read() {
        Console.WriteLine("Number 1: ");
        FirstNumber = Convert.ToInt32(Console.ReadLine());

        Console.WriteLine("Number 2: ");
        SecondNumber = Convert.ToInt32(Console.ReadLine());
    }
}

//Responsible for calculating the sum of two integers
class SumOperation {
    public int Result { get; set; }
    public void Calculate(int a, int b) {
        Result = a + b;
    }
}

//Responsible for the output of some value to the user
class Output {
    public void Write(int result) {
        Console.WriteLine("Hello! The result is: {0}", result);
    }
}

Luego, el programa refactorizado se convierte en:

//Program: responsible for main execution.
//Gets two numbers from user and output their sum.
static void Main(string[] args)
{
    var input = new Input();
    input.Read();

    var operation = new SumOperation();
    operation.Calculate(input.FirstNumber, input.SecondNumber);

    var output = new Output();
    output.Write(operation.Result);

    Console.ReadLine();
}

Nota: este ejemplo muy simple toma en consideración solo el principio SRP. El uso de los otros principios (por ejemplo: el código "L" debe depender de abstracciones en lugar de concreciones) proporcionaría más beneficios al código y lo haría más sostenible para los cambios comerciales.

Emerson Cardoso
fuente
1
Su ejemplo es demasiado simple para ilustrar adecuadamente SRP. Nadie haría esto en la vida real.
Robert Harvey
Sí, en proyectos reales escribo un pseudocódigo en lugar de escribir el código exacto como en mi ejemplo. Después del pseudocódigo, trato de dividir las responsabilidades tal como lo hice en el ejemplo. De todos modos, así es como lo hago.
Emerson Cardoso
0

Del libro Robert C. Martins Clean Architecture: A Craftsman's Guide to Software Structure and Design , publicado el 10 de septiembre de 2017, Robert escribe en la página 62 lo siguiente:

Históricamente, el SRP se ha descrito de esta manera:

Un módulo debe tener una, y solo una, razón para cambiar

Los sistemas de software se cambian para satisfacer a los usuarios y partes interesadas; esos usuarios y partes interesadas son la "razón para cambiar". de lo que habla el principio. De hecho, podemos reformular el principio para decir esto:

Un módulo debe ser responsable ante uno, y solo uno, usuario o parte interesada

Desafortunadamente, la palabra "usuario" y "parte interesada" no son realmente la palabra correcta para usar aquí. Es probable que haya más de un usuario o parte interesada que quiera cambiar el sistema de la manera más sensata. En cambio, nos estamos refiriendo realmente a un grupo: una o más personas que requieren ese cambio. Nos referiremos a ese grupo como actor .

Por lo tanto, la versión final del SRP es:

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

Entonces esto no se trata de código. El SRP se trata de controlar el flujo de requisitos y necesidades comerciales, que solo pueden provenir de una fuente.

Benny Skogberg
fuente
No estoy seguro de por qué hace la distinción de que "no se trata de código". Por supuesto que se trata de código; Esto es desarrollo de software.
Robert Harvey
@RobertHarvey Mi punto es que el flujo de requisitos proviene de una fuente, el actor. Los usuarios y las partes interesadas no están en el código, están en las reglas de negocios que nos llegan como requisitos. Entonces, el SRP es un proceso para controlar estos requisitos, que para mí no es código. Es desarrollo de software (!), Pero no código.
Benny Skogberg