Asegúrese de que cada clase tenga una sola responsabilidad, ¿por qué?

37

De acuerdo con la documentación de Microsoft, el artículo principal de Wikipedia SOLID, o la mayoría de los arquitectos de TI, debemos asegurarnos de que cada clase tenga una sola responsabilidad. Me gustaría saber por qué, porque si todos parecen estar de acuerdo con esta regla, nadie parece estar de acuerdo con las razones de esta regla.

Algunos citan un mejor mantenimiento, otros dicen que proporciona pruebas fáciles o hace que la clase sea más robusta o de seguridad. ¿Qué es correcto y qué significa realmente? ¿Por qué mejora el mantenimiento, facilita las pruebas o hace que el código sea más robusto?

Bastien Vandamme
fuente
1
También tuve una pregunta que podría encontrar relacionada: ¿Cuál es la verdadera responsabilidad de una clase?
Pierre Arlaud
3
¿No pueden ser correctas todas las razones de la responsabilidad individual? Hace que el mantenimiento sea más fácil. Hace que las pruebas sean más fáciles. Hace que la clase sea más robusta (en general).
Martin York
2
Por la misma razón que tienes clases en absoluto.
Davor Ždralo
2
Siempre he tenido un problema con esta declaración. Es muy difícil definir qué es una "responsabilidad única". Una sola responsabilidad puede variar desde "verificar que 1 + 1 = 2" hasta "mantener un registro preciso de todo el dinero ingresado y salido de las cuentas bancarias corporativas".
Dave Nay

Respuestas:

57

Modularidad. Cualquier lenguaje decente le dará los medios para pegar piezas de código, pero no hay una forma general de pegar una gran pieza de código sin que el programador realice una cirugía en la fuente. Al agrupar muchas tareas en una construcción de código, se priva a usted mismo y a otros de la oportunidad de combinar sus piezas de otras maneras, e introduce dependencias innecesarias que podrían causar cambios en una pieza para afectar a las otras.

SRP es tan aplicable a las funciones como a las clases, pero los lenguajes OOP convencionales son relativamente pobres para pegar funciones juntas.

Doval
fuente
12
+1 por mencionar la programación funcional como una forma más segura de componer abstracciones.
logc
> pero los lenguajes OOP convencionales son relativamente pobres para pegar funciones juntas. <Tal vez porque los lenguajes OOP no están relacionados con funciones, sino con mensajes.
Jeff Hubbard
@JeffHubbard Creo que quieres decir que es porque toman después de C / C ++. No hay nada en los objetos que los haga mutuamente excluyentes con funciones. Demonios, un objeto / interfaz es solo un registro / estructura de funciones. Pretender que las funciones no son importantes es injustificado tanto en teoría como en práctica. No se pueden evitar de todos modos: terminas teniendo que tirar "Runnables", "Fábricas", "Proveedores" y "Acciones" o haciendo uso de los llamados "patrones" de Estrategia y Método de plantilla, y terminas con un montón de repeticiones innecesarias para hacer el mismo trabajo.
Doval
@Doval No, realmente quiero decir que OOP se preocupa por los mensajes. Eso no quiere decir que un lenguaje dado sea mejor o peor que un lenguaje de programación funcional para pegar funciones juntas, solo que no es la principal preocupación del lenguaje .
Jeff Hubbard
Básicamente, si su lenguaje se trata de hacer que la OOP sea más fácil / mejor / más rápida / más fuerte, entonces no es en lo que se concentre si las funciones se unen bien o no.
Jeff Hubbard
30

Un mejor mantenimiento, pruebas fáciles, corrección de errores más rápida son solo resultados (muy agradables) de la aplicación de SRP. La razón principal (como dice Robert C. Matin) es:

Una clase debe tener una, y solo una, razón para cambiar.

En otras palabras, SRP plantea cambio de localidad .

SRP también promueve el código DRY. Mientras tengamos clases que tengan una sola responsabilidad, podemos elegir usarlas en cualquier lugar que queramos. Si tenemos una clase que tiene dos responsabilidades, pero solo necesitamos una de ellas y la segunda interfiere, tenemos 2 opciones:

  1. Copie y pegue la clase en otra y tal vez incluso cree otro mutante de responsabilidad múltiple (aumente un poco la deuda técnica).
  2. Divida la clase y hágalo como debería ser, lo que puede ser costoso debido al uso extensivo de la clase original.
Maciej Chałapuk
fuente
1
+1 Para vincular SRP a DRY.
Mahdi
21

Es fácil crear código para solucionar un problema en particular. Es más complicado crear código que solucione ese problema al tiempo que permite realizar cambios posteriores de forma segura. SOLID proporciona un conjunto de prácticas que mejoran el código.

En cuanto a cuál es correcto: los tres. Todos son beneficios de usar responsabilidad única y la razón por la que debe usarla.

En cuanto a lo que significan:

  • Un mejor mantenimiento significa que es más fácil cambiar y no cambia con tanta frecuencia. Debido a que hay menos código, y ese código se enfoca en algo específico, si necesita cambiar algo que no está relacionado con la clase, la clase no necesita ser cambiada. Además, cuando necesita cambiar la clase, siempre y cuando no necesite cambiar la interfaz pública, solo debe preocuparse por esa clase y nada más.
  • La prueba fácil significa menos pruebas, menos configuración. No hay tantas partes móviles en la clase, por lo que el número de posibles fallas en la clase es menor y, por lo tanto, hay menos casos que necesite probar. Habrá menos campo privado / miembros para configurar.
  • Debido a los dos anteriores, obtienes una clase que cambia menos y falla menos, y por lo tanto es más robusta.

Intente crear código por un tiempo siguiendo el principio, y vuelva a visitar ese código más adelante para hacer algunos cambios. Verá la gran ventaja que proporciona.

Usted hace lo mismo con cada clase y termina con más clases, todas cumpliendo con SRP. Hace que la estructura sea más compleja desde el punto de vista de las conexiones, pero la simplicidad de cada clase lo justifica.

Miyamoto Akira
fuente
No creo que los puntos aquí expuestos estén justificados. En particular, ¿cómo evita los juegos de suma cero donde la simplificación de una clase hace que otras clases se vuelvan más complicadas, sin un efecto neto en la base del código en su conjunto? Creo que la respuesta de @ Doval aborda este problema.
2
Haces lo mismo con todas las clases. Termina con más clases, todas cumpliendo con SRP. Hace que la estructura sea más compleja desde el punto de vista de las conexiones. Pero la simplicidad en cada clase lo justifica. Sin embargo, me gusta la respuesta de @ Doval.
Miyamoto Akira
Creo que deberías agregar eso a tu respuesta.
4

Aquí están los argumentos que, en mi opinión, respaldan la afirmación de que el Principio de responsabilidad única es una buena práctica. También proporciono enlaces a más literatura, donde puedes leer razonamientos aún más detallados, y más elocuentes que los míos:

  • Mejor mantenimiento : idealmente, cada vez que una funcionalidad del sistema tiene que cambiar, habrá una única clase que debe cambiarse. Un mapeo claro entre clases y responsabilidades significa que cualquier desarrollador involucrado en el proyecto puede identificar de qué clase se trata. (Como ha señalado @ MaciejChałapuk, vea Robert C. Martin, "Código limpio" ).

  • Pruebas más fáciles : idealmente, las clases deberían tener una interfaz pública lo más mínima posible, y las pruebas deberían abordar solo esta interfaz pública. Si no puede realizar la prueba con claridad, ya que muchas partes de su clase son privadas, esta es una señal clara de que su clase tiene demasiadas responsabilidades y que debe dividirla en subclases más pequeñas. Tenga en cuenta que esto también se aplica a los idiomas en los que no hay miembros de clase "públicos" o "privados"; tener una pequeña interfaz pública significa que es muy claro para el código del cliente qué partes de la clase se supone que debe usar. (Ver Kent Beck, "Desarrollo guiado por pruebas" para más detalles).

  • Código robusto : su código no fallará más o menos a menudo porque está bien escrito; pero, como todo código, su objetivo final no es comunicarse con la máquina , sino con otros desarrolladores (vea Kent Beck, "Patrones de implementación" , Capítulo 1.) Una base de código clara es más fácil de razonar, por lo que se introducirán menos errores , y pasará menos tiempo entre el descubrimiento de un error y su reparación.

logc
fuente
Si algo tiene que cambiar por razones comerciales, créanme con SRP, tendrá que modificar más de una clase. Decir que si un cambio de clase es solo por una razón no es lo mismo que si hay un cambio solo afectará a una clase.
Bastien Vandamme
@ B413: No quise decir que cualquier cambio implicaría un solo cambio en una sola clase. Muchas partes del sistema podrían necesitar cambios para cumplir con un solo cambio comercial. Pero tal vez tenga un punto y debería haber escrito "cada vez que una funcionalidad del sistema tiene que cambiar".
logc
3

Existen varias razones, pero la que me gusta es el enfoque utilizado por muchos de los primeros programas de UNIX: hacer una cosa bien. Es bastante difícil hacer eso con una cosa, y cada vez es más difícil cuanto más intentas hacer.

Otra razón es limitar y controlar los efectos secundarios. Me encantó mi abridor de puerta de cafetera combinada. Desafortunadamente, el café generalmente se desbordó cuando tuve visitas. Olvidé cerrar la puerta después de preparar café el otro día y alguien lo robó.

Desde el punto de vista psicológico, solo puede realizar un seguimiento de algunas cosas a la vez. Las estimaciones generales son siete más o menos dos. Si una clase hace varias cosas, debe realizar un seguimiento de todas ellas a la vez. Esto reduce su capacidad de rastrear lo que está haciendo. Si una clase hace tres cosas, y solo quieres una, puedes agotar tu capacidad de hacer un seguimiento de las cosas antes de hacer algo con la clase.

Hacer varias cosas aumenta la complejidad del código. Más allá del código más simple, el aumento de la complejidad aumenta la probabilidad de errores. Desde este punto de vista, desea que las clases sean lo más simples posible.

Probar una clase que hace una cosa es mucho más simple. No tiene que verificar que la segunda cosa que la clase hizo o no sucedió en cada prueba. Tampoco tiene que arreglar las condiciones rotas y volver a probar cuando una de estas pruebas falla.

BillThor
fuente
2

Porque el software es orgánico. Los requisitos cambian constantemente, por lo que debe manipular los componentes con el menor dolor de cabeza posible. Al no seguir los principios de SOLID, puede terminar con una base de código que se establece en concreto.

Imagine una casa con un muro de hormigón con carga. ¿Qué sucede cuando eliminas este muro sin ningún tipo de apoyo? La casa probablemente se derrumbará. En el software no queremos esto, por lo que estructuramos las aplicaciones de tal manera que pueda mover / reemplazar / modificar fácilmente los componentes sin causar mucho daño.

CodeART
fuente
1

Sigo el pensamiento: 1 Class = 1 Job.

Uso de una analogía de fisiología: motriz (sistema neural), respiración (pulmones), digestivo (estómago), olfatorio (observación), etc. Cada uno de estos tendrá un subconjunto de controladores, pero cada uno solo tiene una responsabilidad, ya sea administrar la forma en que funciona cada uno de sus respectivos subsistemas o si son un subsistema de punto final y realizan solo 1 tarea, como levantar un dedo o hacer crecer un folículo piloso.

No confunda el hecho de que puede estar actuando como gerente en lugar de como trabajador. Algunos trabajadores eventualmente son promovidos a Gerente, cuando el trabajo que realizaban se ha vuelto demasiado complicado para que un proceso lo maneje solo.

La parte más complicada de lo que he experimentado es saber cuándo designar una Clase como un proceso de Operador, Supervisor o Gerente. En cualquier caso, deberá observar y denotar su funcionalidad para 1 responsabilidad (Operador, Supervisor o Gerente).

Cuando una clase / objeto realiza más de 1 de estos roles de tipo, encontrará que el proceso general comenzará a tener problemas de rendimiento, o procesará cuellos de botella.

Obispo de oro
fuente
Incluso si la implementación del procesamiento del oxígeno atmosférico en la hemoglobina se manejara como una Lungclase, yo postularía que una instancia Personaún debería Breathe, y por lo tanto la Personclase debería contener suficiente lógica para, como mínimo, delegar las responsabilidades asociadas con ese método. Sugeriría además que un bosque de entidades interconectadas a las que solo se puede acceder a través de un propietario común a menudo es más fácil de razonar que un bosque que tiene múltiples puntos de acceso independientes.
supercat
Sí, en ese caso, el Pulmón es el Administrador, aunque las Villi serían un Supervisor, y el proceso de Respiración sería la clase Trabajador para transferir las partículas de Aire al torrente sanguíneo.
GoldBishop
@GoldBishop: Quizás la opinión correcta sería decir que una Personclase tiene como función delegar toda la funcionalidad asociada con una entidad monstruosamente complicada de "ser humano del mundo real", incluida la asociación de sus diversas partes. . Tener una entidad que haga 500 cosas es feo, pero si esa es la forma en que funciona el sistema del mundo real que se está modelando, tener una clase que delegue 500 funciones mientras conserva una identidad puede ser mejor que tener que hacer todo con piezas disjuntas.
supercat
@supercat O simplemente podría compartimentar todo y tener 1 clase con los supervisores necesarios sobre los subprocesos. De esa manera, podría (en teoría) hacer que cada proceso se separe de la clase base, Personpero aún así informa de la cadena sobre el éxito / el fracaso, pero no necesariamente afecta al otro. 500 funciones (aunque establecido como ejemplo, sería exagerado y no soportable) Intento mantener ese tipo de funcionalidad derivada y modular.
GoldBishop
@GoldBishop: Desearía que hubiera alguna forma de declarar un tipo "efímero", de modo que el código externo pueda invocar a los miembros o pasarlo como un parámetro a los propios métodos del tipo, pero nada más. Desde la perspectiva de la organización del código, clases subdividiendo tiene sentido, y ni siquiera tener el código externo a llamar a los métodos uno abajo nivel (por ejemplo, Fred.vision.lookAt(George)tiene sentido, pero permitiendo que el código que decir someEyes = Fred.vision; someTubes = Fred.digestionque parece repulsivo ya que oscurece la relación entre someEyesy someTubes.
supercat
1

Especialmente con un principio tan importante como la responsabilidad única, personalmente esperaría que haya muchas razones por las cuales las personas adoptan este principio.

Algunas de esas razones pueden ser:

  • Mantenimiento: SRP garantiza que el cambio de responsabilidad en una clase no afecte a otras responsabilidades, lo que simplifica el mantenimiento. Esto se debe a que si cada clase tiene una sola responsabilidad, los cambios realizados en una responsabilidad están aislados de otras responsabilidades.
  • Prueba: si una clase tiene una responsabilidad, es mucho más fácil descubrir cómo evaluar esta responsabilidad. Si una clase tiene múltiples responsabilidades, debe asegurarse de que está probando la correcta y que la prueba no se ve afectada por otras responsabilidades que tiene la clase.

También tenga en cuenta que SRP viene con un paquete de principios SÓLIDOS. Cumplir con SRP e ignorar el resto es tan malo como no hacer SRP en primer lugar. Por lo tanto, no debe evaluar SRP por sí mismo, sino en el contexto de todos los principios SÓLIDOS.

Eufórico
fuente
... test is not affected by other responsibilities the class has, ¿podría por favor elaborar sobre este?
Mahdi
1

La mejor manera de comprender la importancia de estos principios es tener la necesidad.

Cuando era un programador novato, no pensaba mucho en el diseño, de hecho, ni siquiera sabía que existían patrones de diseño. A medida que crecían mis programas, cambiar una cosa significaba cambiar muchas otras. Fue difícil rastrear errores, el código era enorme y repetitivo. No había mucha jerarquía de objetos, las cosas estaban por todas partes. Agregar algo nuevo o eliminar algo viejo generaría errores en las otras partes del programa. Imagínate.

En proyectos pequeños, puede que no importe, pero en proyectos grandes, las cosas pueden ser muy pesadillas. Más tarde, cuando me topé con los conceptos de patrones de diseño, me dije a mí mismo: "Oh, sí, hacer esto hubiera hecho las cosas mucho más fáciles".

Realmente no puede comprender la importancia de los patrones de diseño hasta que surja la necesidad. Respeto los patrones porque, por experiencia, puedo decir que hacen que el mantenimiento del código sea fácil y robusto.

Sin embargo, al igual que usted, todavía no estoy seguro acerca de las "pruebas fáciles", porque todavía no he tenido la necesidad de entrar en las pruebas unitarias.

harsimranb
fuente
Los patrones están de alguna manera conectados a SRP, pero SRP no es necesario cuando se aplican patrones de diseño. Es posible usar patrones e ignorar completamente SRP. Usamos patrones para abordar requisitos no funcionales, pero no se requiere el uso de patrones en ningún programa. Los principios son esenciales para hacer que el desarrollo sea menos doloroso y (OMI) debería ser un hábito.
Maciej Chałapuk
Estoy completamente de acuerdo contigo. SRP, hasta cierto punto, los ejecutores limpian el diseño y la jerarquía de los objetos. Es una buena mentalidad para un desarrollador entrar, ya que hace que el desarrollador comience a pensar en términos de objetos y cómo deberían ser. Personalmente, también ha tenido un muy buen impacto en mi desarrollo.
harsimranb
1

La respuesta es, como han señalado otros, todos son correctos, y todos se alimentan entre sí, más fácil de probar hace que el mantenimiento sea más fácil, hace que el código sea más robusto, hace que el mantenimiento sea más fácil ...

Todo esto se reduce a un principal clave: el código debe ser tan pequeño y hacer tan poco como sea necesario para hacer el trabajo. Esto se aplica a una aplicación, un archivo o una clase tanto como a una función. Cuantas más cosas haga un fragmento de código, más difícil será comprenderlo, mantenerlo, extenderlo o probarlo.

Creo que se puede resumir en una palabra: alcance. Preste mucha atención al alcance de los artefactos, cuanto menos alcance haya en un punto en particular en una aplicación, mejor.

Alcance ampliado = más complejidad = más formas de que las cosas salgan mal.

jmoreno
fuente
1

Si una clase es demasiado grande, se vuelve difícil de mantener, evaluar y comprender, otras respuestas han cubierto esta voluntad.

Es posible que una clase tenga más de una responsabilidad sin problemas, pero pronto tendrá problemas con clases demasiado complejas.

Sin embargo, tener una regla simple de "solo una responsabilidad" hace que sea más fácil saber cuándo necesita una nueva clase .

Sin embargo, definir "responsabilidad" es difícil, no significa "hacer todo lo que dice la especificación de la aplicación", la verdadera habilidad es saber cómo dividir el problema en pequeñas unidades de "responsabilidad".

Ian
fuente