¿Administrar y organizar el número masivo de clases después de cambiar a SOLID?

50

En los últimos años, hemos estado cambiando lentamente a un código progresivamente mejor escrito, unos pocos pasos a la vez. Finalmente estamos comenzando a hacer el cambio a algo que al menos se asemeja a SOLID, pero aún no hemos llegado allí. Desde que realizó el cambio, una de las mayores quejas de los desarrolladores es que no pueden soportar la revisión por pares y atravesar docenas y docenas de archivos donde anteriormente cada tarea solo requería que el desarrollador tocara de 5 a 10 archivos.

Antes de comenzar a hacer el cambio, nuestra arquitectura se organizó de manera muy similar a la siguiente (concedido, con uno o dos órdenes de magnitud más archivos):

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

En cuanto al archivo, todo fue increíblemente lineal y compacto. Obviamente, hubo una gran cantidad de duplicación de código, acoplamiento apretado y dolores de cabeza, sin embargo, todos podían atravesarlo y resolverlo. Los principiantes completos, personas que nunca habían abierto Visual Studio, podrían resolverlo en solo unas pocas semanas. La falta de complejidad general del archivo hace que sea relativamente sencillo para los desarrolladores novatos y los nuevos empleados comenzar a contribuir sin demasiado tiempo de aceleración. Pero esto es más o menos donde cualquier beneficio del estilo de código desaparece.

Respaldo de todo corazón todos los intentos que hacemos para mejorar nuestra base de código, pero es muy común obtener un retroceso del resto del equipo en cambios masivos de paradigma como este. Algunos de los mayores puntos conflictivos actualmente son:

  • Pruebas unitarias
  • Cuenta de clase
  • Complejidad de la revisión por pares

Las pruebas unitarias han sido increíblemente difíciles de vender para el equipo, ya que todos creen que es una pérdida de tiempo y que pueden manejar y probar su código mucho más rápido en conjunto que cada pieza individualmente. El uso de pruebas unitarias como un aval para SOLID ha sido en gran parte inútil y se ha convertido en una broma en este punto.

El conteo de clases es probablemente el mayor obstáculo para superar. ¡Las tareas que solían tomar de 5 a 10 archivos ahora pueden tomar de 70 a 100! Si bien cada uno de estos archivos tiene un propósito distinto, el gran volumen de archivos puede ser abrumador. La respuesta del equipo ha sido principalmente gemidos y rascarse la cabeza. Anteriormente, una tarea puede haber requerido uno o dos repositorios, un modelo o dos, una capa lógica y un método de controlador.

Ahora, para crear una aplicación simple para guardar archivos, tiene una clase para verificar si el archivo ya existe, una clase para escribir los metadatos, una clase para abstraer DateTime.Nowy poder inyectar tiempos para pruebas unitarias, interfaces para cada archivo que contiene lógica, archivos para contener pruebas unitarias para cada clase, y uno o más archivos para agregar todo a su contenedor DI.

Para aplicaciones de tamaño pequeño a mediano, SOLID es una venta súper fácil. Todos ven el beneficio y la facilidad de mantenimiento. Sin embargo, simplemente no están viendo una buena propuesta de valor para SOLID en aplicaciones a gran escala. Así que estoy tratando de encontrar formas de mejorar la organización y la gestión para superar los dolores de crecimiento.


Pensé que daría un poco más fuerte de un ejemplo del volumen del archivo basado en una tarea recientemente completada. Me dieron la tarea de implementar alguna funcionalidad en uno de nuestros microservicios más nuevos para recibir una solicitud de sincronización de archivos. Cuando se recibe la solicitud, el servicio realiza una serie de búsquedas y comprobaciones, y finalmente guarda el documento en una unidad de red, así como en 2 tablas de base de datos separadas.

Para guardar el documento en la unidad de red, necesitaba algunas clases específicas:

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

Entonces, hay un total de 15 clases (excluyendo POCO y andamios) para realizar un ahorro bastante sencillo. Este número se disparó significativamente cuando necesité crear POCO para representar entidades en unos pocos sistemas, construí algunos repositorios para comunicarme con sistemas de terceros que son incompatibles con nuestros otros ORM y construí métodos lógicos para manejar las complejidades de ciertas operaciones.

JD Davis
fuente
52
"¡Las tareas que solían tomar entre 5 y 10 archivos ahora pueden tomar entre 70 y 100!" ¿Cómo demonios? Esto no es de ninguna manera normal. ¿Qué tipo de cambios está haciendo que requieren cambiar esa cantidad de archivos?
Eufórico
43
El hecho de que tenga que cambiar más archivos por tarea (¡significativamente más!) Significa que está haciendo SOLID incorrectamente. El objetivo es organizar su código (a lo largo del tiempo) de una manera que refleje los patrones de cambio observados, simplificando los cambios. Cada principio en SOLID viene con cierto razonamiento detrás (cuándo y por qué debería aplicarse); parece que se han metido en esta situación al aplicar esto a ciegas. Lo mismo con las pruebas unitarias (TDD); Si lo está haciendo sin tener una buena comprensión de cómo hacer diseño / arquitectura, se va a hundir en un agujero.
Filip Milovanović
6060
Claramente ha adoptado SOLID como una religión en lugar de una herramienta pragmática para ayudar a hacer el trabajo. Si algo en SOLID está haciendo más trabajo o haciendo las cosas más difíciles, no lo haga.
cuál es el
25
@Euphoric: el problema puede ocurrir de ambas maneras. Sospecho que estás respondiendo a la posibilidad de que 70-100 clases sean excesivas. Pero no es imposible que se trate de un proyecto masivo que se acumuló en 5-10 archivos (he trabajado en archivos 20KLOC antes ...) y 70-100 es en realidad la cantidad correcta de archivos.
Flater
18
Hay un trastorno del pensamiento que llamo "enfermedad de felicidad de los objetos", que es la creencia de que las técnicas de OO son un fin en sí mismas, en lugar de solo una de las muchas técnicas posibles para disminuir los costos de trabajar en una gran base de código. Tiene una forma particularmente avanzada, "enfermedad de la felicidad SÓLIDA". SÓLIDO no es el objetivo. Reducir el costo de mantener la base de código es el objetivo. Evalúe sus propuestas en ese contexto, no en si es doctrinario SÓLIDO. (Que sus propuestas probablemente tampoco sean realmente doctrinarias SÓLIDAS también es un buen punto a considerar).
Eric Lippert

Respuestas:

104

Ahora, para crear una aplicación simple para guardar archivos, tiene una clase para verificar si el archivo ya existe, una clase para escribir los metadatos, una clase para abstraer DateTime. Ahora, para que pueda inyectar tiempos para pruebas unitarias, interfaces para cada archivo que contenga lógica, archivos para contener pruebas unitarias para cada clase, y uno o más archivos para agregar todo a su contenedor DI.

Creo que has entendido mal la idea de una sola responsabilidad. La única responsabilidad de una clase podría ser "guardar un archivo". Para hacer eso, puede dividir esa responsabilidad en un método que verifique si existe un archivo, un método que escribe metadatos, etc. Cada uno de esos métodos tiene una responsabilidad única, que es parte de la responsabilidad general de la clase.

Una clase para abstraer DateTime.Nowsuena bien. Pero solo necesita uno de esos y podría combinarse con otras características del entorno en una sola clase con la responsabilidad de abstraer las características del entorno. Nuevamente, una sola responsabilidad con múltiples sub-responsabilidades.

No necesita "interfaces para cada archivo que contenga lógica", necesita interfaces para clases que tienen efectos secundarios, por ejemplo, aquellas clases que leen / escriben en archivos o bases de datos; e incluso entonces, solo son necesarios para las partes públicas de esa funcionalidad. Entonces, por ejemplo AccountRepo, es posible que no necesite ninguna interfaz, es posible que solo necesite una interfaz para el acceso real a la base de datos que se inyecta en ese repositorio.

Las pruebas unitarias han sido increíblemente difíciles de vender para el equipo, ya que todos creen que es una pérdida de tiempo y que pueden manejar y probar su código mucho más rápido en conjunto que cada pieza individualmente. El uso de pruebas unitarias como un aval para SOLID ha sido en gran parte inútil y se ha convertido en una broma en este punto.

Esto sugiere que también ha entendido mal las pruebas unitarias. La "unidad" de una prueba unitaria no es una unidad de código. ¿Qué es incluso una unidad de código? ¿Una clase? ¿Un método? ¿Una variable? ¿Una sola instrucción de máquina? No, la "unidad" se refiere a una unidad de aislamiento, es decir, un código que puede ejecutarse de forma aislada de otras partes del código. Una prueba simple de si una prueba automatizada es una prueba unitaria o no es si puede ejecutarla en paralelo con todas sus otras pruebas unitarias sin afectar su resultado. Hay un par de reglas más generales para las pruebas unitarias, pero esa es su medida clave.

Entonces, si partes de su código se pueden probar como un todo sin afectar otras partes, entonces haga eso.

Siempre sea pragmático y recuerde que todo es un compromiso. Cuanto más se adhiera a DRY, más estrechamente debe estar su código. Cuanto más introduzcas abstracciones, más fácil será probar el código, pero más difícil será de entender. Evite la ideología y encuentre un buen equilibrio entre el ideal y mantenerlo simple. Ahí radica el punto óptimo de máxima eficiencia tanto para el desarrollo como para el mantenimiento.

David Arno
fuente
27
Me gustaría agregar que surge un dolor de cabeza similar cuando las personas intentan adherirse al mantra demasiado repetido de "los métodos solo deben hacer una cosa" y terminan con toneladas de métodos de una sola línea solo porque técnicamente se puede convertir en un método .
Logarr
8
Re "Siempre se pragmático y recuerda que todo es un compromiso" : los discípulos del tío Bob no son conocidos por esto (sin importar la intención original).
Peter Mortensen
13
Para resumir la primera parte, generalmente tiene un pasante de café, no un conjunto completo de percolador enchufable, interruptor de encendido, verificar si el azúcar necesita rellenar, abrir el refrigerador, sacar la leche, obtener -cucharas, tazas para bajar, servir café, agregar azúcar, agregar leche, agitar la taza y entregar la taza de pasantes. ; P
Justin Time 2 Restablece a Monica el
12
La causa raíz del problema del OP parece ser la incomprensión de la diferencia entre las funciones que deberían realizar una sola tarea y las clases que deberían tener una única responsabilidad.
alephzero
66
"Las reglas son para la guía de los sabios y la obediencia de los tontos". - Douglas Bader
Calanus
29

¡Las tareas que solían tomar de 5 a 10 archivos ahora pueden tomar de 70 a 100!

Esto es lo opuesto al principio de responsabilidad única (SRP). Para llegar a ese punto, debe haber dividido su funcionalidad de una manera muy fina, pero de eso no se trata el SRP: hacer eso ignora la idea clave de la cohesión .

Según el SRP, el software debe dividirse en módulos a lo largo de líneas definidas por sus posibles razones para cambiar, de modo que se pueda aplicar un solo cambio de diseño en un solo módulo sin requerir modificaciones en otro lugar. Un solo "módulo" en este sentido puede corresponder a más de una clase, pero si un cambio requiere que toque decenas de archivos, entonces es realmente múltiples cambios o está haciendo mal SRP.

Bob Martin, quien originalmente formuló el SRP, escribió una publicación de blog hace unos años para tratar de aclarar la situación. Discute con cierta extensión cuál es una "razón para cambiar" a los efectos del SRP. Vale la pena leerlo en su totalidad, pero entre las cosas que merecen especial atención se encuentra esta redacción alternativa del SRP:

Reúna las cosas que cambian por las mismas razones . Separe las cosas que cambian por diferentes razones.

(énfasis mío). El SRP no se trata de dividir las cosas en las piezas más pequeñas posibles. Ese no es un buen diseño, y su equipo tiene razón para resistir. Hace que su base de código sea más difícil de actualizar y mantener. Parece que puede estar tratando de vender a su equipo en base a consideraciones de pruebas unitarias, pero eso sería poner el carro antes que el caballo.

Del mismo modo, el principio de segregación de interfaz no debe tomarse como un absoluto. No es más una razón para dividir su código tan finamente que el SRP, y generalmente se alinea bastante bien con el SRP. Que una interfaz contenga algunos métodos que algunos clientes no usan no es una razón para dividirla. De nuevo estás buscando cohesión.

Además, le insto a que no tome el principio abierto-cerrado o el principio de sustitución de Liskov como una razón para favorecer las jerarquías de herencia profundas. No hay un acoplamiento más apretado que una subclase con sus superclases, y el acoplamiento apretado es un problema de diseño. En cambio, favorezca la composición sobre la herencia donde sea que tenga sentido hacerlo. Esto reducirá su acoplamiento y, por lo tanto, la cantidad de archivos que un cambio en particular puede necesitar tocar, y se alinea muy bien con la inversión de dependencia.

John Bollinger
fuente
1
Supongo que solo estoy tratando de averiguar dónde está la línea. En una tarea reciente, tuve que realizar una operación bastante simple, pero estaba en una base de código sin mucho andamiaje o funcionalidad existente. Como tal, todo lo que necesitaba hacer era muy simple, pero todo bastante único y no parecía encajar en clases compartidas. En mi caso, necesitaba guardar un documento en una unidad de red y registrarlo en dos tablas de base de datos separadas. Las reglas que rodean cada paso eran bastante particulares. Incluso la generación de nombre de archivo (un guid simple) tenía algunas clases para hacer las pruebas más convenientes.
JD Davis
3
Nuevamente, @JDDavis, elegir múltiples clases en lugar de una sola para fines de prueba es poner el carro delante del caballo, y va directamente en contra del SRP, que requiere agrupar funcionalidades cohesivas. No puedo aconsejarle sobre detalles, pero el problema de que los cambios funcionales individuales requieren modificar muchos archivos es un problema que debe abordar (e intentar evitar), no uno que deba tratar de justificar.
John Bollinger
De acuerdo, agrego esto. Para citar Wikipedia, "Martin define una responsabilidad como una razón para cambiar, y concluye que una clase o módulo debe tener una, y solo una, razón para ser cambiada (es decir, reescrita)". y "declaró más recientemente" Este principio es sobre las personas "." De hecho, creo que esto significa que la "responsabilidad" en SRP se refiere a las partes interesadas, no a la funcionalidad. Una clase debe ser responsable de los cambios requeridos por una sola parte interesada (persona que requiere que cambie su programa), para que cambie lo MÁS POCOS posibles en respuesta a las diferentes partes interesadas que exigen un cambio.
Corrodias
12

¡Las tareas que solían tomar de 5 a 10 archivos ahora pueden tomar de 70 a 100!

Esto es una mentira. Las tareas nunca tomaron solo 5-10 archivos.

No está resolviendo ninguna tarea con menos de 10 archivos. ¿Por qué? Porque estás usando C #. C # es un lenguaje de alto nivel. Estás utilizando más de 10 archivos solo para crear hello world.

Oh, seguro que no los notas porque no los escribiste. Entonces no los miras. Confías en ellos

El problema no es la cantidad de archivos. Es que ahora tienes tantas cosas que no confías.

Entonces, descubra cómo hacer que esas pruebas funcionen hasta el punto de que una vez que pasen, confíe en estos archivos de la misma manera que confía en los archivos en .NET. Hacer eso es el punto de las pruebas unitarias. A nadie le importa la cantidad de archivos. Se preocupan por la cantidad de cosas en las que no pueden confiar.

Para aplicaciones de tamaño pequeño a mediano, SOLID es una venta súper fácil. Todos ven el beneficio y la facilidad de mantenimiento. Sin embargo, simplemente no están viendo una buena propuesta de valor para SOLID en aplicaciones a gran escala.

El cambio es difícil en aplicaciones a gran escala, sin importar lo que haga. La mejor sabiduría para aplicar aquí no proviene del tío Bob. Proviene de Michael Feathers en su libro Working Effectively with Legacy Code.

No comience un festival de reescritura. El antiguo código representa el conocimiento ganado con esfuerzo. Lanzarlo porque tiene problemas y no se expresa en un nuevo y mejorado paradigma X es solo pedir un nuevo conjunto de problemas y ningún conocimiento ganado con esfuerzo.

En su lugar, encuentre formas de hacer que su código antiguo no comprobable sea verificable (el código heredado en Feathers habla). En esta metáfora el código es como una camisa. Las piezas grandes se unen en costuras naturales que se pueden deshacer para separar el código de la forma en que las quitaría. Haga esto para permitirle adjuntar "fundas" de prueba que le permitan aislar el resto del código. Ahora, cuando crea las mangas de prueba, tiene confianza en las mangas porque lo hizo con una camisa de trabajo. (Ahora, esta metáfora está empezando a doler).

Esta idea surge del supuesto de que, como en la mayoría de las tiendas, los únicos requisitos actualizados están en el código de trabajo. Esto le permite bloquear eso en pruebas que le permiten realizar cambios en el código de trabajo comprobado sin que pierda cada parte de su estado de trabajo comprobado. Ahora, con esta primera ola de pruebas en su lugar, puede comenzar a hacer cambios que hacen que el código "heredado" (no comprobable) sea verificable. Puede ser audaz porque las pruebas de costuras lo respaldan al decir que esto es lo que siempre hizo y las nuevas pruebas muestran que su código realmente hace lo que cree que hace.

¿Qué tiene que ver todo esto con:

¿Administrar y organizar el número masivo de clases después de cambiar a SOLID?

Abstracción.

Puedes hacerme odiar cualquier código base con malas abstracciones. Una mala abstracción es algo que me hace mirar dentro. No me sorprendas cuando miro dentro. Sé más o menos lo que esperaba.

Dame un buen nombre, pruebas legibles (ejemplos) que muestren cómo usar la interfaz y organízala para que pueda encontrar cosas y no me importará si usamos 10, 100 o 1000 archivos.

Me ayudas a encontrar cosas con buenos nombres descriptivos. Poner cosas con buenos nombres en cosas con buenos nombres.

Si hace todo esto correctamente, abstraerá los archivos donde tiene que terminar una tarea, dependiendo de otros 3 a 5 archivos. Los archivos 70-100 todavía están allí. Pero se esconden detrás del 3 al 5. Eso solo funciona si confías en el 3 al 5 para hacerlo bien.

Entonces, lo que realmente necesita es el vocabulario para encontrar buenos nombres para todas estas cosas y pruebas en las que las personas confían para que dejen de leer todo. Sin eso, también me estarías volviendo loco.

@Delioth hace un buen punto sobre los dolores de crecimiento. Cuando estás acostumbrado a que los platos se encuentren en el armario sobre el lavavajillas, es necesario acostumbrarse a que estén encima de la barra de desayuno. Hace algunas cosas más difíciles. Hace algunas cosas más fáciles. Pero causa todo tipo de pesadillas si la gente no está de acuerdo a dónde van los platos. En una base de código grande, el problema es que solo puede mover algunos de los platos a la vez. Así que ahora tienes platos en dos lugares. Es confuso. Hace que sea difícil confiar en que los platos están donde se supone que deben estar. Si quieres superar esto, lo único que debes hacer es seguir moviendo los platos.

El problema con eso es que realmente te gustaría saber si vale la pena tomar los platos en la barra de desayuno antes de pasar por todas estas tonterías. Bueno, para eso todo lo que puedo recomendar es ir de campamento.

Al probar un nuevo paradigma por primera vez, el último lugar donde debería aplicarlo es en una base de código grande. Esto va para cada miembro del equipo. Nadie debería creer que SOLID funciona, que OOP funciona o que la programación funcional funciona. Cada miembro del equipo debe tener la oportunidad de jugar con la nueva idea, sea cual sea, en un proyecto de juguete. Les permite ver al menos cómo funciona. Les permite ver lo que no hace bien. Les permite aprender a hacerlo justo antes de hacer un gran desastre.

Darles a las personas un lugar seguro para jugar les ayudará a adoptar nuevas ideas y les dará la confianza de que los platos realmente podrían funcionar en su nuevo hogar.

naranja confitada
fuente
3
Vale la pena mencionar que parte del dolor de la pregunta probablemente también sea un dolor creciente, mientras que, sí, es posible que necesiten hacer 15 archivos para esta única cosa ... ahora nunca tienen que volver a escribir un GUIDProvider o un BasePathProvider , o un ExtensionProvider, etc. Es el mismo tipo de obstáculo que obtienes cuando comienzas un nuevo proyecto greenfield: grupos de características de soporte que son en su mayoría triviales, estúpidas para escribir, y aún así necesitan ser escritas. Apesta construirlos, pero una vez que estén allí, no deberías tener que pensar en ellos ... nunca.
Delioth
@Delioth Estoy increíblemente inclinado a creer que este es el caso. Anteriormente, si necesitábamos un subconjunto de funcionalidades (digamos que simplemente queríamos una URL alojada en AppSettings), simplemente teníamos una clase masiva que se pasaba y se usaba. Con el nuevo enfoque, no hay razón para pasar la totalidad AppSettingssolo para obtener una URL o ruta de archivo.
JD Davis
1
No comience un festival de reescritura. El antiguo código representa el conocimiento ganado con esfuerzo. Lanzarlo porque tiene problemas y no se expresa en un nuevo y mejorado paradigma X es solo pedir un nuevo conjunto de problemas y ningún conocimiento ganado con esfuerzo. Esta. Absolutamente.
Flot2011
10

Parece que su código no está muy bien desacoplado y / o los tamaños de sus tareas son demasiado grandes.

Los cambios de código deben ser de 5 a 10 archivos, a menos que esté haciendo una coodod o refactorización a gran escala. Si un solo cambio toca muchos archivos, probablemente signifique que sus cambios caen en cascada. Algunas abstracciones mejoradas (más responsabilidad individual, segregación de interfaz, inversión de dependencia) deberían ayudar. También es posible que tal vez se fue demasiado sola responsabilidad y podría utilizar un poco más el pragmatismo - más corto y más delgadas jerarquías de tipos. Eso también debería hacer que el código sea más fácil de entender, ya que no tiene que comprender docenas de archivos para saber qué está haciendo el código.

También podría ser una señal de que tu trabajo es demasiado grande. En lugar de "hey, agregue esta función" (que requiere cambios en la interfaz de usuario y cambios en la API y cambios en el acceso a datos y cambios en la seguridad y cambios en las pruebas y ...) divídalos en partes más útiles. Eso se vuelve más fácil de revisar y más fácil de entender porque requiere que establezca contratos decentes entre los bits.

Y, por supuesto, las pruebas unitarias ayudan a todo esto. Te obligan a hacer interfaces decentes. Te obligan a hacer que tu código sea lo suficientemente flexible como para inyectar los bits necesarios para probar (si es difícil de probar, será difícil de reutilizar). Y alejan a las personas de las cosas de ingeniería excesiva porque cuanto más ingenieras, más necesitas probar.

Telastyn
fuente
2
Los archivos 5-10 a 70-100 archivos son un poco más que hipotéticos. Mi última tarea fue crear alguna funcionalidad en uno de nuestros microservicios más nuevos. Se suponía que el nuevo servicio recibiría una solicitud y guardaría un documento. Al hacerlo, necesitaba clases para representar las entidades de usuario en 2 bases de datos y repositorios separados para cada uno. Repos para representar otras tablas en las que necesitaba escribir. Clases dedicadas para manejar la verificación de datos de archivos y la generación de nombres. Y la lista continúa. Sin mencionar que cada clase que contenía lógica estaba representada por una interfaz, por lo que se podía imitar para pruebas unitarias.
JD Davis
1
En cuanto a nuestras bases de código más antiguas, todas están estrechamente acopladas e increíblemente monolíticas. Con el enfoque SOLID, el único acoplamiento entre clases ha sido en el caso de POCO, todo lo demás se pasa a través de DI e interfaces.
JD Davis
3
@JDDavis: espera, ¿por qué un microservicio funciona directamente con varias bases de datos?
Telastyn
1
Fue un compromiso con nuestro gerente de desarrollo. Él prefiere masivamente el software monolítico y de procedimiento. Como tal, nuestros microservicios son mucho más macro de lo que deberían ser. A medida que nuestra infraestructura mejora, lentamente las cosas pasarán a sus propios microservicios. Por ahora estamos siguiendo el enfoque de estrangulador para mover cierta funcionalidad a microservicios. Como los servicios múltiples necesitan acceso a un recurso específico, también los estamos trasladando a sus propios microservicios.
JD Davis
4

Me gustaría exponer algunas de las cosas ya mencionadas aquí, pero más desde una perspectiva de dónde se dibujan los límites de los objetos. Si está siguiendo algo similar al diseño impulsado por dominio, entonces sus objetos probablemente representarán aspectos de su negocio. Customery Order, por ejemplo, serían objetos. Ahora, si tuviera que adivinar en función de los nombres de clase que tenía como punto de partida, su AccountLogicclase tenía un código que se ejecutaría para cualquier cuenta. En OO, sin embargo, cada clase debe tener contexto e identidad. No debe obtener un Accountobjeto y luego pasarlo a una AccountLogicclase y hacer que esa clase realice cambios en el Accountobjeto. Eso es lo que se llama un modelo anémico, y no representa muy bien a OO. En cambio, tuAccountla clase debería tener un comportamiento, como Account.Close()o Account.UpdateEmail(), y esos comportamientos afectarían solo esa instancia de la cuenta.

Ahora, CÓMO se manejan estos comportamientos puede (y en muchos casos debería) descargarse en dependencias representadas por abstracciones (es decir, interfaces). Account.UpdateEmail, por ejemplo, podría querer actualizar una base de datos o un archivo, o enviar un mensaje a un bus de servicio, etc. Y eso podría cambiar en el futuro. Por lo tanto, su Accountclase puede depender, por ejemplo, de una IEmailUpdate, que podría ser una de las muchas interfaces implementadas por un AccountRepositoryobjeto. No querría pasar una IAccountRepositoryinterfaz completa al Accountobjeto porque probablemente haría demasiado, como buscar y encontrar otras (cualquiera) cuentas, a las que puede que no desee Accountque tenga acceso el objeto, pero a pesar de que AccountRepositorypodría implementar ambas IAccountRepositorye IEmailUpdateinterfaces, elAccountEl objeto solo tendría acceso a las pequeñas porciones que necesita. Esto le ayuda a mantener el Principio de segregación de interfaz .

Siendo realistas, como han mencionado otras personas, si se trata de una explosión de clases, lo más probable es que esté utilizando el principio SOLID (y, por extensión, OO) de la manera incorrecta. SOLID debería ayudarlo a simplificar su código, no complicarlo. Pero lleva tiempo entender realmente qué significan cosas como el SRP. Sin embargo, lo más importante es que el funcionamiento de SOLID dependerá mucho de su dominio y contextos limitados (otro término DDD). No hay bala de plata ni talla única.

Una cosa más que me gusta enfatizar a las personas con las que trabajo: una vez más, un objeto OOP debe tener comportamiento y, de hecho, está definido por su comportamiento, no por sus datos. Si su objeto no tiene más que propiedades y campos, todavía tiene comportamiento, aunque probablemente no sea el comportamiento que pretendía. Una propiedad públicamente editable / configurable sin otra lógica establecida implica que el comportamiento de su clase que contiene es que cualquier persona en cualquier lugar por cualquier razón y en cualquier momento puede modificar el valor de esa propiedad sin ninguna lógica comercial o validación necesaria. Por lo general, ese no es el comportamiento que la gente pretende, pero si tienes un modelo anémico, generalmente es el comportamiento que tus clases anuncian a cualquiera que los use.

Laos
fuente
2

Entonces, hay un total de 15 clases (excluyendo POCO y andamios) para realizar un ahorro bastante sencillo.

Eso es una locura ... pero estas clases suenan como algo que yo mismo escribiría. Así que echemos un vistazo a ellos. Ignoremos las interfaces y pruebas por ahora.

  • BasePathProvider- En mi humilde opinión, cualquier proyecto no trivial que trabaje con archivos lo necesita. Así que supongo que ya existe tal cosa y puede usarla como está.
  • UniqueFilenameProvider - Claro, ya lo tienes, ¿no?
  • NewGuidProvider - El mismo caso, a menos que solo estés usando GUID.
  • FileExtensionCombiner - El mismo caso.
  • PatientFileWriter - Supongo que esta es la clase principal para la tarea actual.

Para mí, se ve bien: debe escribir una nueva clase que necesite cuatro clases auxiliares. Las cuatro clases de ayuda parecen bastante reutilizables, por lo que apuesto a que ya están en algún lugar de su código base. De lo contrario, es mala suerte (¿eres realmente la persona de tu equipo para escribir archivos y usar GUID?) O algún otro problema.


Con respecto a las clases de prueba, seguro, cuando crea una nueva clase o la actualiza, debe probarse. Así que escribir cinco clases también significa escribir cinco clases de prueba. Pero esto no hace que el diseño sea más complicado:

  • Nunca usará las clases de prueba en otro lugar, ya que se ejecutarán automáticamente y eso es todo.
  • Desea volver a verlos, a menos que actualice las clases bajo prueba o que las use como documentación (idealmente, las pruebas muestran claramente cómo se supone que se debe usar una clase).

Con respecto a las interfaces, solo son necesarias cuando su marco DI o su marco de prueba no pueden manejar clases. Puede verlos como un peaje para herramientas imperfectas. O puede verlos como una abstracción útil que le permite olvidar que hay cosas más complicadas: leer la fuente de una interfaz lleva mucho menos tiempo que leer la fuente de su implementación.

maaartinus
fuente
Estoy agradecido por este punto de vista. En este caso específico, estaba escribiendo funcionalidad en un microservicio bastante nuevo. Desafortunadamente, incluso en nuestra base de código principal, si bien tenemos algunos de los anteriores en uso, ninguno de ellos es realmente de forma remotamente reutilizable. Todo lo que necesita ser reutilizable terminó en una clase estática o simplemente se copia y se pega alrededor del código. Creo que todavía he ido un poco lejos, pero estoy de acuerdo en que no todo necesita ser completamente diseccionado y desacoplado.
JD Davis
@JDDavis Estaba tratando de escribir algo diferente de las otras respuestas (con lo que estoy mayormente de acuerdo). Cada vez que copia y pega algo, está evitando la reutilización, ya que en lugar de generalizar algo, crea otra pieza de código no reutilizable, que lo obligará a copiar y pegar más algún día. En mi humilde opinión, es el segundo pecado más grande, justo después de seguir ciegamente las reglas. Necesita encontrar su punto óptimo, donde seguir las reglas lo hace más productivo (especialmente en los futuros cambios) y ocasionalmente romperlos un poco ayuda en casos en los que el esfuerzo no sería inapropiado. Todo es relativo.
maaartinus
@JDDavis Y todo depende de la calidad de sus herramientas. Ejemplo: hay personas que afirman que la DI es empresarial y complicada, mientras que yo afirmo que es principalmente gratuita . +++Con respecto a romper las reglas: hay cuatro clases que necesito en lugares, donde solo podría inyectarlas después de una refactorización importante que hace que el código sea más feo (al menos para mis ojos), así que decidí convertirlos en singletons (un mejor programador podría encontrar una mejor manera, pero estoy contento con eso; el número de estos singletons no ha cambiado desde hace años).
maaartinus
Esta respuesta expresa más o menos lo que estaba pensando cuando el OP agregó el ejemplo a la pregunta. @JDDavis Permítanme agregar que pueden guardar algunos códigos / clases repetitivos utilizando herramientas funcionales para los casos simples. Un proveedor de GUI, por ejemplo, en lugar de introducir una nueva interfaz para una nueva clase para esto, ¿por qué no utilizarlo Func<Guid>para esto e inyectar un método anónimo como ()=>Guid.NewGuid()en el constructor? Y no hay necesidad de probar esta función .Net framework, esto es algo que Microsoft ha hecho por usted. En total, esto te ahorrará 4 clases.
Doc Brown
... y debe verificar si los otros casos que presentó pueden simplificarse de la misma manera (probablemente no todos).
Doc Brown
2

Dependiendo de las abstracciones, crear clases de responsabilidad única y escribir pruebas unitarias no son ciencias exactas. Es perfectamente normal balancearse demasiado en una dirección cuando se aprende, ir al extremo y luego encontrar una norma que tenga sentido. Parece que su péndulo se ha movido demasiado, e incluso podría estar atascado.

Aquí es donde sospecho que esto está saliendo de los rieles:

Las pruebas unitarias han sido increíblemente difíciles de vender para el equipo, ya que todos creen que es una pérdida de tiempo y que pueden manejar y probar su código mucho más rápido en conjunto que cada pieza individualmente. El uso de pruebas unitarias como un aval para SOLID ha sido en gran parte inútil y se ha convertido en una broma en este punto.

Uno de los beneficios que proviene de la mayoría de los principios SÓLIDOS (ciertamente no es el único beneficio) es que facilita la escritura de pruebas unitarias para nuestro código. Si una clase depende de abstracciones podemos burlarnos de las abstracciones. Las abstracciones segregadas son más fáciles de burlar. Si una clase hace una cosa, es probable que tenga una menor complejidad, lo que significa que es más fácil conocer y probar todas sus rutas posibles.

Si su equipo no está escribiendo pruebas unitarias, están sucediendo dos cosas relacionadas:

Primero, están haciendo mucho trabajo adicional para crear todas estas interfaces y clases sin darse cuenta de todos los beneficios. Se necesita un poco de tiempo y práctica para ver cómo escribir pruebas unitarias nos facilita la vida. Hay razones por las cuales las personas que aprenden a escribir pruebas unitarias se adhieren a él, pero hay que persistir el tiempo suficiente para descubrirlas por sí mismo. Si su equipo no está intentando eso, sentirán que el resto del trabajo extra que están haciendo es inútil.

Por ejemplo, ¿qué sucede cuando necesitan refactorizar? Si tienen cien clases pequeñas pero no hay pruebas que les indiquen si sus cambios funcionarán o no, esas clases e interfaces adicionales parecerán una carga, no una mejora.

En segundo lugar, escribir pruebas unitarias puede ayudarlo a comprender cuánta abstracción realmente necesita su código. Como dije, no es una ciencia. Comenzamos mal, viramos por todo el lugar y mejoramos. Las pruebas unitarias tienen una forma peculiar de complementar SOLID. ¿Cómo sabes cuándo necesitas agregar una abstracción o separar algo? En otras palabras, ¿cómo sabes cuándo estás "suficientemente SÓLIDO"? A menudo la respuesta es cuando no puedes probar algo.

Tal vez su código sea comprobable sin crear tantas pequeñas abstracciones y clases. Pero si no estás escribiendo las pruebas, ¿cómo puedes saberlo? ¿Cuán lejos llegamos? Podemos obsesionarnos con dividir las cosas cada vez más pequeñas. Es una madriguera de conejo. La capacidad de escribir pruebas para nuestro código nos ayuda a ver cuándo hemos cumplido nuestro propósito para que podamos dejar de obsesionarnos, seguir adelante y divertirnos escribiendo más código.

Las pruebas unitarias no son una bala de plata que resuelve todo, pero son una bala realmente increíble que mejora la vida de los desarrolladores. No somos perfectos, y tampoco lo son nuestras pruebas. Pero las pruebas nos dan confianza. Esperamos que nuestro código sea correcto y nos sorprende cuando está mal, no al revés. No somos perfectos y nuestras pruebas tampoco. Pero cuando se prueba nuestro código, tenemos confianza. Es menos probable que nos muerdamos las uñas cuando se implementa nuestro código y nos preguntamos qué es lo que se romperá esta vez y si será culpa nuestra.

Además de eso, una vez que lo entendemos, escribir pruebas unitarias hace que el desarrollo del código sea más rápido, no más lento. Pasamos menos tiempo revisando código viejo o depurando para encontrar problemas que son como agujas en un pajar.

Los errores disminuyen, hacemos más y reemplazamos la ansiedad con confianza. No es una moda o aceite de serpiente. Es real. Muchos desarrolladores darán fe de esto. Si su equipo no ha experimentado esto, deben superar esa curva de aprendizaje y superar el obstáculo. Dale una oportunidad, dándote cuenta de que no obtendrán resultados al instante. Pero cuando sucede, se alegrarán de haberlo hecho y nunca mirarán hacia atrás. (O se convertirán en parias aislados y escribirán publicaciones de blog enojadas sobre cómo las pruebas unitarias y la mayoría de los otros conocimientos de programación acumulados son una pérdida de tiempo).

Desde que realizó el cambio, una de las mayores quejas de los desarrolladores es que no pueden soportar la revisión por pares y atravesar docenas y docenas de archivos donde anteriormente cada tarea solo requería que el desarrollador tocara de 5 a 10 archivos.

La revisión por pares es mucho más fácil cuando pasan todas las pruebas unitarias y una gran parte de esa revisión es solo asegurarse de que las pruebas sean significativas.

Scott Hannen
fuente