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.Now
y 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.