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?
architecture
class-design
solid
single-responsibility
Robert Harvey
fuente
fuente
Respuestas:
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:
O:
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.IO
espacio de nombres: no podemos encontrar un varios tipos de flujos físicos (por ejemploFileStream
,MemoryStream
oNetworkStream
) 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 tenerFileStreamTextWriter
,FileStreamBinaryWriter
,NetworkStreamTextWriter
,NetworkStreamBinaryWriter
,MemoryStreamTextWriter
, yMemoryStreamBinaryWriter
, que simplemente conectar el escritor y la corriente y se puede tener lo que quiere. Luego, más tarde, podemos agregar, digamos, unXmlWriter
, sin necesidad de volver a implementarlo para la memoria, el archivo y la red por separado.fuente
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.
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.
fuente
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
SupportChinese
có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.
fuente
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.
fuente
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:
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:
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.
fuente
En esta conferencia en Yale, el tío Bob da este divertido ejemplo:
Él dice que
Employee
tiene tres razones para cambiar, tres fuentes de requisitos de cambio, y da esta explicación humorística e irónica , pero no obstante ilustrativa: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.
fuente
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.
fuente
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.
fuente
"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:
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:
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.
fuente
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.
fuente
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:
Presumiblemente, tiene varias clases que se ajustan a la
CustomerCRUD
interfaz (de lo contrario, una interfaz no es necesaria), y alguna funcióndo_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
Read
método disponible. Pero desea escribir una funcióndo_query_stuff(customer: ???)
que opere de forma transparente en tablas o vistas completas; solo usa elRead
método, después de todo.Entonces crea una interfaz
Public Interface CustomerReader {public Customer Read (customerID: int)}
y factoriza tu
CustomerCrud
interfaz como: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
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
customer
tiene 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
CustomerReader
yCustomerWriter
? ¿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 :)fuente
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.
fuente
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:
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 .
fuente
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.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
self
a la lista de parámetros en python;), usoSRP
como 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 .
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 ;)
fuente
Lo que intento hacer para escribir el código que sigue al SRP:
Ejemplo:
Problema: obtenga dos números del usuario, calcule su suma y envíe el resultado al usuario:
A continuación, intente definir las responsabilidades en función de las tareas que deben realizarse. De esto, extraiga las clases apropiadas:
Luego, el programa refactorizado se convierte en:
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.
fuente
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:
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.
fuente