Separar un proyecto de utilidad "fajo de cosas" en componentes individuales con dependencias "opcionales"

26

A lo largo de los años de usar C # / .NET para un montón de proyectos internos, hemos tenido una biblioteca que creció orgánicamente en un gran fajo de cosas. Se llama "Util", y estoy seguro de que muchos de ustedes han visto una de estas bestias en sus carreras.

Muchas partes de esta biblioteca son muy independientes y podrían dividirse en proyectos separados (que nos gustaría abrir en código fuente). Pero hay un problema importante que debe resolverse antes de que puedan lanzarse como bibliotecas separadas. Básicamente, hay muchos casos de lo que podría llamar "dependencias opcionales" entre estas bibliotecas.

Para explicar esto mejor, considere algunos de los módulos que son buenos candidatos para convertirse en bibliotecas independientes. CommandLineParseres para analizar líneas de comando. XmlClassifyes para serializar clases a XML. PostBuildCheckrealiza comprobaciones en el ensamblado compilado e informa un error de compilación si fallan. ConsoleColoredStringes una biblioteca para literales de cadena de colores. Lingoes para traducir interfaces de usuario.

Cada una de esas bibliotecas se puede usar de manera completamente independiente, pero si se usan juntas, entonces hay características adicionales útiles que se pueden tener. Por ejemplo, ambos CommandLineParsery XmlClassifyexponen la funcionalidad de comprobación posterior a la compilación, que requiere PostBuildCheck. Del mismo modo, CommandLineParserpermite que se proporcione la documentación de la opción utilizando los literales de cadena de color, lo que requiere ConsoleColoredString, y admite documentación traducible a través de Lingo.

Entonces, la distinción clave es que estas son características opcionales . Se puede usar un analizador de línea de comandos con cadenas simples y sin color, sin traducir la documentación ni realizar ninguna comprobación posterior a la compilación. O uno podría hacer que la documentación sea traducible pero aún sin color. O tanto de color como traducible. Etc.

Al mirar a través de esta biblioteca "Util", veo que casi todas las bibliotecas potencialmente separables tienen características opcionales que las vinculan a otras bibliotecas. Si realmente requiriera esas bibliotecas como dependencias, este fajo de cosas no está realmente desenredado en absoluto: básicamente necesitaría todas las bibliotecas si desea usar solo una.

¿Existen enfoques establecidos para administrar tales dependencias opcionales en .NET?

Roman Starkov
fuente
2
Incluso si las bibliotecas dependen unas de otras, aún podría haber algún beneficio en separarlas en bibliotecas coherentes pero separadas, cada una con una amplia categoría de funcionalidad.
Robert Harvey

Respuestas:

20

Refactorizar lentamente.

Espere que esto tarde un tiempo en completarse , y puede ocurrir en varias iteraciones antes de que pueda eliminar por completo su ensamblaje de Utils .

Enfoque global:

  1. Primero tómese un tiempo y piense cómo quiere que se vean estos conjuntos de utilidades cuando haya terminado. No se preocupe demasiado por su código existente, piense en el objetivo final. Por ejemplo, es posible que desee tener:

    • MyCompany.Utilities.Core (que contiene algoritmos, registros, etc.)
    • MyCompany.Utilities.UI (código de dibujo, etc.)
    • MyCompany.Utilities.UI.WinForms (Código relacionado con System.Windows.Forms, controles personalizados, etc.)
    • MyCompany.Utilities.UI.WPF (código relacionado con WPF, clases base MVVM).
    • MyCompany.Utilities.Serialization (código de serialización).
  2. Cree proyectos vacíos para cada uno de estos proyectos y cree referencias de proyecto apropiadas (referencias de UI Core, UI.WinForms referencias UI), etc.

  3. Mueva cualquiera de las frutas bajas (clases o métodos que no sufren los problemas de dependencia) de su ensamblaje de Utils a los nuevos ensamblajes de destino.

  4. Obtenga una copia de NDepend y Martin Fowler's Refactoring para comenzar a analizar su ensamblaje de Utils para comenzar a trabajar en los más difíciles. Dos técnicas que serán útiles:

    • En muchos casos, necesitará Invertir el control a través de interfaces, delegados o eventos .
    • En el caso de sus dependencias "opcionales" , consulte a continuación.

Manejo de interfaces opcionales

O un ensamblaje hace referencia a otro ensamblaje, o no lo hace. La única otra forma de usar la funcionalidad en un ensamblaje no vinculado es a través de una interfaz cargada mediante la reflexión de una clase común. La desventaja de esto es que su ensamblaje central necesitará contener interfaces para todas las características compartidas, pero la ventaja es que puede implementar sus utilidades según sea necesario sin el "fajo" de archivos DLL dependiendo de cada escenario de implementación. Así es como manejaría este caso, usando la cadena de color como ejemplo:

  1. Primero, defina las interfaces comunes en su ensamblaje central:

    ingrese la descripción de la imagen aquí

    Por ejemplo, la IStringColorerinterfaz se vería así:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. Luego, implemente la interfaz en el ensamblaje con la función. Por ejemplo, la StringColorerclase se vería así:

    using MyCompany.Utilities.Core.OptionalInterfaces;
    namespace MyCompany.Utilities.Console
    {
        class StringColorer : IStringColorer
        {
            #region IStringColorer Members
    
            public string Decorate(string s)
            {
                return "*" + s + "*";   //TODO: implement coloring
            }
    
            #endregion
        }
    }
    
  3. Crear un PluginFinder clase (o quizás InterfaceFinder es un nombre mejor en este caso) que puede encontrar interfaces de archivos DLL en la carpeta actual. Aquí hay un ejemplo simplista. Según el consejo de @ EdWoodcock (y estoy de acuerdo), cuando sus proyectos crezcan, sugeriría utilizar uno de los marcos de inyección de dependencia disponibles ( Common Serivce Locator con Unity y Spring.NET ) para una implementación más robusta con más "encuéntreme" esa característica "capacidades, también conocida como el patrón de localización de servicios . Puede modificarlo para satisfacer sus necesidades.

    using System;
    using System.Linq;
    using System.IO;
    using System.Reflection;
    
    namespace UtilitiesCore
    {
        public static class PluginFinder
        {
            private static bool _loadedAssemblies;
    
            public static T FindInterface<T>() where T : class
            {
                if (!_loadedAssemblies)
                    LoadAssemblies();
    
                //TODO: improve the performance vastly by caching RuntimeTypeHandles
    
                foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    foreach (Type type in assembly.GetTypes())
                    {
                        if (type.IsClass && typeof(T).IsAssignableFrom(type))
                            return Activator.CreateInstance(type) as T;
                    }
                }
    
                return null;
            }
    
            private static void LoadAssemblies()
            {
                foreach (FileInfo file in new DirectoryInfo(Directory.GetCurrentDirectory()).GetFiles())
                {
                    if (file.Extension != ".DLL")
                        continue;
    
                    if (!AppDomain.CurrentDomain.GetAssemblies().Any(a => a.Location == file.FullName))
                    {
                        try
                        {
                            //TODO: perhaps filter by certain known names
                            Assembly.LoadFrom(file.FullName);
                        }
                        catch { }
                    }
                }
            }
        }
    }
    
  4. Por último, use estas interfaces en sus otros ensamblados llamando al método FindInterface. Aquí hay un ejemplo de CommandLineParser:

    static class CommandLineParser
    {
        public static string ParseCommandLine(string commandLine)
        {
            string parsedCommandLine = ParseInternal(commandLine);
    
            IStringColorer colorer = PluginFinder.FindInterface<IStringColorer>();
    
            if(colorer != null)
                parsedCommandLine = colorer.Decorate(parsedCommandLine);
    
            return parsedCommandLine;
        }
    
        private static string ParseInternal(string commandLine)
        {
            //TODO: implement parsing as desired
            return commandLine;
        }
    

    }

MÁS IMPORTANTE: Prueba, prueba, prueba entre cada cambio.

Kevin McCormick
fuente
¡Agregué el ejemplo! :-)
Kevin McCormick
1
Esa clase de PluginFinder se parece sospechosamente a un controlador DI automático automotriz (usando un patrón ServiceLocator), pero esto es un buen consejo. Tal vez sería mejor apuntar el OP a algo como Unity, ya que eso no tendría problemas con múltiples implementaciones de una interfaz particular dentro de las bibliotecas (StringColourer vs StringColourerWithHtmlWrapper, o lo que sea).
Ed James
@EdWoodcock Buen punto Ed, y no puedo creer que no haya pensado en el patrón del Localizador de servicios mientras escribía esto. El PluginFinder es definitivamente una implementación inmadura y un marco DI ciertamente funcionaría aquí.
Kevin McCormick
Le he otorgado la recompensa por el esfuerzo, pero no vamos a seguir esta ruta. Compartir un conjunto central de interfaces significa que solo logramos alejar las implementaciones, pero todavía hay una biblioteca que contiene un fajo de interfaces poco relacionadas (relacionadas a través de dependencias opcionales, como antes). La configuración es mucho más complicada ahora con pocos beneficios para bibliotecas tan pequeñas como esta. La complejidad adicional podría valer la pena para proyectos enormes, pero no estos.
Roman Starkov
@romkyns Entonces, ¿qué ruta estás tomando? ¿Dejándolo como está? :)
Max
5

Puede utilizar las interfaces declaradas en una biblioteca adicional.

Intente resolver un contrato (clase a través de la interfaz) utilizando una inyección de dependencia (MEF, Unity, etc.). Si no se encuentra, configúrelo para que devuelva una instancia nula.
Luego verifique si la instancia es nula, en cuyo caso no realiza las funcionalidades adicionales.

Esto es especialmente fácil de hacer con MEF, ya que es el uso del libro de texto para ello.

Le permitiría compilar las bibliotecas, a costa de dividirlas en n + 1 dlls.

HTH

Louis Kottmann
fuente
Esto suena casi correcto, si no fuera por esa DLL adicional, que es básicamente como un montón de esqueletos del fajo original de cosas. Las implementaciones están todas divididas, pero todavía queda un "fajo de esqueletos". Supongo que tiene algunas ventajas, pero no estoy convencido de que las ventajas superen todos los costos de este conjunto particular de bibliotecas ...
Roman Starkov
Además, incluir un marco completo es totalmente un paso atrás; esta biblioteca tal cual es aproximadamente del tamaño de uno de esos marcos, negando totalmente el beneficio. En todo caso, solo usaría un poco de reflexión para ver si hay una implementación disponible, ya que solo puede haber entre cero y uno, y no se requiere configuración externa.
Roman Starkov
2

Pensé en publicar la opción más viable que se nos haya ocurrido hasta ahora, para ver cuáles son los pensamientos.

Básicamente, separaríamos cada componente en una biblioteca con cero referencias; Todo el código que requiere una referencia se colocará en un #if/#endifbloque con el nombre apropiado. Por ejemplo, el código CommandLineParserque maneja ConsoleColoredStrings se colocaría en #if HAS_CONSOLE_COLORED_STRING.

Cualquier solución que desee incluir solo CommandLineParserpuede hacerlo fácilmente, ya que no hay más dependencias. Sin embargo, si la solución también incluye el ConsoleColoredStringproyecto, el programador ahora tiene la opción de:

  • agregar una referencia CommandLineParseraConsoleColoredString
  • agregue la HAS_CONSOLE_COLORED_STRINGdefinición al CommandLineParserarchivo del proyecto.

Esto haría que la funcionalidad relevante estuviera disponible.

Hay varios problemas con esto:

  • Esta es una solución de solo fuente; cada consumidor de la biblioteca debe incluirlo como código fuente; no pueden incluir solo un binario (pero esto no es un requisito absoluto para nosotros).
  • La biblioteca de archivo de proyecto de la biblioteca consigue un par de soluciones ediciones específicas de, y no es exactamente claro cómo este cambio se ha comprometido a SMC.

Más bien poco bonito, pero aún así, esto es lo más cercano que hemos encontrado.

Otra idea que consideramos fue usar configuraciones de proyecto en lugar de requerir que el usuario edite el archivo de proyecto de la biblioteca. Pero esto es absolutamente inviable en VS2010 porque agrega todas las configuraciones de proyecto a la solución de forma no deseada .

Roman Starkov
fuente
1

Voy a recomendar el libro Brownfield Application Development en .Net . Dos capítulos directamente relevantes son 8 y 9. El capítulo 8 habla sobre la retransmisión de su aplicación, mientras que el capítulo 9 habla sobre la dependencia de la domesticación, la inversión del control y el impacto que esto tiene en las pruebas.

Tangurena
fuente
1

Revelación completa, soy un chico de Java. Así que entiendo que probablemente no estés buscando las tecnologías que mencionaré aquí. Pero los problemas son los mismos, por lo que tal vez te indique la dirección correcta.

En Java, hay una serie de sistemas de compilación que respaldan la idea de un repositorio de artefactos centralizado que alberga "artefactos" construidos, que yo sepa, esto es algo análogo al GAC en .NET (perdone mi ignorancia si es una anaología tensa) pero más que eso porque se usa para producir compilaciones repetibles independientes en cualquier momento.

De todos modos, otra característica que es compatible (en Maven, por ejemplo) es la idea de una dependencia OPCIONAL, que luego depende de versiones o rangos específicos y potencialmente excluye las dependencias transitivas. Esto me parece lo que estás buscando, pero podría estar equivocado. Eche un vistazo a esta página de introducción sobre la gestión de dependencias de Maven con un amigo que conoce Java y vea si los problemas le resultan familiares. Esto le permitirá construir su aplicación y construirla con o sin tener estas dependencias disponibles.

También hay construcciones si necesita una arquitectura verdaderamente dinámica y conectable; Una tecnología que intenta abordar esta forma de resolución de dependencia de tiempo de ejecución es OSGI. Este es el motor detrás del sistema de complementos de Eclipse . Verá que puede admitir dependencias opcionales y un rango de versión mínimo / máximo. Este nivel de modularidad en tiempo de ejecución le impone una buena cantidad de restricciones y cómo se desarrolla. La mayoría de las personas pueden sobrevivir con el grado de modularidad que ofrece Maven.

Otra posible idea que podría considerar que podría ser un orden de magnitud más simple de implementar para usted es utilizar un estilo de arquitectura de Tuberías y Filtros. Esto es en gran medida lo que ha convertido a UNIX en un ecosistema exitoso y de larga data que ha sobrevivido y evolucionado durante medio siglo. Eche un vistazo a este artículo sobre Tuberías y filtros en .NET para obtener algunas ideas sobre cómo implementar este tipo de patrón en su marco.

cwash
fuente
0

Quizás el libro "Diseño de software C ++ a gran escala" de John Lakos sea útil (por supuesto, C # y C ++ o no es lo mismo, pero puede extraer técnicas útiles del libro).

Básicamente, vuelva a factorizar y mueva la funcionalidad que usa dos o más bibliotecas a un componente separado que depende de estas bibliotecas. Si es necesario, utilice técnicas como tipos opacos, etc.

Kasper van den Berg
fuente