Rozdzielenie projektu narzędzia „wad of stuff” na poszczególne komponenty z zależnościami „opcjonalnymi”

26

Przez lata używania C # / .NET do wielu wewnętrznych projektów, jedna biblioteka rozwinęła się organicznie w jeden wielki zbiór rzeczy. Nazywa się to „Util” i jestem pewien, że wielu z was widziało jedną z tych bestii w swojej karierze.

Wiele części tej biblioteki jest bardzo samodzielnych i można je podzielić na osobne projekty (które chcielibyśmy otworzyć). Jest jednak jeden poważny problem, który należy rozwiązać, zanim zostaną one wydane jako osobne biblioteki. Zasadniczo istnieje wiele przypadków tego, co mogę nazwać „opcjonalnymi zależnościami” między tymi bibliotekami.

Aby to lepiej wyjaśnić, rozważ niektóre moduły, które są dobrymi kandydatami do zostania samodzielnymi bibliotekami. CommandLineParsersłuży do analizowania wierszy poleceń. XmlClassifysłuży do szeregowania klas do formatu XML. PostBuildCheckwykonuje sprawdzanie skompilowanego zestawu i zgłasza błąd kompilacji, jeśli się nie powiedzie. ConsoleColoredStringto biblioteka kolorowych literałów łańcuchowych. Lingosłuży do tłumaczenia interfejsów użytkownika.

Każda z tych bibliotek może być używana całkowicie autonomicznie, ale jeśli są one używane razem, istnieją przydatne dodatkowe funkcje. Na przykład, zarówno CommandLineParseri XmlClassifynarazić funkcjonalność sprawdzania post-build, który wymaga PostBuildCheck. Podobnie, CommandLineParserpozwala na dostarczenie dokumentacji opcji przy użyciu kolorowych literałów łańcuchowych, co jest wymagane ConsoleColoredString, i obsługuje dokumentację do przetłumaczenia przez Lingo.

Dlatego kluczowym rozróżnieniem jest to, że są to funkcje opcjonalne . Można użyć parsera wiersza poleceń z prostymi, bezbarwnymi łańcuchami, bez tłumaczenia dokumentacji lub wykonywania jakichkolwiek kontroli po kompilacji. Lub można sprawić, by dokument był przetłumaczalny, ale nadal bezbarwny. Lub zarówno kolorowe, jak i do przetłumaczenia. Itp.

Przeglądając tę ​​bibliotekę „Util”, widzę, że prawie wszystkie potencjalnie rozdzielne biblioteki mają takie opcjonalne funkcje, które wiążą je z innymi bibliotekami. Gdybym naprawdę potrzebował tych bibliotek jako zależności, to ten plik rzeczy nie jest wcale nieplątany: nadal potrzebujesz wszystkich bibliotek, jeśli chcesz użyć tylko jednej.

Czy istnieją ustalone podejścia do zarządzania takimi opcjonalnymi zależnościami w .NET?

Roman Starkov
źródło
2
Nawet jeśli biblioteki są od siebie zależne, rozdzielenie ich na spójne, ale oddzielne biblioteki może przynieść pewne korzyści, z których każda zawiera szeroką kategorię funkcjonalności.
Robert Harvey

Odpowiedzi:

20

Refaktoryzuj powoli.

Spodziewaj się, że wykonanie tego zajmie trochę czasu i może wystąpić w kilku iteracjach, zanim będzie można całkowicie usunąć zespół Utils .

Ogólne podejście:

  1. Najpierw poświęć trochę czasu i zastanów się, jak chcesz, aby te zestawy narzędzi wyglądały po zakończeniu. Nie przejmuj się zbytnio istniejącym kodem, pomyśl o celu końcowym. Na przykład możesz chcieć mieć:

    • MyCompany.Utilities.Core (zawierający algorytmy, rejestrowanie itp.)
    • MyCompany.Utilities.UI (kod rysunkowy itp.)
    • MyCompany.Utilities.UI.WinForms (kod związany z System.Windows.Forms, niestandardowe elementy sterujące itp.)
    • MyCompany.Utilities.UI.WPF (kod związany z WPF, klasy podstawowe MVVM).
    • MyCompany.Utilities.Serialization (kod serializacji).
  2. Utwórz puste projekty dla każdego z tych projektów i utwórz odpowiednie odwołania do projektu (odniesienia do interfejsu Core, UI.WinForms odniesienia do interfejsu użytkownika itp.)

  3. Przenieś dowolny nisko wiszący owoc (klasy lub metody, które nie mają problemów z zależnościami) z zestawu Utils do nowych zespołów docelowych.

  4. Zdobądź kopię NDepend i Martina Fowlera Refactoring, aby rozpocząć analizę zestawu Utils i rozpocząć pracę na trudniejszych. Dwie techniki, które będą pomocne:

Obsługa opcjonalnych interfejsów

Albo zespół odwołuje się do innego zestawu, albo nie. Jedynym innym sposobem użycia funkcji w niepowiązanym zestawie jest interfejs załadowany przez odbicie od wspólnej klasy. Wadą tego jest to, że zestaw podstawowy będzie musiał zawierać interfejsy dla wszystkich współużytkowanych funkcji, ale wadą jest to, że możesz wdrożyć narzędzia w razie potrzeby bez „wad” plików DLL w zależności od każdego scenariusza wdrażania. Oto jak poradziłbym sobie z tą sprawą, używając kolorowego sznurka jako przykładu:

  1. Najpierw zdefiniuj wspólne interfejsy w zestawie podstawowym:

    wprowadź opis zdjęcia tutaj

    Na przykład IStringColorerinterfejs wyglądałby następująco:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. Następnie zaimplementuj interfejs w zespole z funkcją. Na przykład StringColorerklasa wyglądałaby następująco:

    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. Utwórz PluginFinder(lub może w tym przypadku lepszą nazwę interfejsu), która może znaleźć interfejsy z plików DLL w bieżącym folderze. Oto uproszczony przykład. Zgodnie z radą Per @ EdWoodcock (i zgadzam się), gdy Twoje projekty będą rosły, sugerowałbym użycie jednego z dostępnych frameworków Dependency Injection ( przychodzi mi na myśl Common Serivce Locator z Unity i Spring.NET ) dla bardziej niezawodnej implementacji z bardziej zaawansowanym „znajdź mnie tej funkcji ”, znanej również jako wzorzec lokalizatora usług . Możesz go zmodyfikować zgodnie z własnymi potrzebami.

    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. Na koniec użyj tych interfejsów w innych zestawach, wywołując metodę FindInterface. Oto przykład 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;
        }
    

    }

NAJWAŻNIEJSZE: Testuj, testuj, testuj między każdą zmianą.

Kevin McCormick
źródło
Dodałem przykład! :-)
Kevin McCormick,
1
Ta klasa PluginFinder wygląda podejrzanie jak automatyczny moduł obsługi DI (wykorzystujący wzorzec ServiceLocator), ale poza tym jest to dobra rada. Być może lepiej byłoby po prostu wskazać OP na coś takiego jak Unity, ponieważ nie miałoby to problemów z wieloma implementacjami konkretnego interfejsu w bibliotekach (StringColourer vs. StringColourerWithHtmlWrapper lub cokolwiek innego).
Ed James
@EdWoodcock Dobra uwaga Ed, i nie mogę uwierzyć, że nie pomyślałem o wzorcu Lokalizatora usług podczas pisania tego. PluginFinder jest zdecydowanie niedojrzałą implementacją, a środowisko DI na pewno by tu działało.
Kevin McCormick,
Przyznałem ci nagrodę za wysiłek, ale nie zamierzamy iść tą drogą. Udostępnianie podstawowego zestawu interfejsów oznacza, że ​​udało nam się jedynie odejść od implementacji, ale wciąż istnieje biblioteka zawierająca pakiet mało powiązanych interfejsów (powiązanych opcjonalnie, jak poprzednio). Konfiguracja jest teraz znacznie bardziej skomplikowana, przy niewielkich korzyściach dla tak małych bibliotek. Dodatkowa złożoność może być tego warta w przypadku ogromnych projektów, ale nie tych.
Roman Starkov
@romkyns Więc jaką drogą wybierasz? Zostawiasz tak, jak jest? :)
Max
5

Możesz skorzystać z interfejsów zadeklarowanych w dodatkowej bibliotece.

Spróbuj rozwiązać kontrakt (klasa przez interfejs) przy użyciu wstrzykiwania zależności (MEF, Unity itp.). Jeśli nie zostanie znaleziony, ustaw go tak, aby zwracał instancję zerową.
Następnie sprawdź, czy instancja ma wartość NULL, w którym to przypadku nie wykonujesz dodatkowych funkcji.

Jest to szczególnie łatwe w MEF, ponieważ jest to podręcznik do tego celu.

Umożliwiłoby to skompilowanie bibliotek kosztem podziału ich na n + 1 bibliotek dll.

HTH.

Louis Kottmann
źródło
Brzmi to prawie poprawnie - gdyby nie jedna dodatkowa biblioteka DLL, która jest w zasadzie jak wiązka szkieletów oryginalnego pliku rzeczy. Wszystkie implementacje są podzielone, ale wciąż pozostaje „zwój szkieletów”. Przypuszczam, że ma to pewne zalety, ale nie jestem przekonany, że korzyści przewyższają wszystkie koszty związane z tym konkretnym zestawem bibliotek ...
Roman Starkov
Ponadto włączenie całego frameworka jest całkowicie krok wstecz; ta biblioteka w obecnym kształcie jest mniej więcej wielkości jednego z tych frameworków, całkowicie negując korzyści. Jeśli już, skorzystam z odrobiny refleksji, aby sprawdzić, czy implementacja jest dostępna, ponieważ może być tylko od zera do jednego, a konfiguracja zewnętrzna nie jest wymagana.
Roman Starkov
2

Myślałem, że opublikuję najbardziej opłacalną opcję, jaką do tej pory wymyśliliśmy, aby zobaczyć, jakie są myśli.

Zasadniczo oddzielilibyśmy każdy komponent do biblioteki z zerowymi referencjami; cały kod, który wymaga odwołania, zostanie umieszczony w #if/#endifbloku o odpowiedniej nazwie. Na przykład kod wCommandLineParser tych uchwytach ConsoleColoredStrings zostałby umieszczony w #if HAS_CONSOLE_COLORED_STRING.

Każde rozwiązanie, które chce zawierać tylko to, co CommandLineParsermożna łatwo zrobić, ponieważ nie ma dalszych zależności. Jeśli jednak rozwiązanie obejmuje również ConsoleColoredStringprojekt, programista ma teraz opcję:

  • dodaj odniesienie CommandLineParserdoConsoleColoredString
  • dodaj HAS_CONSOLE_COLORED_STRINGdefinicję do CommandLineParserpliku projektu.

Dzięki temu dostępna będzie odpowiednia funkcjonalność.

Jest z tym kilka problemów:

  • Jest to rozwiązanie tylko dla źródła; każdy konsument biblioteki musi dołączyć go jako kod źródłowy; nie mogą po prostu zawierać pliku binarnego (ale nie jest to dla nas absolutnym wymogiem).
  • Biblioteka plik projektu biblioteki dostaje kilka rozwiązań specyficznych dla edycji, a to nie jest dokładnie tak oczywiste, jak ta zmiana jest zaangażowana w SCM.

Raczej nie ładny, ale wciąż jest to najbliższy wymysł.

Kolejnym pomysłem, który rozważaliśmy, było użycie konfiguracji projektu zamiast wymagania od użytkownika edycji pliku projektu biblioteki. Jest to jednak absolutnie niewykonalne w VS2010, ponieważ niepotrzebnie dodaje wszystkie konfiguracje projektu do rozwiązania .

Roman Starkov
źródło
1

Mam zamiar polecić książkę Brownfield Application Development w .Net . Dwa bezpośrednio powiązane rozdziały to 8 i 9. Rozdział 8 mówi o przekazywaniu aplikacji, podczas gdy rozdział 9 mówi o oswajaniu zależności, odwróceniu kontroli i wpływie, jaki ma to na testowanie.

Tangurena
źródło
1

Pełne ujawnienie, jestem facetem z Javy. Rozumiem więc, że prawdopodobnie nie szukasz technologii, o których tu wspomnę. Ale problemy są takie same, więc być może wskaże ci właściwy kierunek.

W Javie istnieje wiele systemów kompilacji, które wspierają ideę scentralizowanego repozytorium artefaktów, w którym mieszczą się zbudowane „artefakty” - według mojej wiedzy jest to nieco analogiczne do GAC w .NET (proszę zignorować moją ignorancję, jeśli jest to napięta anaologia) ale więcej, ponieważ jest on używany do tworzenia niezależnych powtarzalnych kompilacji w dowolnym momencie.

W każdym razie inną obsługiwaną funkcją (na przykład w Maven) jest koncepcja OPCJONALNEJ zależności, a następnie zależna od konkretnych wersji lub zakresów i potencjalnie wykluczająca zależności przechodnie. Brzmi dla mnie jak to, czego szukasz, ale mogę się mylić. Spójrz na tę stronę wprowadzającą dotyczącą zarządzania zależnościami od Maven ze znajomym, który zna Javę, i sprawdź, czy problemy wydają się znajome. Pozwoli ci to zbudować aplikację i zbudować ją z lub bez dostępności tych zależności.

Istnieją również konstrukcje, jeśli potrzebujesz naprawdę dynamicznej, wtykowej architektury; jedną z technologii, która próbuje rozwiązać tę formę rozwiązywania zależności w czasie wykonywania, jest OSGI. To jest silnik systemu wtyczek Eclipse . Zobaczysz, że może obsługiwać opcjonalne zależności oraz minimalny / maksymalny zakres wersji. Ten poziom modułowości środowiska wykonawczego nakłada na ciebie wiele ograniczeń i sposobu rozwoju. Większość ludzi może sobie poradzić ze stopniem modułowości zapewnianym przez Maven.

Innym możliwym pomysłem, który można by rozważyć, może być rzędów wielkości prostszych do wdrożenia dla ciebie, jest użycie architektury w stylu rur i filtrów. To w dużej mierze sprawiło, że UNIX stał się tak dobrze prosperującym ekosystemem, który przetrwał i ewoluował przez pół wieku. Zapoznaj się z tym artykułem na temat rur i filtrów w .NET, aby uzyskać kilka pomysłów na temat implementacji tego rodzaju wzorca w swoim środowisku.

cwash
źródło
0

Być może książka „Wielkoskalowe projektowanie oprogramowania C ++” Johna Lakosa jest przydatna (oczywiście C # i C ++ lub nie to samo, ale można wyodrębnić przydatne techniki z książki).

Zasadniczo, ponownie uwzględnij czynnik i przenieś funkcjonalność korzystającą z dwóch lub więcej bibliotek do osobnego komponentu zależnego od tych bibliotek. W razie potrzeby użyj technik takich jak typy nieprzezroczyste itp.

Kasper van den Berg
źródło