Estoy tratando de adherirme al Principio de Responsabilidad Única (SRP) tanto como sea posible y me acostumbré a un cierto patrón (para el SRP en los métodos) confiando en gran medida en los delegados. Me gustaría saber si este enfoque es sólido o si hay problemas graves con él.
Por ejemplo, para verificar la entrada a un constructor, podría presentar el siguiente método (la Stream
entrada es aleatoria, podría ser cualquier cosa)
private void CheckInput(Stream stream)
{
if(stream == null)
{
throw new ArgumentNullException();
}
if(!stream.CanWrite)
{
throw new ArgumentException();
}
}
Este método (posiblemente) hace más de una cosa.
- Comprueba las entradas
- Lanzar diferentes excepciones
Para cumplir con el SRP, por lo tanto, cambié la lógica a
private void CheckInput(Stream stream,
params (Predicate<Stream> predicate, Action action)[] inputCheckers)
{
foreach(var inputChecker in inputCheckers)
{
if(inputChecker.predicate(stream))
{
inputChecker.action();
}
}
}
Lo que supuestamente solo hace una cosa (¿lo hace?): Verifique la entrada. Para la comprobación real de las entradas y el lanzamiento de las excepciones, he introducido métodos como
bool StreamIsNull(Stream s)
{
return s == null;
}
bool StreamIsReadonly(Stream s)
{
return !s.CanWrite;
}
void Throw<TException>() where TException : Exception, new()
{
throw new TException();
}
y puede llamar CheckInput
como
CheckInput(stream,
(this.StreamIsNull, this.Throw<ArgumentNullException>),
(this.StreamIsReadonly, this.Throw<ArgumentException>))
¿Es esto mejor que la primera opción o introduzco una complejidad innecesaria? ¿Hay alguna manera de que aún pueda mejorar este patrón, si es viable?
fuente
CheckInput
todavía está haciendo varias cosas: está iterando sobre una matriz y llamando a una función de predicado y llamando a una función de acción. ¿No es eso una violación del SRP?Respuestas:
SRP es quizás el principio de software más incomprendido.
Una aplicación de software se crea a partir de módulos, que se crean a partir de módulos, que se crean a partir de ...
En la parte inferior, una sola función, como
CheckInput
solo contendrá un poco de lógica, pero a medida que avanza, cada módulo sucesivo encapsula más y más lógica y esto es normal .SRP no se trata de hacer una sola acción atómica . Se trata de tener una sola responsabilidad, incluso si esa responsabilidad requiere múltiples acciones ... y, en última instancia, se trata de mantenimiento y capacidad de prueba :
El hecho de que
CheckInput
se implemente con dos controles y genere dos excepciones diferentes es irrelevante en cierta medida.CheckInput
tiene una responsabilidad limitada: garantizar que la entrada cumpla con los requisitos. Sí, hay múltiples requisitos, pero esto no significa que haya múltiples responsabilidades. Sí, podría dividir los cheques, pero ¿cómo ayudaría eso? En algún momento, los cheques deben estar listados de alguna manera.Comparemos:
versus:
Ahora,
CheckInput
hace menos ... ¡pero la persona que llama hace más!Ha cambiado la lista de requisitos desde
CheckInput
, donde están encapsulados, aConstructor
donde están visibles.¿Es un buen cambio? Depende:
CheckInput
solo se llama allí: es discutible, por un lado, hace visibles los requisitos, por otro lado, abarrota el código;CheckInput
se llama varias veces con los mismos requisitos , entonces viola DRY y tiene un problema de encapsulación.Es importante darse cuenta de que una sola responsabilidad puede implicar mucho trabajo. El "cerebro" de un auto sin conductor tiene una sola responsabilidad:
Es una responsabilidad única, pero requiere coordinar una tonelada de sensores y actores, tomar muchas decisiones e incluso tiene requisitos posiblemente conflictivos 1 ...
... sin embargo, todo está encapsulado. Entonces al cliente no le importa.
1 seguridad de los pasajeros, seguridad de los demás, respeto de la normativa, ...
fuente
Citando al tío Bob sobre el SRP ( https://8thlight.com/blog/uncle-bob/2014/05/08/SingleReponilabilityPrinciple.html ):
Explica que los módulos de software deben abordar las preocupaciones específicas de los interesados. Por lo tanto, respondiendo a su pregunta:
En mi opinión, solo está buscando un método, cuando debería buscar un nivel superior (nivel de clase en este caso). Quizás deberíamos echar un vistazo a lo que su clase está haciendo actualmente (y esto requiere más explicación sobre su escenario). Por ahora, tu clase sigue haciendo lo mismo. Por ejemplo, si mañana hay alguna solicitud de cambio sobre alguna validación (por ejemplo: "ahora la secuencia puede ser nula"), entonces aún debe ir a esta clase y cambiar las cosas dentro de ella.
fuente
checkInputs()
deben dividir, digamos encheckMarketingInputs()
ycheckRegulatoryInputs()
. De lo contrario, está bien combinarlos todos en un solo método.No, este cambio no es informado por el SRP.
Pregúntese por qué no hay ninguna verificación en su corrector para "el objeto pasado es una secuencia" . La respuesta es obvia: el lenguaje evita que la persona que llama compile un programa que no se transmite.
El sistema de tipos de C # es insuficiente para satisfacer sus necesidades; sus cheques están implementando la aplicación de invariantes que no se pueden expresar en el sistema de tipos hoy . Si hubiera una manera de decir que el método toma una secuencia de escritura no anulable, lo habría escrito, pero no lo hay, por lo que hizo lo siguiente mejor: impuso la restricción de tipo en tiempo de ejecución. Con suerte, también lo documentó, para que los desarrolladores que usan su método no tengan que violarlo, suspendan sus casos de prueba y luego solucionen el problema.
Poner tipos en un método no es una violación del Principio de Responsabilidad Única; tampoco es el método hacer cumplir sus precondiciones o afirmar sus postcondiciones.
fuente
No todas las responsabilidades son iguales.
Aquí hay dos cajones. Ambos tienen una responsabilidad. Cada uno tiene nombres que le permiten saber qué les pertenece. Uno es el cajón de los cubiertos. El otro es el cajón de basura.
Entonces, ¿cuál es la diferencia? El cajón de los cubiertos deja en claro lo que no le pertenece. Sin embargo, el cajón de basura acepta todo lo que cabe. Sacar las cucharas del cajón de los cubiertos parece estar muy mal. Sin embargo, me cuesta pensar en cualquier cosa que se pierda si se retira del cajón de basura. La verdad es que puedes afirmar que cualquier cosa tiene una responsabilidad única, pero ¿cuál crees que tiene la responsabilidad individual más centrada?
Un objeto que tiene una sola responsabilidad no significa que solo una cosa pueda pasar aquí. Las responsabilidades pueden anidar. Pero esas responsabilidades de anidación deberían tener sentido, no deberían sorprenderte cuando las encuentres aquí y deberías extrañarlas si se hubieran ido.
Entonces cuando ofreces
CheckInput(Stream stream);
No me preocupa que esté revisando entradas y lanzando excepciones. Me preocuparía si también estaba revisando la entrada y guardando la entrada. Esa es una sorpresa desagradable. Uno que no extrañaría si se hubiera ido.
fuente
Cuando se hace un nudo y escribe un código extraño para cumplir con un Principio de software importante, por lo general, ha entendido mal el principio (aunque a veces el principio es incorrecto). Como señala la excelente respuesta de Matthieu, todo el significado de SRP depende de la definición de "responsabilidad".
Los programadores experimentados ven estos principios y los relacionan con recuerdos del código que arruinamos; los programadores menos experimentados los ven y pueden no tener nada con lo que relacionarlos. Es una abstracción flotando en el espacio, toda sonrisa y ningún gato. Entonces adivinan, y por lo general va mal. Antes de que hayas desarrollado el sentido del caballo de programación, la diferencia entre el código extraño y complicado y el código normal no es del todo obvio.
Este no es un mandamiento religioso que debes obedecer independientemente de las consecuencias personales. Es más una regla práctica destinada a formalizar un elemento de programación de sentido común y ayudarlo a mantener su código lo más simple y claro posible. Si tiene el efecto contrario, tiene razón al buscar alguna entrada externa.
En la programación, no puede equivocarse mucho más que tratar de deducir el significado de un identificador a partir de los primeros principios con solo mirarlo, y eso se aplica tanto a los identificadores por escrito sobre la programación como a los identificadores en el código real.
fuente
Rol CheckInput
Primero, permítanme decir lo obvio
CheckInput
: hacer una cosa, incluso si está revisando varios aspectos. En última instancia, verifica la entrada . Se podría argumentar que no es una cosa si se trata de métodos llamadosDoSomething
, pero creo que es seguro asumir que la entrada de verificación no es demasiado vaga.Agregar este patrón para predicados podría ser útil si no desea que la lógica para verificar la entrada se coloque en su clase, pero este patrón parece bastante detallado para lo que está tratando de lograr. Puede ser mucho más directo simplemente pasar una interfaz
IStreamValidator
con un método únicoisValid(Stream)
si eso es lo que desea obtener. Cualquier implementación de clasesIStreamValidator
puede usar predicados comoStreamIsNull
oStreamIsReadonly
si lo desean, pero volviendo al punto central, es un cambio bastante ridículo hacer en aras del mantenimiento del principio de responsabilidad única.Prueba de cordura
Es mi idea que a todos se nos permita una "verificación de cordura" para asegurarnos de que al menos se esté tratando con un Stream que no sea nulo y que se pueda escribir, y este chequeo básico no está haciendo de alguna manera que su clase sea un validador de streams. Eso sí, sería mejor dejar los controles más sofisticados fuera de tu clase, pero ahí es donde se traza la línea. Una vez que necesite comenzar a cambiar el estado de su transmisión al leerla o dedicar recursos a la validación, ha comenzado a realizar una validación formal de su transmisión y esto es lo que debe incorporarse a su propia clase.
Conclusión
Creo que si está aplicando un patrón para organizar mejor un aspecto de su clase, merece estar en su propia clase. Dado que un patrón no encaja, también debe preguntarse si realmente pertenece o no a su propia clase en primer lugar. Creo que, a menos que crea que la validación de la transmisión probablemente cambiará en el futuro, y especialmente si cree que esta validación puede ser de naturaleza dinámica, entonces el patrón que describió es una buena idea, incluso si puede ser inicialmente trivial De lo contrario, no hay necesidad de hacer arbitrariamente que su programa sea más complejo. Llamemos a las cosas por su nombre. La validación es una cosa, pero verificar la entrada nula no es validación y, por lo tanto, creo que puede estar seguro de mantenerla en su clase sin violar el principio de responsabilidad única.
fuente
El principio enfáticamente no establece que un fragmento de código debería "hacer solo una cosa".
La "responsabilidad" en SRP debe entenderse a nivel de los requisitos. La responsabilidad del código es satisfacer los requisitos comerciales. Se viola SRP si un objeto satisface más de un requisito comercial independiente . Por independiente significa que un requisito podría cambiar mientras el otro requisito permanece en su lugar.
Es concebible que se introduzca un nuevo requisito comercial, lo que significa que este objeto en particular no debería ser legible, mientras que otro requisito comercial aún requiere que el objeto verifique si es legible. No, porque los requisitos empresariales no especifican detalles de implementación a ese nivel.
Un ejemplo real de una violación de SRP sería un código como este:
Este código es muy simple, pero aún es concebible que el texto cambie independientemente de la fecha de entrega esperada, ya que estos son decididos por diferentes partes del negocio.
fuente
Me gusta el punto de la respuesta de @ EricLippert :
EricLippert tiene razón en que este es un problema para el sistema de tipos. Y dado que desea utilizar el principio de responsabilidad única (SRP), básicamente necesita que el sistema de tipos sea responsable de este trabajo.
En realidad, es posible hacer esto en C #. Podemos capturar literales
null
en tiempo de compilación, luego capturar no literalesnull
en tiempo de ejecución. Eso no es tan bueno como una verificación completa en tiempo de compilación, pero es una mejora estricta respecto a no atrapar nunca en tiempo de compilación.Entonces, ¿sabes cómo tiene C #
Nullable<T>
? Vamos a revertir eso y hacer unNonNullable<T>
:Ahora, en lugar de escribir
, solo escribe:
Entonces, hay tres casos de uso:
El usuario llama
Foo()
con un no nuloStream
:Este es el caso de uso deseado, y funciona con o sin
NonNullable<>
.El usuario llama
Foo()
con un nuloStream
:Este es un error de llamada. Aquí
NonNullable<>
ayuda a informar al usuario que no deberían estar haciendo esto, pero en realidad no los detiene. De cualquier manera, esto resulta en un tiempo de ejecuciónNullArgumentException
.El usuario llama
Foo()
connull
:null
no se convertirá implícitamente en aNonNullable<>
, por lo que el usuario obtiene un error en el IDE antes del tiempo de ejecución. Esto es delegar la verificación nula al sistema de tipos, tal como lo recomendaría el SRP.También puede extender este método para afirmar otras cosas sobre sus argumentos. Por ejemplo, dado que desea una secuencia que se pueda escribir, puede definir una
struct WriteableStream<T> where T:Stream
que compruebe ambasnull
ystream.CanWrite
el constructor. Esto todavía sería una verificación de tipo en tiempo de ejecución, pero:Decora el tipo con el
WriteableStream
calificador, lo que indica la necesidad de las personas que llaman.Realiza la verificación en un solo lugar en el código, por lo que no tiene que repetir la verificación y
throw InvalidArgumentException
cada vez.Se ajusta mejor al SRP empujando las tareas de verificación de tipos al sistema de tipos (según lo extendido por los decoradores genéricos).
fuente
Su enfoque es actualmente procesal. Estás rompiendo el
Stream
objeto y validado desde el exterior. No hagas eso: rompe la encapsulación. Deje que elStream
responsable de su propia validación. No podemos tratar de aplicar el SRP hasta que tengamos algunas clases para aplicarlo.Aquí hay una
Stream
que realiza una acción solo si pasa la validación:¡Pero ahora estamos violando SRP! "Una clase debería tener una sola razón para cambiar". Tenemos una combinación de 1) validación y 2) lógica real. Tenemos dos razones por las que podría necesitar cambiar.
Podemos resolver esto validando decoradores . Primero, necesitamos convertir nuestro
Stream
a una interfaz e implementarlo como una clase concreta.Ahora podemos escribir un decorador que envuelva a
Stream
, realice la validación y difiera lo dadoStream
para la lógica real de la acción.Ahora podemos componer estos de la forma que queramos:
¿Quieres validación adicional? Agrega otro decorador.
fuente
El trabajo de una clase es proporcionar un servicio que cumpla con un contrato . Una clase siempre tiene un contrato: un conjunto de requisitos para usarlo, y promete cumplir con su estado y resultados siempre que se cumplan los requisitos. Este contrato puede ser explícito, a través de documentación y / o aserciones, o implícito, pero siempre existe.
Parte del contrato de su clase es que la persona que llama le da al constructor algunos argumentos que no deben ser nulos. La implementación del contrato es responsabilidad de la clase, por lo que verificar que la persona que llama ha cumplido su parte del contrato puede considerarse fácilmente dentro del alcance de la responsabilidad de la clase.
La idea de que una clase implemente un contrato se debe a Bertrand Meyer , el diseñador del lenguaje de programación Eiffel y de la idea del diseño por contrato . El lenguaje Eiffel hace que la especificación y la verificación del contrato formen parte del lenguaje.
fuente
Como se ha señalado en otras respuestas, SRP a menudo se malinterpreta. No se trata de tener un código atómico que solo haga una función. Se trata de asegurarse de que sus objetos y métodos solo hagan una cosa, y que la única cosa se haga en un solo lugar.
Veamos un mal ejemplo en pseudocódigo.
En nuestro ejemplo bastante absurdo, la "responsabilidad" del constructor Math # es hacer que el objeto matemático sea utilizable. Lo hace desinfectando primero la entrada y luego asegurándose de que los valores no sean -1.
Este es un SRP válido porque el constructor solo está haciendo una cosa. Está preparando el objeto matemático. Sin embargo, no es muy fácil de mantener. Viola SECO.
Así que demos otro paso
En este pase, mejoramos un poco acerca de DRY, pero todavía tenemos formas de ir con DRY. SRP por otro lado parece un poco fuera de lugar. Ahora tenemos dos funciones con el mismo trabajo. Tanto cleanX como cleanY desinfectan la entrada.
Vamos a darle otra oportunidad
Ahora finalmente estaban mejor sobre DRY, y SRP parece estar de acuerdo. Solo tenemos un lugar que hace el trabajo de "desinfección".
En teoría, el código es más fácil de mantener y mejor cuando vamos a corregir el error y ajustar el código, solo tenemos que hacerlo en un solo lugar.
En la mayoría de los casos del mundo real, los objetos serían más complejos y SRP se aplicaría en un conjunto de objetos. Por ejemplo, la edad puede pertenecer a Padre, Madre, Hijo, Hija, por lo que en lugar de tener 4 clases que calculan la edad desde la fecha de nacimiento, tiene una clase Persona que hace eso y las 4 clases heredan de eso. Pero espero que este ejemplo ayude a explicar. SRP no se trata de acciones atómicas, sino de un "trabajo" realizado.
fuente
Hablando de SRP, al tío Bob no le gustan los cheques nulos esparcidos por todas partes. En general, usted, como equipo, debe evitar el uso de parámetros nulos para los constructores siempre que sea posible. Cuando publica su código fuera de su equipo, las cosas pueden cambiar.
Hacer cumplir la no nulabilidad de los parámetros del constructor sin garantizar primero la cohesión de la clase en cuestión da como resultado una hinchazón en el código de llamada, especialmente las pruebas.
Si realmente quiere hacer cumplir dichos contratos, considere usar
Debug.Assert
o algo similar para reducir el desorden:fuente