El Principio de Responsabilidad Única establece que una clase debe hacer una y solo una cosa. Algunos casos son bastante claros. Otros, sin embargo, son difíciles porque lo que parece "una cosa" cuando se ve en un nivel dado de abstracción puede ser varias cosas cuando se ve en un nivel inferior. También temo que si se respeta el Principio de Responsabilidad Única en los niveles más bajos, puede resultar un código de ravioles excesivamente desacoplado y detallado, en el que se gastan más líneas creando clases pequeñas para todo y sondeando información en lugar de resolver el problema en cuestión.
¿Cómo describirías lo que significa "una cosa"? ¿Cuáles son algunos signos concretos de que una clase realmente hace más que "una cosa"?
design
object-oriented
dsimcha
fuente
fuente
Respuestas:
Me gusta mucho la forma en que Robert C. Martin (tío Bob) reitera el Principio de responsabilidad única (vinculado a PDF) :
Es sutilmente diferente de la definición tradicional de "debería hacer una sola cosa", y me gusta porque te obliga a cambiar la forma en que piensas sobre tu clase. En lugar de pensar en "¿esto está haciendo una cosa?", Piensas qué puede cambiar y cómo esos cambios afectan a tu clase. Entonces, por ejemplo, si la base de datos cambia, ¿su clase necesita cambiar? ¿Qué pasa si el dispositivo de salida cambia (por ejemplo, una pantalla, un dispositivo móvil o una impresora)? Si su clase necesita cambiar debido a cambios de muchas otras direcciones, entonces esa es una señal de que su clase tiene demasiadas responsabilidades.
En el artículo vinculado, el tío Bob concluye:
fuente
Sigo preguntándome qué problema está tratando de resolver SRP. ¿Cuándo me ayuda SRP? Esto es lo que se me ocurrió:
Debe refactorizar la responsabilidad / funcionalidad fuera de una clase cuando:
1) Has duplicado la funcionalidad (DRY)
2) Descubres que tu código necesita otro nivel de abstracción para ayudarte a entenderlo (KISS)
3) Usted encuentra que los expertos en dominio entienden que las funciones son parte de un componente diferente (lenguaje ubicuo)
NO DEBE refactorizar la responsabilidad fuera de una clase cuando:
1) No hay ninguna funcionalidad duplicada.
2) La funcionalidad no tiene sentido fuera del contexto de su clase. Dicho de otra manera, su clase proporciona un contexto en el que es más fácil entender la funcionalidad.
3) Sus expertos en dominios no tienen un concepto de esa responsabilidad.
Se me ocurre que si SRP se aplica de manera amplia, intercambiamos un tipo de complejidad (tratando de hacer cara o cruz de una clase con demasiadas cosas dentro) con otro tipo (tratando de mantener a todos los colaboradores / niveles de abstracción directa para descubrir qué hacen realmente estas clases).
En caso de duda, ¡déjalo afuera! Siempre puede refactorizar más tarde, cuando haya un caso claro para ello.
¿Qué piensas?
fuente
No sé si hay una escala objetiva, pero lo que lo delataría serían los métodos, no tanto el número de ellos sino la variedad de su función. Estoy de acuerdo en que puede llevar la descomposición demasiado lejos, pero no seguiría ninguna regla dura y rápida al respecto.
fuente
La respuesta está en la definición.
Lo que define la responsabilidad de ser, en última instancia, le da el límite.
Ejemplo:
Tengo un componente que tiene la responsabilidad de mostrar facturas -> en este caso, si comencé a agregar algo más, entonces estoy rompiendo el principio.
Por otro lado, si dije responsabilidad de manejar las facturas -> agregar múltiples funciones más pequeñas (por ejemplo, imprimir Factura , actualizar Factura ) todas están dentro de ese límite.
Sin embargo, si ese módulo comenzó a manejar cualquier funcionalidad fuera de las facturas , entonces estaría fuera de ese límite.
fuente
Siempre lo veo en dos niveles:
Entonces, algo así como un objeto de dominio llamado Perro:
Dog
es mi clase pero los perros pueden hacer muchas cosas! Podría tener métodos comowalk()
,run()
ybite(DotNetDeveloper spawnOfBill)
(lo siento, no pude resistir; p).Si
Dog
se vuelve difícil de manejar a continuación, que lo pensaría cómo los grupos de esos métodos se pueden modelar juntos en otra clase, como unaMovement
clase, lo que podría contener miswalk()
yrun()
métodos.No existe una regla estricta y rápida, su diseño OO evolucionará con el tiempo. Intento buscar una interfaz clara / API pública, así como métodos simples que hagan una cosa y una cosa bien.
fuente
Lo miro más a lo largo de las líneas de una clase solo debería representar una cosa. Apropiarse @ ejemplo de Karianna, tengo a mi
Dog
clase, que tiene métodos parawalk()
,run()
ybark()
. No voy a añadir métodos parameaow()
,squeak()
,slither()
ofly()
porque esas no son cosas que hacen los perros. Son cosas que hacen otros animales, y esos otros animales tendrían sus propias clases para representarlos.(Por cierto, si su perro no vuela, entonces probablemente debería dejar de arrojándolo por la ventana).
fuente
SeesFood
como una característica deDogEyes
,Bark
como algo hecho por aDogVoice
, yEat
como algo hecho por aDogMouth
, entoncesif (dog.SeesFood) dog.Eat(); else dog.Bark();
se convertirá en una lógica similarif (eyes.SeesFood) mouth.Eat(); else voice.Bark();
, perdiendo cualquier sentido de identidad de que los ojos, la boca y la voz están todos conectados a un solo derecho.Dog
clase, entonces probablemente estéDog
relacionado. Si no, entonces probablemente terminarías con algo así enmyDog.Eyes.SeesFood
lugar de soloeyes.SeesFood
. Otra posibilidad es queDog
expone laISee
interfaz que exige laDog.Eyes
propiedad y elSeesFood
método.Eye
clase maneje la mecánica , pero un perro debería "ver" usando sus ojos, en lugar de simplemente tener ojos que puedan ver. Un perro no es un ojo, pero tampoco es simplemente un sostenedor de ojos. Es una "cosa que puede [al menos intentar] ver", y debe describirse a través de la interfaz como tal. Incluso a un perro ciego se le puede preguntar si ve comida; no será muy útil, ya que el perro siempre dirá "no", pero no tiene nada de malo preguntar.Una clase debe hacer una cosa cuando se ve en su propio nivel de abstracción. Sin duda hará muchas cosas en un nivel menos abstracto. Así es como funcionan las clases para hacer que los programas sean más fáciles de mantener: ocultan detalles de implementación si no necesita examinarlos de cerca.
Yo uso los nombres de clase como prueba para esto. Si no puedo dar a una clase un nombre descriptivo bastante corto, o si ese nombre tiene una palabra como "Y", probablemente esté violando el Principio de Responsabilidad Única.
En mi experiencia, es más fácil mantener este principio en los niveles inferiores, donde las cosas son más concretas.
fuente
Se trata de tener un rol único .
Cada clase debe resumirse con un nombre de rol. Un rol es de hecho un (conjunto de) verbo (s) asociado (s) con un contexto.
Por ejemplo :
Archivo proporciona acceso a un archivo. FileManager gestiona objetos File.
Datos de retención de recursos para un recurso de un archivo. ResourceManager mantiene y proporciona todos los recursos.
Aquí puede ver que algunos verbos como "administrar" implican un conjunto de otros verbos. Los verbos solos se consideran mejor como funciones que las clases, la mayoría de las veces. Si el verbo implica demasiadas acciones que tienen su propio contexto común, entonces debería ser una clase en sí misma.
Por lo tanto, la idea es solo permitirle tener una idea simple de lo que hace la clase al definir un rol único, que podría ser el agregado de varios sub-roles (realizados por objetos miembros u otros objetos).
A menudo construyo clases de Manager que tienen varias otras clases diferentes. Como una Fábrica, un Registro, etc. Vea una clase de Gerente como una especie de jefe de grupo, un jefe de orquesta que guía a otras personas a trabajar juntas para lograr una idea de alto nivel. Él tiene un rol, pero implica trabajar con otros roles únicos en su interior. También se puede ver cómo se organiza una empresa: un CEO no es productivo en el nivel de productividad pura, pero si no está allí, entonces nada puede funcionar correctamente juntos. Ese es su papel.
Cuando diseñe, identifique roles únicos. Y para cada rol, nuevamente vea si no se puede cortar en varios otros roles. De esa manera, si necesita cambiar fácilmente la forma en que su Gerente construye objetos, simplemente cambie la Fábrica y vaya con tranquilidad.
fuente
El SRP no se trata solo de dividir las clases, sino también de delegar la funcionalidad.
En el ejemplo de Dog utilizado anteriormente, no use SRP como justificación para tener 3 clases separadas como DogBarker, DogWalker, etc. (baja cohesión). En cambio, mire la implementación de los métodos de una clase y decida si "saben demasiado". Todavía puede tener dog.walk (), pero probablemente el método walk () debería delegar a otra clase los detalles de cómo se logra caminar.
En efecto, estamos permitiendo que la clase Perro tenga una razón para cambiar: porque los Perros cambian. Por supuesto, combinando esto con otros principios SÓLIDOS, Extendería Dog para una nueva funcionalidad en lugar de cambiar Dog (abierto / cerrado). E inyectaría sus dependencias como IMove e IEat. Y, por supuesto, haría estas interfaces separadas (segregación de interfaz). El perro solo cambiaría si encontramos un error o si los perros cambian fundamentalmente (Liskov Sub, no extienda y luego elimine el comportamiento).
El efecto neto de SOLID es que podemos escribir código nuevo con más frecuencia de la que tenemos que modificar el código existente, y eso es una gran victoria.
fuente
Todo depende de la definición de responsabilidad y de cómo esa definición va a afectar el mantenimiento de su código. Todo se reduce a una cosa y así es como su diseño lo ayudará a mantener su código.
Y como alguien dijo: "Caminar sobre el agua y diseñar software a partir de especificaciones específicas es fácil, siempre que ambos estén congelados".
Entonces, si estamos definiendo la responsabilidad de una manera más concreta, entonces no necesitamos cambiarla.
A veces las responsabilidades serán evidentemente obvias, pero a veces puede ser sutil y debemos decidir juiciosamente.
Supongamos que agregamos otra responsabilidad a la clase Dog llamada catchThief (). Ahora podría estar conduciendo a una responsabilidad diferente adicional. Mañana, si el Departamento de Policía debe cambiar la forma en que el Perro atrapa al ladrón, entonces la clase del Perro tendrá que cambiar. Sería mejor crear otra subclase en este caso y llamarla ThiefCathcerDog. Pero desde una perspectiva diferente, si estamos seguros de que no va a cambiar en ninguna circunstancia, o la forma en que se ha implementado catchThief depende de algún parámetro externo, entonces está perfectamente bien tener esta responsabilidad. Si las responsabilidades no son sorprendentemente extrañas, entonces tenemos que decidirlo juiciosamente en función del caso de uso.
fuente
"Una razón para cambiar" depende de quién está usando el sistema. Asegúrese de tener casos de uso para cada actor y haga una lista de los cambios más probables; para cada posible cambio de caso de uso, asegúrese de que solo una clase se vea afectada por ese cambio. Si está agregando un caso de uso completamente nuevo, asegúrese de que solo necesite extender una clase para hacerlo.
fuente