Разделение служебного проекта «пачка вещей» на отдельные компоненты с «необязательными» зависимостями

26

За годы использования C # / .NET для множества собственных проектов у нас была одна библиотека, органически растущая в одну огромную пачку вещей. Он называется «Утил», и я уверен, что многие из вас видели одного из этих зверей в своей карьере.

Многие части этой библиотеки очень автономны и могут быть разделены на отдельные проекты (которые мы хотели бы с открытым исходным кодом). Но есть одна важная проблема, которую необходимо решить, прежде чем их можно будет выпускать в виде отдельных библиотек. По сути, существует множество случаев, которые я мог бы назвать «необязательными зависимостями» между этими библиотеками.

Чтобы лучше это объяснить, рассмотрим некоторые из модулей, которые являются хорошими кандидатами на то, чтобы стать автономными библиотеками. CommandLineParserдля анализа командных строк. XmlClassifyдля сериализации классов в XML. PostBuildCheckвыполняет проверку скомпилированной сборки и сообщает об ошибке компиляции, если она не удалась. ConsoleColoredStringбиблиотека для цветных строковых литералов Lingoдля перевода пользовательских интерфейсов.

Каждая из этих библиотек может использоваться полностью автономно, но если они используются вместе, то есть полезные дополнительные функции, которые необходимо иметь. Например, оба CommandLineParserи XmlClassifyвыставляют функциональность проверки после сборки, которая требует PostBuildCheck. Точно так же, CommandLineParserопция позволяет предоставлять документацию опций с использованием цветных строковых литералов, требующих ConsoleColoredString, и поддерживает переводимую документацию через Lingo.

Таким образом, ключевое отличие заключается в том, что это дополнительные функции . Можно использовать синтаксический анализатор командной строки с простыми неокрашенными строками, не переводя документацию и не выполняя никаких проверок после сборки. Или можно сделать документацию переводимой, но все же неокрашенной. Или как цветной, так и переводимый. И т.п.

Просматривая эту библиотеку «Util», я вижу, что почти все потенциально разделяемые библиотеки имеют такие дополнительные функции, которые связывают их с другими библиотеками. Если бы я на самом деле требовал, чтобы эти библиотеки были зависимостями, тогда эта пачка вещей на самом деле совсем не распутана: вам все равно потребуются все библиотеки, если вы хотите использовать только одну.

Существуют ли установленные подходы к управлению такими необязательными зависимостями в .NET?

Роман Старков
источник
2
Даже если библиотеки зависят друг от друга, все же может быть некоторое преимущество в разделении их на последовательные, но отдельные библиотеки, каждая из которых содержит широкую категорию функций.
Роберт Харви

Ответы:

20

Рефакторинг Медленно.

Ожидайте, что это займет некоторое время , и может произойти в течение нескольких итераций, прежде чем вы сможете полностью удалить свою сборку Utils .

Общий подход:

  1. Сначала уделите немного времени и подумайте, как вы хотите, чтобы эти сборки утилит выглядели, когда вы закончите. Не беспокойтесь о существующем коде, подумайте о конечной цели. Например, вы можете захотеть иметь:

    • MyCompany.Utilities.Core (Содержит алгоритмы, ведение журнала и т. Д.)
    • MyCompany.Utilities.UI (рисование кода и т. Д.)
    • MyCompany.Utilities.UI.WinForms (код, связанный с System.Windows.Forms, пользовательские элементы управления и т. Д.)
    • MyCompany.Utilities.UI.WPF (код, связанный с WPF, базовые классы MVVM).
    • MyCompany.Utilities.Serialization (Сериализация кода).
  2. Создайте пустые проекты для каждого из этих проектов и создайте соответствующие ссылки на проекты (ссылки на пользовательский интерфейс Core, UI.WinForms ссылки на пользовательский интерфейс) и т. Д.

  3. Переместите любой низко висящий фрукт (классы или методы, которые не страдают от проблем с зависимостями) из вашей сборки Utils в новые целевые сборки.

  4. Получить копию NDepend и Мартина Фаулера рефакторинга , чтобы начать анализировать ваши Utils сборки , чтобы начать работу на более жесткие из них. Две техники, которые будут полезны:

Обработка дополнительных интерфейсов

Либо сборка ссылается на другую сборку, либо нет. Единственный другой способ использования функциональных возможностей в несвязанной сборке - через интерфейс, загруженный посредством отражения от общего класса. Недостатком этого является то, что ваша сборка ядра должна будет содержать интерфейсы для всех общих функций, но плюсом является то, что вы можете развертывать свои утилиты по мере необходимости без «пачки» DLL-файлов в зависимости от каждого сценария развертывания. Вот как я бы обработал этот случай, используя цветную строку в качестве примера:

  1. Сначала определите общие интерфейсы в вашей базовой сборке:

    введите описание изображения здесь

    Например, IStringColorerинтерфейс будет выглядеть так:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. Затем внедрите интерфейс в сборку с функцией. Например, StringColorerкласс будет выглядеть так:

    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. Создайте PluginFinder(или, возможно, InterfaceFinder - более подходящее имя в данном случае) класс, который может находить интерфейсы из файлов DLL в текущей папке. Вот упрощенный пример. По совету @ EdWoodcock (и я согласен), когда ваши проекты растут, я бы предложил использовать одну из доступных платформ Dependency Injection (на мой взгляд, Common Serivce Locator с Unity и Spring.NET ) для более надежной реализации с более продвинутыми «найди меня» эта функция "возможности, иначе известный как шаблон локатора службы . Вы можете изменить его в соответствии с вашими потребностями.

    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. Наконец, используйте эти интерфейсы в других ваших сборках, вызвав метод FindInterface. Вот пример 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;
        }
    

    }

НАИБОЛЕЕ ВАЖНО: Тестируйте, тестируйте, тестируйте между каждым изменением.

Кевин Маккормик
источник
Я добавил пример! :-)
Кевин Маккормик
1
Этот класс PluginFinder выглядит подозрительно, как автоматический DI-обработчик «по кругу» (используя шаблон ServiceLocator), но в остальном это хороший совет. Может быть, вам было бы лучше просто указать OP на что-то вроде Unity, поскольку у него не было бы проблем с несколькими реализациями определенного интерфейса в библиотеках (StringColourer против StringColourerWithHtmlWrapper или чего-либо еще).
Эд Джеймс
@ EdWoodcock Хорошо, Эд, и я не могу поверить, что я не думал о шаблоне Service Locator при написании этого. PluginFinder определенно является незрелой реализацией, и DI-фреймворк, безусловно, будет работать здесь
Кевин Маккормик
Я наградил вас за эти усилия, но мы не собираемся идти по этому пути. Совместное использование базовой сборки интерфейсов означает, что нам удалось только отодвинуть реализации, но все еще есть библиотека, которая содержит несколько мало связанных интерфейсов (связанных, как и прежде, через необязательные зависимости). Настройка теперь намного сложнее, но для таких маленьких библиотек это мало что дает. Дополнительная сложность может стоить того для огромных проектов, но не для них.
Роман Старков
@romkyns Так по какому маршруту ты идешь? Оставить как есть? :)
Макс
5

Вы можете использовать интерфейсы, объявленные в дополнительной библиотеке.

Попробуйте разрешить контракт (класс через интерфейс), используя внедрение зависимостей (MEF, Unity и т. Д.). Если не найден, установите его для возврата нулевого экземпляра.
Затем проверьте, является ли экземпляр нулевым, и в этом случае вы не выполняете дополнительные функции.

Это особенно легко сделать с помощью MEF, поскольку это учебник для него.

Это позволит вам скомпилировать библиотеки за счет разделения их на n + 1 dll.

НТН.

Луи Коттманн
источник
Это звучит почти правильно - если бы не эта дополнительная DLL, которая в основном похожа на скелеты оригинальной пачки вещей. Все реализации разделены, но все еще остается «кусок скелетов». Я предполагаю, что у этого есть некоторые преимущества, но я не уверен, что преимущества перевешивают все затраты для этого конкретного набора библиотек ...
Роман Старков
Кроме того, включение всего фреймворка - это шаг назад; эта библиотека «как есть» имеет размер одной из этих платформ, что полностью сводит на нет преимущества. Во всяком случае, я просто хотел бы немного подумать, чтобы увидеть, доступна ли реализация, поскольку может быть только от нуля до единицы, и внешняя конфигурация не требуется.
Роман Старков
2

Я думал, что опубликую самый жизнеспособный вариант, который мы придумали до сих пор, чтобы увидеть, что мы думаем.

По сути, мы разделили бы каждый компонент в библиотеку с нулевыми ссылками; весь код, который требует ссылки, будет помещен в #if/#endifблок с соответствующим именем. Например, код в CommandLineParserэтом дескрипторе ConsoleColoredStrings будет помещен в #if HAS_CONSOLE_COLORED_STRING.

Любое решение, которое хочет включить только, CommandLineParserможет легко сделать это, так как больше нет никаких зависимостей. Однако, если решение также включает ConsoleColoredStringпроект, у программиста теперь есть возможность:

  • добавить ссылку CommandLineParserнаConsoleColoredString
  • добавить HAS_CONSOLE_COLORED_STRINGопределение в CommandLineParserфайл проекта.

Это сделало бы соответствующую функциональность доступной.

Есть несколько проблем с этим:

  • Это только исходное решение; каждый потребитель библиотеки должен включить его в качестве исходного кода; они не могут просто включать двоичный файл (но это не является абсолютным требованием для нас).
  • Библиотека файл проект библиотеки получает несколько решений Определённых правок, и это не совсем понятно , как это изменение стремятся к SCM.

Скорее не красиво, но все же, это самое близкое, что мы придумали.

Еще одна идея, которую мы рассмотрели, заключалась в том, чтобы использовать конфигурации проекта, а не требовать от пользователя редактирования файла проекта библиотеки. Но это абсолютно неосуществимо в VS2010, потому что он добавляет все конфигурации проекта в решение нежелательно .

Роман Старков
источник
1

Я собираюсь порекомендовать книгу Разработка приложений Brownfield в .Net . Две непосредственно соответствующие главы - это 8 и 9. Глава 8 рассказывает о ретрансляции вашего приложения, а глава 9 рассказывает о приручении зависимостей, инверсии управления и влиянии, которое это оказывает на тестирование.

Tangurena
источник
1

Полное раскрытие, я парень из Java. Так что я понимаю, что вы, вероятно, не ищете технологии, о которых я здесь упомяну. Но проблемы одинаковы, поэтому, возможно, это укажет вам правильное направление.

В Java существует ряд систем сборки, которые поддерживают идею централизованного хранилища артефактов, в котором хранятся встроенные «артефакты» - насколько мне известно, это несколько аналогично GAC в .NET (пожалуйста, извините мое невежество, если это напряженная анология) но более того, потому что он используется для создания независимых повторяемых сборок в любой момент времени.

В любом случае, другой поддерживаемой функцией (например, в Maven) является идея НЕОБЯЗАТЕЛЬНОЙ зависимости, которая затем зависит от конкретных версий или диапазонов и потенциально исключает переходные зависимости. Это звучит для меня как то, что вы ищете, но я могу ошибаться. Взгляните на эту вводную страницу по управлению зависимостями от Maven с другом, который знает Java, и посмотрите, кажутся ли проблемы знакомыми. Это позволит вам построить ваше приложение и создать его с доступностью этих зависимостей или без нее.

Существуют также конструкции, если вам нужна действительно динамическая подключаемая архитектура; Одной из технологий, которая пытается решить эту форму разрешения зависимостей во время выполнения, является OSGI. Это двигатель системы плагинов Eclipse . Вы увидите, что он может поддерживать необязательные зависимости и минимальный / максимальный диапазон версий. Этот уровень модульности времени исполнения накладывает множество ограничений на вас и то, как вы развиваетесь. Большинство людей могут обойтись той степенью модульности, которую обеспечивает Maven.

Еще одна возможная идея, которую вы могли бы рассмотреть, которая может быть проще для вас реализовать, - это использовать стиль архитектуры Pipes and Filters. Это во многом то, что сделало UNIX такой давней, успешной экосистемой, которая выжила и развивалась в течение полувека. Взгляните на эту статью о трубах и фильтрах в .NET, чтобы узнать, как реализовать этот тип шаблона в вашей среде.

cwash
источник
0

Возможно, книга Джона Лакоса «Проектирование крупномасштабного программного обеспечения на C ++» полезна (конечно, C # и C ++ или не одно и то же, но вы можете извлечь полезные приемы из этой книги).

По сути, перефакторинг и перемещение функций, использующих две или более библиотек, в отдельный компонент, который зависит от этих библиотек. При необходимости используйте такие методы, как непрозрачные типы и т. Д.

Каспер ван ден Берг
источник