Escribo mucho código que implica tres pasos básicos.
- Obtenga datos de alguna parte.
- Transforma esos datos.
- Ponga esos datos en alguna parte.
Normalmente termino usando tres tipos de clases, inspirados por sus respectivos patrones de diseño.
- Fábricas: para construir un objeto a partir de algún recurso.
- Mediadores: para usar la fábrica, realizar la transformación y luego usar el comandante.
- Comandantes: para poner esos datos en otro lugar.
Mis clases tienden a ser bastante pequeñas, a menudo un único método (público), por ejemplo, obtener datos, transformar datos, trabajar, guardar datos. Esto conduce a una proliferación de clases, pero en general funciona bien.
Cuando lucho es cuando vengo a las pruebas, termino con pruebas estrechamente acopladas. Por ejemplo;
- Fábrica: lee archivos del disco.
- Comandante: escribe archivos en el disco.
No puedo probar uno sin el otro. Podría escribir código de 'prueba' adicional para hacer lectura / escritura en disco también, pero luego me estoy repitiendo.
Mirando a .Net, la clase File adopta un enfoque diferente, combina las responsabilidades (de mi) fábrica y comandante juntos. Tiene funciones para Crear, Eliminar, Existe y Leer todo en un solo lugar.
¿Debería seguir el ejemplo de .Net y combinar, particularmente cuando se trata de recursos externos, mis clases juntas? El código aún se acopla, pero es más intencional: ocurre en la implementación original, en lugar de en las pruebas.
¿Es mi problema aquí que he aplicado el Principio de Responsabilidad Única de manera algo entusiasta? Tengo clases separadas responsables de leer y escribir. Cuando podría tener una clase combinada que es responsable de tratar con un recurso en particular, por ejemplo, el disco del sistema.
fuente
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.
- Tenga en cuenta que está combinando "responsabilidad" con "algo que hacer". Una responsabilidad es más como un "área de preocupación". La responsabilidad de la clase File es realizar operaciones de archivo.File
biblioteca de C # es que, por lo que sabemos, laFile
clase podría ser simplemente una fachada, colocando todas las operaciones de archivo en un solo lugar, dentro de la clase, pero podría estar usando internamente clases de lectura / escritura similares a las suyas, lo que en realidad contienen la lógica más complicada para el manejo de archivos. Dicha clase (theFile
) aún se adheriría al SRP, porque el proceso de trabajar realmente con el sistema de archivos se abstraería detrás de otra capa, muy probablemente con una interfaz unificadora. No digo que sea el caso, pero podría serlo. :)Respuestas:
Seguir el principio de responsabilidad única puede haber sido lo que lo guió aquí, pero el lugar donde se encuentra tiene un nombre diferente.
Segmentación de responsabilidad de consulta de comando
Ve a estudiar eso y creo que lo encontrarás siguiendo un patrón familiar y que no estás solo preguntándote hasta dónde llegarás. La prueba de fuego es si seguir esto te está dando beneficios reales o si es solo un mantra ciego que sigues para que no tengas que pensar.
Has expresado preocupación por las pruebas. No creo que seguir CQRS impida escribir código comprobable. Simplemente podría estar siguiendo CQRS de una manera que haga que su código no sea verificable.
Ayuda a saber cómo usar el polimorfismo para invertir las dependencias del código fuente sin necesidad de cambiar el flujo de control. No estoy realmente seguro de dónde está su conjunto de habilidades en las pruebas de escritura.
Una advertencia: seguir los hábitos que encuentras en las bibliotecas no es óptimo. Las bibliotecas tienen sus propias necesidades y son francamente viejas. Entonces, incluso el mejor ejemplo es solo el mejor ejemplo de aquel entonces.
Esto no quiere decir que no haya ejemplos perfectamente válidos que no sigan CQRS. Seguirlo siempre será un poco doloroso. No siempre vale la pena pagar. Pero si lo necesita, se alegrará de haberlo usado.
Si lo usa, preste atención a esta palabra de advertencia:
fuente
Necesita una perspectiva más amplia para determinar si el código se ajusta al Principio de responsabilidad única. No se puede responder simplemente analizando el código en sí, debe considerar qué fuerzas o actores podrían hacer que los requisitos cambien en el futuro.
Digamos que almacena los datos de la aplicación en un archivo XML. ¿Qué factores podrían hacer que cambie el código relacionado con la lectura o la escritura? Algunas posibilidades:
En todos estos casos, deberá cambiar tanto la lógica de lectura como la de escritura. En otras palabras, son no responsabilidades separadas.
Pero imaginemos un escenario diferente: su aplicación es parte de una tubería de procesamiento de datos. Lee algunos archivos CSV generados por un sistema separado, realiza algunos análisis y procesamiento y luego genera un archivo diferente para que sea procesado por un tercer sistema. En este caso, la lectura y la escritura son responsabilidades independientes y deben desacoplarse.
En pocas palabras: en general, no puede decir si leer y escribir archivos son responsabilidades separadas, depende de los roles en la aplicación. Pero según su sugerencia sobre las pruebas, supongo que es una responsabilidad única en su caso.
fuente
Generalmente tienes la idea correcta.
Parece que tienes tres responsabilidades. OMI, el "Mediador" puede estar haciendo demasiado. Creo que deberías comenzar modelando tus tres responsabilidades:
Entonces un programa puede expresarse como:
No creo que esto sea un problema. Muchas clases IMO cohesivas y comprobables son mejores que las clases grandes y menos cohesivas.
Cada pieza debe ser independientemente comprobable. Modelado anteriormente, puede representar la lectura / escritura en un archivo como:
Puede escribir pruebas de integración para probar estas clases y verificar que leen y escriben en el sistema de archivos. El resto de la lógica se puede escribir como transformaciones. Por ejemplo, si los archivos tienen formato JSON, puede transformar el
String
s.Entonces puedes transformarte en objetos apropiados:
Cada uno de estos es independientemente comprobable. También puede probar la unidad
program
anterior al burlarsereader
,transformer
ywriter
.fuente
FileWriter
leyendo directamente desde el sistema de archivos en lugar de usarFileReader
. Realmente depende de usted cuáles son sus objetivos en la prueba. Si lo usaFileReader
, la prueba se interrumpirá si se rompeFileReader
o noFileWriter
, lo que puede llevar más tiempo depurar.Entonces, el enfoque aquí está en lo que los une . ¿Pasa un objeto entre los dos (como un
File
?) Entonces es el archivo con el que están acoplados, no entre sí.De lo que has dicho, has separado tus clases. La trampa es que los estás probando juntos porque es más fácil o 'tiene sentido' .
¿Por qué necesita la entrada
Commander
para provenir de un disco? Lo único que le importa es escribir usando una determinada entrada, luego puede verificar que escribió el archivo correctamente usando lo que está en la prueba .La parte real que está probando
Factory
es "¿leerá este archivo correctamente y generará lo correcto"? Así que mofa el archivo antes de leerlo en la prueba .Alternativamente, probar que la Fábrica y el Comandante funcionan cuando están acoplados está bien: coincide bastante bien con las Pruebas de integración. La pregunta aquí es más una cuestión de si su unidad puede probarlos por separado o no.
fuente
Es un enfoque de procedimiento típico, sobre el que David Parnas escribió en 1972. Te concentras en cómo van las cosas. Tomas la solución concreta de tu problema como un patrón de nivel superior, que siempre está mal.
Si persigue un enfoque orientado a objetos, prefiero concentrarme en su dominio . ¿Que es todo esto? ¿Cuáles son las principales responsabilidades de su sistema? ¿Cuáles son los conceptos principales que se presentan en el lenguaje de los expertos de su dominio? Entonces, comprenda su dominio, descomponga, trate las áreas de responsabilidad de nivel superior como sus módulos , trate los conceptos de nivel inferior representados como sustantivos como sus objetos. Aquí hay un ejemplo que proporcioné a una pregunta reciente, es muy relevante.
Y hay un problema evidente con la cohesión, usted mismo lo ha mencionado. Si realiza alguna modificación es una lógica de entrada y escribe pruebas en ella, de ninguna manera demuestra que su funcionalidad funciona, ya que podría olvidarse de pasar esos datos a la siguiente capa. Ver, estas capas están intrínsecamente acopladas. Y un desacoplamiento artificial empeora las cosas. Lo sé yo mismo: proyecto de 7 años con 100 años-hombre detrás de mis hombros, escrito completamente en este estilo. Huye de él si puedes.
Y sobre todo el asunto SRP. Se trata de cohesión aplicada a su espacio problemático, es decir, dominio. Ese es el principio fundamental detrás de SRP. Esto da como resultado que los objetos sean inteligentes e implementen sus responsabilidades por sí mismos. Nadie los controla, nadie les proporciona datos. Combinan datos y comportamiento, exponiendo solo lo último. Por lo tanto, sus objetos combinan validación de datos sin procesar, transformación de datos (es decir, comportamiento) y persistencia. Podría verse así:
Como resultado, hay bastantes clases cohesivas que representan alguna funcionalidad. Tenga en cuenta que la validación generalmente va a los objetos de valor, al menos en el enfoque DDD .
fuente
Tenga cuidado con las abstracciones con fugas cuando trabaje con el sistema de archivos: lo vi descuidado con demasiada frecuencia y tiene los síntomas que ha descrito.
Si la clase funciona con datos que provienen / entran en estos archivos, el sistema de archivos se convierte en detalles de implementación (E / S) y debe separarse de ellos. Estas clases (fábrica / comandante / mediador) no deben conocer el sistema de archivos a menos que su único trabajo sea almacenar / leer los datos proporcionados. Las clases que se ocupan del sistema de archivos deben encapsular parámetros específicos del contexto, como las rutas (podrían pasar a través del constructor), por lo que la interfaz no reveló su naturaleza (la palabra "Archivo" en el nombre de la interfaz es un olor casi siempre).
fuente
En mi opinión, parece que has comenzado a seguir el camino correcto, pero no lo has llevado lo suficientemente lejos. Creo que dividir la funcionalidad en diferentes clases que hacen una cosa y lo hacen bien es correcto.
Para ir un paso más allá, debe crear interfaces para sus clases Factory, Mediator y Commander. Luego puede usar versiones simuladas de esas clases al escribir sus pruebas unitarias para las implementaciones concretas de las otras. Con los simulacros puede validar que los métodos se invocan en el orden correcto y con los parámetros correctos y que el código bajo prueba se comporta correctamente con diferentes valores de retorno.
También podría considerar abstraer la lectura / escritura de los datos. Vas a un sistema de archivos ahora pero quizás quieras ir a una base de datos o incluso a un socket en algún momento en el futuro. Su clase de mediador no debería tener que cambiar si el origen / destino de los datos cambia.
fuente