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


26

За роки використання C # /. NET для ряду внутрішніх проектів у нас була одна бібліотека, яка органічно зростала в один величезний пакет матеріалів. Це називається "Util", і я впевнений, що багато хто з вас бачили одного з цих звірів у своїй кар'єрі.

Багато частин цієї бібліотеки дуже автономні, і їх можна розділити на окремі проекти (які ми хотіли б відкрити). Але є одна основна проблема, яку потрібно вирішити, перш ніж їх можна випустити як окремі бібліотеки. В основному, між цими бібліотеками є багато і багато випадків того, що я можу назвати "необов'язковими залежностями" .

Щоб пояснити це краще, розглянемо деякі модулі, які є хорошими кандидатами, щоб стати самостійними бібліотеками. 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 (і я згоден), коли ваші проекти зростають, я б запропонував використовувати одну з доступних рамок введення залежностей ( загальний локатор Serivce з 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), але це інакше є надійною порадою. Можливо, вам буде краще просто вказати на ОП щось на кшталт Unity, оскільки це не матиме проблем із багаторазовою реалізацією певного інтерфейсу в бібліотеках (StringColourer vs StringColourerWithHtmlWrapper чи інше).
Ед Джеймс

@EdWoodcock Добре, Ед, і я не можу повірити, що я не замислювався над шаблоном службового локатора, коли писав це. PluginFinder, безумовно, є незрілою реалізацією, і структура DI, безумовно, працює тут.
Кевін Маккормік

Я нагородив вас винагородою за зусилля, але ми не збираємося йти цим шляхом. Спільний доступ до основної збірки інтерфейсів означає, що нам вдалося лише відійти від реалізацій, але все ще є бібліотека, яка містить безліч взаємозалежних інтерфейсів (пов'язаних через необов'язкові залежності, як і раніше). Налаштування набагато складніше зараз з невеликою користю для менших бібліотек. Додаткова складність, можливо, варта того для гумористичних проектів, але не цих.
Роман Старков

@romkyns Отже, яким маршрутом ви їдете? Залишити це як є? :)
Макс

5

Ви можете використовувати інтерфейси, оголошені в додатковій бібліотеці.

Спробуйте вирішити договір (клас через інтерфейс), використовуючи ін'єкцію залежності (MEF, Unity тощо). Якщо його не знайдено, встановіть його, щоб повернути нульовий екземпляр.
Потім перевірте, чи примірник недійсний, і в цьому випадку ви не виконайте додаткових функцій.

Це особливо легко зробити з MEF, оскільки саме для цього використовується підручник.

Це дозволить вам складати бібліотеки, ціною їх поділу на n + 1 dlls.

HTH.


Це звучить майже правильно - якби не ця додаткова DLL, яка в основному схожа на купу скелетів оригінальної вати матеріалів. Реалізації всі розбиті, але все ще залишилося "кучу скелетів". Я вважаю, що це має деякі переваги, але я не переконаний, що переваги переважають усі витрати на цей конкретний набір бібліотек ...
Роман Старков

Крім того, включення цілої рамки - це повністю крок назад; ця бібліотека є приблизно розміром однієї з цих фреймворків, абсолютно не заперечуючи перевагу. Якщо що, я просто використав би трохи роздумів, щоб побачити, чи доступна реалізація, оскільки їх може бути лише між нулем та одиницею, а зовнішня конфігурація не потрібна.
Роман Старков

2

Я думав, що опублікую найбільш життєздатний варіант, який ми придумали поки що, щоб побачити, що думки.

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

Будь-яке рішення, яке бажає включити тільки те, CommandLineParserможе легко зробити це, оскільки немає ніяких залежностей. Однак якщо рішення також включає ConsoleColoredStringпроект, програміст тепер має можливість:

  • додати посилання в CommandLineParserдоConsoleColoredString
  • додайте HAS_CONSOLE_COLORED_STRINGвизначення до CommandLineParserфайлу проекту.

Це зробить доступною відповідну функціональність.

З цим існує кілька проблем:

  • Це лише джерело; кожен споживач бібліотеки повинен включати її як вихідний код; вони не можуть просто включати двійковий код (але це не є абсолютною вимогою для нас).
  • Бібліотека файл проект бібліотеки отримує кілька рішень Певних правок, і це не зовсім зрозуміло , як це зміна прагнуть до SCM.

Швидше не дуже, але все-таки це найближче, що ми придумали.

Ще однією ідеєю, яку ми розглядали, було використання конфігурацій проекту, а не вимагати від користувача редагування файлу проекту бібліотеки. Але це абсолютно не працює у VS2010, оскільки він додає всі налаштування проектів до рішення небажано .


1

Я порекомендую книгу « Brownfield Application Development» в .Net . Дві безпосередньо відповідні глави - це 8 та 9. Розділ 8 розповідає про ретрансляцію вашої програми, тоді як глава 9 розповідає про приборкання залежностей, інверсію контролю та вплив цього на тестування.


1

Повне розкриття, я хлопець на Java. Тож я розумію, що ви, мабуть, не шукаєте технологій, про які я згадаю тут. Але проблеми однакові, тому, можливо, це направить вас у правильному напрямку.

У Java існує ряд систем побудови, які підтримують ідею централізованого сховища артефактів, в якому розташовані побудовані "артефакти" - наскільки мені відомо, це дещо аналогічно GAC в .NET (будь ласка, виконайте моє незнання, якщо це напружена аналогія) але більше того, тому що він використовується для створення незалежних повторюваних конструкцій у будь-який момент часу.

У будь-якому випадку, інша функція, яка підтримується (наприклад, в Maven), - це ідея ОПЦІОНАЛЬНОЇ залежності, що залежить від конкретних версій або діапазонів і потенційно виключає транзитивні залежності. Це звучить мені як те, що ви шукаєте, але я можу помилитися. Подивіться на цю вступну сторінку про управління залежністю від Maven з другом, який знає Java, і перевірте, чи звучать проблеми знайомі. Це дозволить вам створити свою програму та сконструювати її з наявністю цих залежностей або без них.

Є також конструкції, якщо вам потрібна справді динамічна, підключається архітектура; однією з технологій, яка намагається вирішити цю форму вирішення залежності часу виконання, є OSGI. Це двигун системи плагінів Eclipse . Ви побачите, що він може підтримувати необов'язкові залежності та мінімальний / максимальний діапазон версій. Цей рівень модульності виконання виконує велику кількість обмежень для вас і того, як ви розвиваєтесь. Більшість людей можуть обійтись із ступенем модульності, який надає Maven.

Ще одна можлива ідея, яку ви могли б розглянути, що може бути на порядок простішим у здійсненні для вас, - це використовувати стиль архітектури Труби та фільтри. Це значною мірою те, що зробило UNIX такою багаторічною, успішною екосистемою, яка вижила і розвивалася протягом півстоліття. Погляньте на цю статтю про труби та фільтри в .NET, щоб ознайомитись із деякими ідеями про те, як реалізувати подібну схему у своїх рамках.


0

Можливо, книга "Масштабний дизайн програмного забезпечення на C ++" Джона Лакоса корисна (звичайно, C # і C ++ чи не те саме, але ви можете вилучити корисні методи з книги).

В основному перефактуруйте та перемістіть функціонал, який використовує дві або більше бібліотек, в окремий компонент, який залежить від цих бібліотек. Якщо потрібно, використовуйте такі методи, як непрозорі типи тощо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.