c# dependency-injection refactoring dependencies code-injection

c# - Refactorización para DI en grandes proyectos.



dependency-injection refactoring (7)

Trabajo en un proyecto de plataforma a gran escala que admite alrededor de 10 productos que utilizan nuestro código.

Hasta ahora, todos los productos han estado utilizando la funcionalidad completa de nuestra plataforma:
- Recuperación de datos de configuración de una base de datos.
- Acceso remoto al sistema de archivos.
- autorización de seguridad
- Lógica base (lo que nos pagan por ofrecer).

Para un nuevo producto se nos ha pedido que sea compatible con un subconjunto más pequeño de funcionalidad sin la infraestructura que traen las plataformas. Nuestra arquitectura es antigua (inicio de la codificación a partir de 2005 aproximadamente) pero razonablemente sólida.

Estamos seguros de que podemos hacerlo utilizando DI en nuestras clases existentes, pero los tiempos estimados para hacerlo varían de 5 a 70 semanas, dependiendo de con quién hable.

Hay muchos artículos por ahí que te dicen cómo hacer DI, pero ¿no puedo encontrar ninguno que te diga cómo refactorizar la DI de la manera más eficiente? ¿Hay herramientas que hacen esto en lugar de tener que pasar por 30,000 líneas de código y tener que presionar CTRL + R para extactar interfaces y agregarlas a los constructores muchas veces? (tenemos resharper si eso ayuda) Si no, ¿cuál es el flujo de trabajo ideal para lograrlo rápidamente?


Este libro probablemente sería muy útil:

Trabajando eficazmente con el código heredado - Michael C. Feathers - http://www.amazon.com/gp/product/0131177052

Yo sugeriría comenzar con pequeños cambios. Poco a poco mueva dependencias para ser inyectadas a través del constructor. Siempre mantenga el sistema en funcionamiento. Extraiga interfaces de las dependencias inyectadas del constructor y comience a ajustar con pruebas unitarias. Traer herramientas cuando tenga sentido. No tienes que empezar a usar marcos de inyección de dependencia y burla de inmediato. Puede realizar muchas mejoras mediante la inyección manual de dependencias a través del constructor.


Gracias por todas las respuestas. Ya casi hemos pasado un año y creo que casi puedo responder a mi propia pregunta.

Por supuesto, solo convertimos las partes de nuestra plataforma que debían reutilizarse en el nuevo producto, como señala Lasseeskildsen. Dado que esto fue solo una conversión parcial del código base, optamos por el enfoque DIY para la inyección de dependencia.

Nuestro enfoque era hacer que estas partes estuvieran disponibles sin traer dependencias no deseadas, no para permitir la prueba de unidades de ellas. Esto hace una diferencia en la forma en que aborda el problema. No hay cambios de diseño reales en este caso.

El trabajo involucrado es mundano, de ahí la cuestión de cómo hacerlo de forma rápida o incluso automática. La respuesta es que no se puede automatizar, pero con algunos métodos abreviados de teclado y con la función de re-intercambio se puede hacer bastante rápido. Para mí este es el flujo óptimo:

  1. Trabajamos a través de múltiples soluciones. Creamos una solución "maestra" temporal que contiene todos los proyectos en todos los archivos de soluciones. Aunque las herramientas de refactorización no siempre son lo suficientemente inteligentes como para detectar la diferencia entre referencias binarias y de proyectos, al menos esto las hará funcionar parcialmente en múltiples soluciones.

  2. Crea una lista de todas las dependencias que necesitas cortar. Agruparlos por función. En la mayoría de los casos, pudimos abordar múltiples dependencias relacionadas a la vez.

  3. Estarás haciendo muchos pequeños cambios de código en muchos archivos. Esta tarea la realiza mejor un solo desarrollador, o dos a lo sumo para evitar tener que fusionar constantemente sus cambios.

  4. Deshágase de los singletons primero: después de convertirlos fuera de este patrón, extraiga una interfaz (resharper -> refactor -> extract interface) Elimine el acceso singleton para obtener una lista de errores de compilación. En el paso 6.

  5. Para deshacerse de otras referencias: a. Extraiga la interfaz como se indica arriba. segundo. Comenta la implementación original. Esto te da una lista de errores de compilación.

  6. Resharper se convierte en una gran ayuda ahora:

    • Alt + shift + pg abajo / arriba rápidamente navega por referencias rotas.
    • Si varias referencias comparten una clase base común, navegue hasta su constructor y presione ctrl + r + s ("cambiar la firma del método") para agregar la nueva interfaz al constructor. Resharper 8 le ofrece una opción para "resolver por árbol de llamadas", lo que significa que puede hacer que las clases heredadas cambien su firma automáticamente. Esta es una característica muy buena (parece nuevo en la versión 8).
    • En el cuerpo del constructor, asigne la interfaz inyectada a una propiedad no existente. Pulsa Alt + Intro para seleccionar "Crear propiedad", muévelo a donde debe estar y listo. Descomenta el código de 5b.
  7. ¡Prueba! Enjuague y repita.

Para hacer uso de estas clases en la solución original sin cambios importantes en el código, creamos constructores sobrecargados que recuperan sus dependencias a través de un localizador de servicios, como menciona Brett Veenstra. Esto puede ser un anti-patrón, pero funciona para este escenario. No se eliminará hasta que todo el código sea compatible con DI.

Convirtimos aproximadamente una cuarta parte de nuestro código a DI de esta manera en aproximadamente 2-3 semanas (1,5 personas). Un año más, y ahora estamos cambiando todo nuestro código a DI. Esta es una situación diferente a medida que el enfoque cambia a la capacidad de prueba de la unidad. Creo que los pasos generales anteriores aún funcionarán, pero esto requiere algunos cambios de diseño reales para hacer cumplir el SOC.


La forma en que me acerco a una conversión es mirar cualquier parte del sistema que modifique permanentemente el estado; Archivos, base de datos, contenido externo. Una vez cambiado y releído, ¿ha cambiado para siempre? Este es el primer lugar para buscar cambiarlo.

Entonces, lo primero que debes hacer es buscar un lugar que modifique una fuente como esta:

class MyXmlFileWriter { public bool WriteData(string fileName, string xmlText) { // TODO: Sort out exception handling try { File.WriteAllText(fileName, xmlText); return true; } catch(Exception ex) { return false; } } }

En segundo lugar, escribe una prueba de unidad para asegurarte de que no estás rompiendo el código al refactorizar.

[TestClass] class MyXmlWriterTests { [TestMethod] public void WriteData_WithValidFileAndContent_ExpectTrue() { var target = new MyXmlFileWriter(); var filePath = Path.GetTempFile(); target.WriteData(filePath, "<Xml/>"); Assert.IsTrue(File.Exists(filePath)); } // TODO: Check other cases }

A continuación, Extraiga una interfaz de la clase original:

interface IFileWriter { bool WriteData(string location, string content); } class MyXmlFileWriter : IFileWriter { /* As before */ }

Vuelva a ejecutar las pruebas y espero que todo sea bueno. Mantenga la prueba original ya que está verificando sus trabajos de implementación anteriores.

A continuación escribe una implementación falsa que no hace nada. Solo queremos implementar un comportamiento muy básico aquí.

// Put this class in the test suite, not the main project class FakeFileWriter : IFileWriter { internal bool WriteDataCalled { get; private set; } public bool WriteData(string file, string content) { this.WriteDataCalled = true; return true; } }

Entonces la unidad lo prueba ...

class FakeFileWriterTests { private IFileWriter writer; [TestInitialize()] public void Initialize() { writer = new FakeFileWriter(); } [TestMethod] public void WriteData_WhenCalled_ExpectSuccess() { writer.WriteData(null,null); Assert.IsTrue(writer.WriteDataCalled); } }

Ahora, con la unidad probada y las versiones refactorizadas aún funcionan, debemos asegurarnos de que cuando se inyecta, la clase que llama está utilizando la interfaz, ¡no la versión concreta!

// Before class FileRepository { public FileRepository() { } public void Save( string content, string xml ) { var writer = new MyXmlFileWriter(); writer.WriteData(content,xml); } } // After class FileRepository { private IFileWriter writer = null; public FileRepository() : this( new MyXmlFileWriter() ){ } public FileRepository(IFileWriter writer) { this.writer = writer; } public void Save( string path, string xml) { this.writer.WriteData(path, xml); } }

entonces, ¿qué hicimos?

  • Tener un constructor predeterminado que use el tipo normal.
  • Tener un constructor que tome un tipo IFileWriter
  • Utilizó un campo de instancia para contener el objeto referenciado.

Entonces es un caso de escribir una prueba unitaria para FileRepository y verificar que el método se llame:

[TestClass] class FileRepositoryTests { private FileRepository repository = null; [TestInitialize()] public void Initialize() { this.repository = new FileRepository( new FakeFileWriter() ); } [TestMethod] public void WriteData_WhenCalled_ExpectSuccess() { // Arrange var target = repository; // Act var actual = repository.Save(null,null); // Assert Assert.IsTrue(actual); } }

Bien, pero aquí, ¿estamos realmente probando FileRepository o FakeFileWriter ? Estamos probando el FileRepository ya que nuestras otras pruebas están probando el FakeFileWriter separado. Esta clase: FileRepositoryTests sería más útil para probar los parámetros entrantes en busca de valores nulos.

Lo falso no es hacer nada inteligente, sin validación de parámetros, sin E / S. Simplemente está sentado para que FileRepository pueda guardar contenido en cualquier trabajo. Su propósito es doble; Para acelerar significativamente las pruebas unitarias y no interrumpir el estado de un sistema.

Si este FileRepository también tenía que leer el archivo, también podría implementar un IFileReader (que es un poco extremo), o simplemente almacenar la última ruta de archivo / xml escrita en una cadena en la memoria y recuperarla en su lugar.

Entonces, con lo básico una vez más, ¿cómo enfocas esto?

En un proyecto grande, que requiere mucha refactorización, siempre es mejor incorporar pruebas de unidad a cualquier clase que sufra un cambio de DI. En teoría, sus datos no deben comprometerse en cientos de ubicaciones [dentro de su código], sino que deben pasar a través de algunas ubicaciones clave. Localízalos en el código y agrega una interfaz para ellos. Un truco que he usado es ocultar cada DB o fuente similar a un índice detrás de una interfaz como esta:

interface IReadOnlyRepository<TKey, TValue> { TValue Retrieve(TKey key); } interface IRepository<TKey, TValue> : IReadOnlyRepository<TKey, TValue> { void Create(TKey key, TValue value); void Update(TKey key, TValue); void Delete(TKey key); }

Lo que lo configura para recuperar de fuentes de datos de una manera muy genérica. Puede cambiar de un XmlRepository a DbRepository reemplazando solo el lugar donde se inyecta. Esto puede ser extremadamente útil para un proyecto que migra de una fuente de datos a otra sin afectar las partes internas de un sistema. Puede ser muy difícil cambiar la manipulación de XML para usar objetos, pero es mucho más fácil mantener e implementar una nueva funcionalidad con este enfoque.

El único otro consejo que puedo dar es hacer 1 fuente de datos a la vez y seguir. Resiste la tentación de hacer demasiados a la vez. Si realmente terminas teniendo que guardar en archivos, DB y servicio web de un solo golpe, utiliza la interfaz de extracción, falsifica las llamadas y no devuelve nada. Es un verdadero acto de malabarismo hacer muchas cosas de una sola vez, pero puedes volver a encajarlas más fácilmente que a partir de los primeros principios.

¡Buena suerte!


Lo que describiste es una gran parte del proceso; Pasando por cada clase, creamos una interfaz y la registramos. Esto es más problemático si se compromete de inmediato a refactorizar la raíz de la composición, en el caso de MVC, eso significaría suponer que iba a inyectar en el controlador.

Eso podría ser una gran cantidad de trabajo y si el código hace mucha creación directa de objetos, podría ser muy complejo tratar de hacerlo todo al mismo tiempo. En estos casos, creo que es aceptable usar el patrón Localizador de servicios y resolver manualmente las llamadas.

Comience por reemplazar algunas de sus llamadas directas a los constructores con un localizador de servicios para resolver llamadas. Esto reducirá la cantidad de refactorización inicialmente necesaria y comenzará a brindarle los beneficios de DI.

Con el tiempo, sus llamadas se acercarán cada vez más a la raíz de la composición y luego podrá comenzar a eliminar el uso del localizador de servicios.


No pienses que hay alguna herramienta para hacer esta conversión de código.

Porque ->

El uso de DI en la base de código existente implicaría,

  • Uso de la interfaz / clase abstracta. De nuevo, aquí debe tomarse el chioce correcto para facilitar la conversión teniendo en cuenta la funcionalidad del principio y el código DI.

  • Segregación / unificación efectiva de las clases existentes en clases múltiples / individuales para mantener el código modular o pequeñas unidades reajustables.


Preguntaste sobre herramientas. Una herramienta que podría ayudar en una refactorización grande como esta es nDepend . Lo he usado para ayudar a identificar lugares para orientar los esfuerzos de refactorización.

Dudo en mencionarlo, porque no quiero dar la impresión de que una herramienta como nDepend es necesaria para emprender este proyecto. Sin embargo, es útil visualizar las dependencias en su base de código. Viene con una prueba totalmente funcional de 14 días, que podría ser suficiente para sus necesidades.


Supongo que desea utilizar una herramienta IoC como StructureMap, Funq, Ninject, etc.

En ese caso, el trabajo de refactorización realmente comienza con la actualización de sus puntos de entrada (o Raíces de composición ) en el código base. Esto podría tener un gran impacto, especialmente si está haciendo un uso generalizado de la estática y administrando la vida útil de sus objetos (por ejemplo, almacenamiento en caché, cargas perezosas). Una vez que tiene una herramienta IoC en su lugar y que transmite los gráficos de objetos, puede comenzar a extender su uso de DI y disfrutar de los beneficios.

Primero me concentraría en dependencias similares a configuraciones (que deberían ser simples objetos de valor) y comenzaría a hacer llamadas de resolución con su herramienta IoC. A continuación, busque crear clases de Factory e inyectelas para administrar la vida útil de sus objetos. Sentirás que vas hacia atrás (y lento) hasta que alcances la cresta donde la mayoría de tus objetos están usando DI, y corolaramente SRP, desde allí debería ser cuesta abajo. Una vez que tenga una mejor separación de preocupaciones, la flexibilidad de su base de código y la velocidad a la que puede hacer cambios aumentarán dramáticamente.

Una advertencia: no se dejen engañar al pensar en rociar un "Localizador de servicios" en todas partes es su panacea, en realidad es un antipatrón DI . Creo que necesitará usar esto al principio, pero luego debe terminar el trabajo de DI con inyecciones de constructor o de definidor y eliminar el Localizador de servicios.