¿Puede / debe aplicarse el Principio de Responsabilidad Única al nuevo código?


20

El principio se define como módulos que tienen una razón para cambiar . Mi pregunta es, ¿seguramente estas razones para cambiar no se conocen hasta que el código realmente comience a cambiar? Casi todas las piezas de código tienen numerosas razones por las cuales podría cambiar, pero seguramente intentar anticipar todo esto y diseñar su código con esto en mente terminaría con un código muy pobre. ¿No es una mejor idea comenzar realmente a aplicar SRP cuando las solicitudes para cambiar el código comienzan a llegar? Más específicamente, cuando un fragmento de código ha cambiado más de una vez por más de una razón, lo que demuestra que tiene más de una razón para cambiar. Suena muy anti-Agile intentar adivinar las razones del cambio.

Un ejemplo sería una pieza de código que imprime un documento. Se presenta una solicitud para cambiarlo para imprimir a PDF y luego se realiza una segunda solicitud para cambiarlo y aplicar un formato diferente al documento. En este punto, tiene pruebas de más de una razón para cambiar (y la violación de SRP) y debe realizar la refactorización adecuada.


66
@Frank - en realidad se define comúnmente de esa manera - ver, por ejemplo, en.wikipedia.org/wiki/Single_responILITY_principle
Joris Timmermans

1
La forma en que lo expresas no es la forma en que entiendo la definición de SRP.
Pieter B

2
Cada línea de código tiene (al menos) dos razones para cambiarla: contribuye a un error o interfiere con un nuevo requisito.
Bart van Ingen Schenau

1
@BartvanIngenSchenau: LOL ;-) Si lo ve de esta manera, el SRP no se puede aplicar en ninguna parte.
Doc Brown

1
@DocBrown: puede hacerlo si no combina SRP con el cambio del código fuente. Por ejemplo, si interpreta que SRP es capaz de dar una descripción completa de lo que hace una clase / función en una oración sin usar la palabra y (y sin palabras de comadreja para sortear esa restricción).
Bart van Ingen Schenau

Respuestas:


27

Por supuesto, el principio YAGNI le indicará que aplique SRP no antes de que realmente lo necesite. Pero la pregunta que debe hacerse es: ¿necesito aplicar SRP primero y solo cuando realmente tengo que cambiar mi código?

Según mi experiencia, la aplicación de SRP le brinda un beneficio mucho antes: cuando tiene que averiguar dónde y cómo aplicar un cambio específico en su código. Para esta tarea, debe leer y comprender sus funciones y clases existentes. Esto se vuelve mucho más fácil cuando todas sus funciones y clases tienen una responsabilidad específica. Entonces, en mi humilde opinión, debe aplicar SRP siempre que haga que su código sea más fácil de leer, cada vez que haga que sus funciones sean más pequeñas y más autodescriptivas. Entonces la respuesta es , tiene sentido aplicar SRP incluso para el nuevo código.

Por ejemplo, cuando su código de impresión lee un documento, formatea el documento e imprime el resultado en un dispositivo específico, estas son 3 responsabilidades separables claras. Por lo tanto, haga al menos 3 funciones de ellas, asígneles nombres. Por ejemplo:

 void RunPrintWorkflow()
 {
     var document = ReadDocument();
     var formattedDocument = FormatDocument(document);
     PrintDocumentToScreen(formattedDocument);
 }

Ahora, cuando tiene un nuevo requisito para cambiar el formato del documento u otro para imprimir en PDF, sabe exactamente en cuál de estas funciones o ubicaciones en el código debe aplicar los cambios, y aún más importante, dónde no.

Por lo tanto, cada vez que llega a una función que no comprende porque la función hace "demasiado", y no está seguro de si aplicar un cambio y dónde, entonces considere refactorizar la función en funciones separadas más pequeñas. No esperes hasta que tengas que cambiar algo. El código es 10 veces más leído que modificado, y las funciones más pequeñas son mucho más fáciles de leer. Según mi experiencia, cuando una función tiene una cierta complejidad, siempre se puede dividir la función en diferentes responsabilidades, independientemente de saber qué cambios vendrán en el futuro. Bob Martin típicamente va un paso más allá, mira el enlace que di en mis comentarios a continuación.

EDITAR: a su comentario: La principal responsabilidad de la función externa en el ejemplo anterior no es imprimir en un dispositivo específico, o formatear el documento, es integrar el flujo de trabajo de impresión . Por lo tanto, en el nivel de abstracción de la función externa, un nuevo requisito como "los documentos ya no deben formatearse" o "el documento debe enviarse por correo en lugar de imprimirse" es simplemente "la misma razón", es decir, "el flujo de trabajo de impresión ha cambiado". Si hablamos de cosas así, es importante mantener el nivel correcto de abstracción .


Por lo general, siempre desarrollo con TDD, por lo que en mi ejemplo no habría podido mantener físicamente toda esa lógica en un módulo porque sería imposible de probar. Esto es solo un subproducto de TDD y no porque estoy aplicando SRP deliberadamente. Mi ejemplo tenía responsabilidades bastante claras y separadas, así que quizás no sea un buen ejemplo. Creo que lo que estoy preguntando es, ¿puede escribir cualquier nueva pieza de código e inequívocamente decir, sí, esto no viole SRP? ¿No son las 'razones para cambiar' esencialmente definidas por el negocio?
SeeNoWeevil

3
@thecapsaicinkid: sí, puede (al menos mediante refactorización inmediata). Pero obtendrá funciones muy, muy pequeñas, y no a todos los programadores les gusta eso. Vea este ejemplo: sites.google.com/site/unclebobconsultingllc/…
Doc Brown

Si estaba aplicando SRP anticipando razones para cambiar, en su ejemplo aún podría argumentar que tiene más de un cambio de razón. La empresa podría decidir que ya no quería formatear un documento y luego decidir que quería que se enviara por correo electrónico en lugar de imprimirse. EDITAR: solo lea el enlace, y aunque no me gusta particularmente el resultado final, 'Extraer hasta que ya no pueda extraer más' tiene mucho más sentido y es menos ambiguo que 'solo una razón para cambiar'. Aunque no muy pragmático.
SeeNoWeevil

1
@thecapsaicinkid: mira mi edición. La principal responsabilidad de la función externa no es imprimir en un dispositivo específico, o formatear el documento, es integrar el flujo de trabajo de impresión. Y cuando este flujo de trabajo cambia, esa es la única razón por la cual cambiará la función
Doc Brown

Su comentario sobre mantener el nivel correcto de abstracción parece ser lo que me faltaba. Un ejemplo, tengo una clase que describiría como 'Crea estructuras de datos a partir de una matriz JSON'. Suena como una responsabilidad única para mí. Recorre los objetos en una matriz JSON y los asigna a POJO. Si me quedo con el mismo nivel de abstracción que mi descripción, es difícil argumentar que tiene más de una razón para cambiar, es decir, 'Cómo se asigna JSON al objeto'. Al ser menos abstracta que podría argumentar que tiene más de una razón, por ejemplo cómo asignar campos de fecha cambia, cómo los valores numéricos se asignan a día, etc.
SeeNoWeevil

7

Creo que estás malinterpretando SRP.

La única razón para el cambio NO se trata de cambiar el código sino de lo que hace su código.


3

Creo que la definición de SRP como "tener una razón para cambiar" es engañosa exactamente por esta razón. Tómelo exactamente al pie de la letra: el Principio de Responsabilidad Única dice que una clase o función debe tener exactamente una responsabilidad. Tener una sola razón para cambiar es un efecto secundario de solo hacer una cosa para comenzar. No hay razón para que al menos no pueda hacer un esfuerzo hacia la responsabilidad única en su código sin saber nada sobre cómo podría cambiar en el futuro.

Una de las mejores pistas para este tipo de cosas es cuando elige nombres de clase o función. Si no es evidente de inmediato cómo debe llamarse la clase, o si el nombre es particularmente largo / complejo, o si el nombre usa términos genéricos como "gerente" o "utilidad", entonces probablemente esté violando SRP. Del mismo modo, al documentar la API, debería ser rápidamente evidente si está violando SRP en función de la funcionalidad que está describiendo.

Hay, por supuesto, matices para SRP que no puede saber hasta más adelante en el proyecto: lo que parecía una responsabilidad única resultó ser dos o tres. Estos son casos en los que tendrá que refactorizar para implementar SRP. Pero eso no significa que se deba ignorar SRP hasta que reciba una solicitud de cambio; que derrota el propósito de SRP!

Para hablar directamente a su ejemplo, considere documentar su método de impresión. Si desea decir "este método da formato a los datos para la impresión y la envía a la impresora", que y es lo que se obtiene: eso no es una única responsabilidad, que es dos responsabilidades: formato y enviar a la impresora. Si reconoce esto y los divide en dos funciones / clases, cuando lleguen sus solicitudes de cambio, ya tendría una sola razón para que cada sección cambie.


3

Un ejemplo sería una pieza de código que imprime un documento. Se presenta una solicitud para cambiarlo para imprimir a PDF y luego se realiza una segunda solicitud para cambiarlo y aplicar un formato diferente al documento. En este punto, tiene pruebas de más de una razón para cambiar (y la violación de SRP) y debe realizar la refactorización adecuada.

Muchas veces me pegué un tiro en el pie al pasar demasiado tiempo adaptando el código para acomodar esos cambios. En lugar de simplemente imprimir el maldito estúpido PDF.

Refactorizador para reducir el código

El patrón de un solo uso puede crear código hinchado. Donde los paquetes están contaminados con pequeñas clases específicas que crean un montón de código basura que no tiene sentido individualmente. Debe abrir docenas de archivos de origen solo para comprender cómo llega a la parte de impresión. Además de eso, puede haber cientos, si no miles de líneas de código que están en su lugar solo para ejecutar 10 líneas de código que hacen la impresión real.

Crea una diana

El patrón de uso único estaba destinado a reducir el código fuente y mejorar la reutilización del código. Estaba destinado a crear especializaciones e implementaciones específicas. Una especie de bullseyecódigo fuente para que usted lo haga go to specific tasks. Cuando había un problema con la impresión, sabía exactamente a dónde ir para solucionarlo.

El uso único no significa fractura ambigua

Sí, tiene un código que ya imprime un documento. Sí, ahora debe cambiar el código para imprimir también archivos PDF. Sí, ahora debe cambiar el formato del documento.

¿Estás seguro de que usageha cambiado significativamente?

Si la refactorización hace que las secciones del código fuente se generalicen demasiado. Hasta el punto de que la intención original de printing stuffya no es explícita, ha creado fracturas ambiguas en el código fuente.

¿El nuevo chico será capaz de resolver esto rápidamente?

Mantenga siempre su código fuente en la organización más fácil de entender.

No seas un relojero

Demasiadas veces he visto a los desarrolladores colocarse un ocular y centrarse en los pequeños detalles hasta el punto de que nadie más podría volver a armar las piezas si se desmoronara.

ingrese la descripción de la imagen aquí


2

Una razón para el cambio es, en última instancia, un cambio en la especificación o información sobre el entorno donde se ejecuta la aplicación. Por lo tanto, un principio de responsabilidad única le dice que escriba cada componente (clase, función, módulo, servicio ...) para que tenga que considerar la menor especificación y el entorno de ejecución posible.

Como conoce la especificación y el entorno cuando escribe el componente, puede aplicar el principio.

Si considera el ejemplo de código que imprime un documento. Debe considerar si puede definir la plantilla de diseño sin considerar que el documento terminará en PDF. Puedes hacerlo, por lo que SRP te dice que deberías.

Por supuesto, YAGNI te está diciendo que no deberías. Necesita encontrar el equilibrio entre los principios de diseño.


2

Flup se dirige en la dirección correcta. El "principio de responsabilidad única" se aplicó originalmente a los procedimientos. Por ejemplo, Dennis Ritchie diría que una función debe hacer una cosa y hacerlo bien. Luego, en C ++, Bjarne Stroustrup diría que una clase debería hacer una cosa y hacerlo bien.

Tenga en cuenta que, excepto como reglas generales, estos dos tienen poco o nada que ver formalmente entre sí. Solo atienden a lo que es conveniente expresar en el lenguaje de programación. Bueno, eso es algo. Pero es una historia bastante diferente a la que está conduciendo flup.

Las implementaciones modernas (es decir, ágiles y DDD) se centran más en lo que es importante para el negocio que en lo que el lenguaje de programación puede expresar. La parte sorprendente es que los lenguajes de programación aún no se han puesto al día. Los antiguos lenguajes similares a FORTRAN capturan responsabilidades que se ajustan a los principales modelos conceptuales de la época: los procesos que uno aplicaba a cada tarjeta a medida que pasaba por el lector de tarjetas, o (como en C) el procesamiento que acompañaba cada interrupción. Luego vinieron los idiomas ADT, que habían madurado hasta el punto de capturar lo que la gente DDD reinventaría más tarde como importante (aunque Jim Neighbours tenía la mayor parte de esto resuelto, publicado y en uso en 1968): lo que hoy llamamos clases . (NO son módulos).

Este paso fue menos una evolución que un columpio pendular. A medida que el péndulo se movía hacia los datos, perdimos el modelo de caso de uso inherente a FORTRAN. Eso está bien cuando su enfoque principal involucra los datos o las formas en una pantalla. Es un gran modelo para programas como PowerPoint, o al menos para sus operaciones simples.

Lo que se perdió son las responsabilidades del sistema . No vendemos los elementos de DDD. Y no conocemos bien los métodos de clase. Vendemos responsabilidades del sistema. En algún nivel, debe diseñar su sistema en torno al principio de responsabilidad única.

Entonces, si nos fijamos en personas como Rebecca Wirfs-Brock, o yo, que solían hablar sobre métodos de clase, ahora estamos hablando en términos de casos de uso. Eso es lo que vendemos. Esas son las operaciones del sistema. Un caso de uso debe tener una sola responsabilidad. Un caso de uso rara vez es una unidad arquitectónica. Pero todos intentaban fingir que sí. Testigo de la gente SOA, por ejemplo.

Es por eso que estoy entusiasmado con la arquitectura DCI de Trygve Reenskaug, que es lo que se describe en el libro de Arquitectura Lean anterior. Finalmente le da cierta importancia a lo que solía ser un respeto arbitrario y místico a la "responsabilidad única", como se encuentra en la mayoría de los argumentos anteriores. Esa estatura se relaciona con los modelos mentales humanos: los usuarios finales primero Y los programadores después. Se relaciona con preocupaciones comerciales. Y, casi por casualidad, encapsula el cambio a medida que flup nos desafía.

El principio de responsabilidad única, tal como lo conocemos, es un dinosaurio sobrante de sus días de origen o un caballo de pasatiempo que utilizamos como sustituto de la comprensión. Necesitas dejar algunos de estos caballos de afición para hacer un gran software. Y eso requiere pensar fuera de la caja. Mantener las cosas simples y fáciles de entender solo funciona cuando el problema es simple y fácil de entender. No estoy terriblemente interesado en esas soluciones: no son típicas, y no es donde radica el desafío.


2
Al leer lo que escribiste, en algún momento del camino perdí totalmente de vista lo que estás hablando. Las buenas respuestas no tratan la pregunta como el punto de partida de una caminata en el bosque, sino más bien como un tema definido para vincular toda la escritura.
Donal Fellows

1
Ah, eres uno de esos, como uno de mis viejos gerentes. "No queremos entenderlo: ¡queremos mejorarlo!" La cuestión temática clave aquí es de principio: esa es la "P" en "SRP". Quizás hubiera respondido directamente a la pregunta si fuera la correcta: no lo era. Puedes abordar eso con quien haya planteado la pregunta.
Hacer frente

Hay una buena respuesta enterrada aquí en alguna parte. Creo que ...
RubberDuck

0

Sí, el Principio de Responsabilidad Única debe aplicarse al nuevo código.

¡Pero! ¿Qué es una responsabilidad?

¿Es "imprimir un informe una responsabilidad"? La respuesta, creo que es "Quizás".

Tratemos de usar la definición de SRP como "tener una sola razón para cambiar".

Supongamos que tiene una función que imprime informes. Si tiene dos cambios:

  1. cambiar esa función porque su informe debe tener un fondo negro
  2. cambie esa función porque necesita imprimir en pdf

Luego, el primer cambio es "cambiar el estilo del informe", el otro es "cambiar el formato de salida del informe" y ahora debe colocarlos en dos funciones diferentes porque estas son cosas diferentes.

Pero si su segundo cambio hubiera sido:

2b. cambiar esa función porque su informe necesita una fuente diferente

Yo diría que ambos cambios son "cambiar el estilo del informe" y pueden permanecer en una función.

Entonces, ¿dónde nos deja eso? Como de costumbre, debe tratar de mantener las cosas simples y fáciles de entender. Si cambiar el color de fondo significa 20 líneas de código y cambiar la fuente significa 20 líneas de código, vuelva a hacer dos funciones. Si es una línea cada una, manténgala en una.


0

Cuando esté diseñando un nuevo sistema, es aconsejable considerar el tipo de cambios que puede tener que hacer durante su vida útil y qué tan caros se les dará a la arquitectura que está implementando. Dividir su sistema en módulos es una decisión costosa de equivocarse.

Una buena fuente de información es el modelo mental en la cabeza de los expertos de dominio de la empresa. Tome el ejemplo del documento, el formato y el pdf. Los expertos en dominios probablemente le dirán que formatean sus letras usando plantillas de documentos. Ya sea en papelería o en Word o lo que sea. Puede recuperar esta información antes de comenzar a codificar y usarla en su diseño.

Una gran lectura sobre estas cosas: Lean Architecture por Coplien


0

"Imprimir" es muy parecido a "ver" en MVC. Cualquiera que entienda los conceptos básicos de los objetos lo entenderá.

Es una responsabilidad del sistema . Se implementa como un mecanismo, MVC, que involucra una impresora (la Vista), lo que se imprime (el Módulo) y la solicitud y las opciones de la impresora (del Controlador).

Tratar de localizar esto como una responsabilidad de clase o módulo es una tontería y refleja el pensamiento de 30 años. Hemos aprendido mucho desde entonces, y se evidencia ampliamente en la literatura y en el código de programadores maduros.


0

¿No es una mejor idea comenzar realmente a aplicar SRP cuando las solicitudes para cambiar el código comienzan a llegar?

Idealmente, ya tendrá una buena idea de cuáles son las responsabilidades de las diversas partes del código. Divídase en responsabilidades de acuerdo con sus primeros instintos, posiblemente teniendo en cuenta lo que quieren hacer las bibliotecas que está utilizando (delegar una tarea, una responsabilidad, a una biblioteca generalmente es una gran cosa, siempre que esa biblioteca pueda realmente hacer la tarea ) Luego, refine su comprensión de las responsabilidades de acuerdo con los requisitos cambiantes. Cuanto mejor entienda el sistema inicialmente, menos necesita cambiar fundamentalmente las asignaciones de responsabilidad (aunque a veces descubre que una responsabilidad se divide mejor en sub-responsabilidades).

No es que deba pasar mucho tiempo preocupándose por eso. Una característica clave del código es que se puede cambiar más tarde, no tiene que hacerlo completamente bien la primera vez. Simplemente trate de mejorar con el tiempo para aprender qué tipo de responsabilidades de forma tiene para que pueda cometer menos errores en el futuro.

Un ejemplo sería una pieza de código que imprime un documento. Se presenta una solicitud para cambiarlo para imprimir a PDF y luego se realiza una segunda solicitud para cambiarlo y aplicar un formato diferente al documento. En este punto, tiene pruebas de más de una razón para cambiar (y la violación de SRP) y debe realizar la refactorización adecuada.

Esto es estrictamente una indicación de que la responsabilidad general - "imprimir" el código - tiene sub-responsabilidades y debe dividirse en partes. Eso no es una violación de SRP per se, sino más bien una indicación de que probablemente se requiera la partición (quizás en subtareas de "formateo" y "representación"). ¿Puede describir esas responsabilidades con claridad para que pueda comprender lo que sucede dentro de las subtareas sin mirar su implementación? Si puede, es probable que sean divisiones razonables.

También podría ser más claro si miramos un ejemplo real simple. Consideremos el sort()método de utilidad en java.util.Arrays. ¿Qué hace? Ordena una matriz, y eso es todo lo que hace. No imprime los elementos, no encuentra al miembro más apto moralmente, no silba a Dixie . Simplemente ordena una matriz. No tienes que saber cómo tampoco. La clasificación es la única responsabilidad de ese método. (De hecho, hay muchos métodos de clasificación en Java por razones técnicas algo desagradables que tienen que ver con los tipos primitivos; sin embargo, no tiene que prestarle atención, ya que todos tienen responsabilidades equivalentes).

Haga sus métodos, sus clases, sus módulos, haga que tengan un papel tan claramente designado en la vida. Mantiene la cantidad que debe comprender de inmediato, y eso a su vez es lo que le permite manejar el diseño y el mantenimiento de un sistema grande.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.