“선택적”종속성을 사용하여“물결”유틸리티 프로젝트를 개별 구성 요소로 분리


26

다년간의 사내 프로젝트에 C # /. NET을 사용하면서 한 라이브러리는 유기적으로 하나의 거대한 뭉치로 성장했습니다. 그것은 "유틸리티 (Util)"라고 불리며, 나는 많은 여러분이 당신의 경력에서이 짐승들 중 하나를 보았을 것입니다.

이 라이브러리의 많은 부분은 매우 독립적이며 별도의 프로젝트 (오픈 소스)로 분리 될 수 있습니다. 그러나 별도의 라이브러리로 릴리스하기 전에 해결해야 할 한 가지 중요한 문제가 있습니다. 기본적으로 이러한 라이브러리 사이에 "선택적 종속성" 이라고 부르는 사례가 많이 있습니다 .

이를 더 잘 설명하려면 독립형 라이브러리가 될 수있는 후보가되는 일부 모듈을 고려하십시오. CommandLineParser명령 줄을 구문 분석하기위한 것입니다. XmlClassify클래스를 XML로 직렬화하기위한 것입니다. PostBuildCheck컴파일 된 어셈블리를 검사하고 실패하면 컴파일 오류를보고합니다. ConsoleColoredString컬러 문자열 리터럴을위한 라이브러리입니다. Lingo사용자 인터페이스를 번역하기위한 것입니다.

각 라이브러리는 완전히 독립형으로 사용할 수 있지만 함께 사용 하면 유용한 추가 기능이 있습니다. 예를 들어, 모두 CommandLineParser와 XmlClassify필요 빌드 후 검사 기능을 노출 PostBuildCheck. 마찬가지로, CommandLineParser옵션 문자열을 요구하는 컬러 문자열 리터럴을 사용하여 제공 할 수 있으며 ConsoleColoredString를 통해 번역 가능한 문서를 지원합니다 Lingo.

중요한 차이점은 이것들이 선택적 기능이라는 것 입니다. 문서를 번역하거나 빌드 후 검사를 수행하지 않고 무색의 일반 문자열로 명령 행 파서를 사용할 수 있습니다. 또는 문서를 번역 할 수는 있지만 여전히 채색 할 수는 없습니다. 또는 채색 및 번역이 가능합니다. 기타.

이 "유틸리티"라이브러리를 살펴보면 거의 모든 잠재적으로 분리 가능한 라이브러리에는 다른 라이브러리와 묶을 수있는 선택적 기능이 있습니다. 실제로 해당 라이브러리를 종속성으로 요구한다면이 뭉치가 전혀 풀리지 않습니다. 단 하나만 사용하려면 기본적으로 모든 라이브러리가 필요합니다.

.NET에서 이러한 선택적 종속성을 관리하는 기존의 접근 방식이 있습니까?


2
라이브러리가 서로 종속되어 있어도 라이브러리를 서로 다른 기능 범주를 포함하는 일관된 별도의 라이브러리로 분리하면 여전히 이점이있을 수 있습니다.
— Robert Harvey

답변:


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. 이러한 각 프로젝트에 대해 빈 프로젝트를 작성하고 적절한 프로젝트 참조 (UI 참조 Core, UI.WinForms 참조 UI) 등을 작성하십시오.

  3. 활용도가 낮은 결실 (종속성 문제가 발생하지 않는 클래스 또는 메서드)을 Utils 어셈블리에서 새 대상 어셈블리로 이동하십시오.

  4. NDepend 와 Martin Fowler 's Refactoring 의 사본을 통해 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현재 폴더에 DLL 파일에서 인터페이스를 찾을 수 클래스 (또는 어쩌면 InterfaceFinder이 경우에 더 좋은 이름입니다). 다음은 간단한 예입니다. @EdWoodcock의 조언에 따르면 (그리고 동의합니다), 프로젝트가 커지면 사용 가능한 Dependency Injection 프레임 워크 중 하나 ( Unity 및 Spring.NET을 사용하는 Common Serivce Locator가 마음에 듭니다 )를 사용하여보다 고급 "찾기" 해당 기능 "기능은, 그렇지 않으면로 알려진 서비스 로케이터 패턴 . 필요에 따라 수정할 수 있습니다.

    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;
        }
    

    }

가장 중요 : 테스트, 테스트, 각 변경 사이에 테스트하십시오.


예를 추가했습니다! :-)
— Kevin McCormick

1
이 PluginFinder 클래스는 롤-자체 자동 DI 처리기 (ServiceLocator 패턴 사용)와 비슷하게 보이지만 그렇지 않은 경우에는 적절한 조언입니다. 라이브러리와 같은 특정 인터페이스의 여러 구현 (StringColourer vs StringColourerWithHtmlWrapper 등)에 문제가 없으므로 Unity와 같은 것을 OP로 지정하는 것이 좋습니다.
— Ed James

@ Edwoodcock 좋은 지적 Ed, 나는 이것을 쓰는 동안 Service Locator 패턴을 생각하지 않았다고 믿을 수 없다. PluginFinder는 확실히 미성숙 한 구현이며 DI 프레임 워크는 확실히 여기에서 작동합니다.
— Kevin McCormick

나는 노력에 대한 현상금을 수여했지만 우리는이 길을 가지 않을 것입니다. 인터페이스의 핵심 어셈블리를 공유한다는 것은 구현을 이동시키는 데 성공했지만 여전히 작은 관련 인터페이스가 포함 된 라이브러리가 있다는 것을 의미합니다 (전과 같이 선택적인 종속성을 통해 관련됨). 이처럼 작은 라이브러리에는 거의 이점이 없으므로 설정이 훨씬 더 복잡해졌습니다. 엄청난 복잡성은 엄청난 프로젝트에 가치가있을 수 있지만 이러한 프로젝트에는 적합하지 않습니다.
— Roman Starkov

@romkyns 그래서 어떤 경로를 복용하고 있습니까? 그대로두고? :)
— Max

5

추가 라이브러리에 선언 된 인터페이스를 사용할 수 있습니다.

의존성 주입 (MEF, Unity 등)을 사용하여 계약 (인터페이스를 통한 클래스)을 해결하십시오. 찾을 수 없으면 널 인스턴스를 리턴하도록 설정하십시오.
그런 다음 인스턴스가 null인지 확인하십시오.이 경우 추가 기능을 수행하지 않습니다.

이것은 교과서 사용이기 때문에 MEF와 특히 쉽습니다.

라이브러리를 n + 1 dll로 나누는 비용으로 라이브러리를 컴파일 할 수 있습니다.

HTH.


이것은 거의 똑같이 들립니다. 하나의 여분의 DLL이 아니었다면 기본적으로 원래 물건 덩어리의 골격과 같습니다. 구현은 모두 분리되어 있지만 여전히 "골격의 무리"가 남아 있습니다. 나는 그것이 몇 가지 장점이 있다고 생각하지만, 그 장점이이 특정 라이브러리 세트에 대한 모든 비용을 능가한다고 확신하지는 않습니다.
— Roman Starkov

또한 전체 프레임 워크를 포함하는 것은 완전히 한 걸음 물러납니다. 이 라이브러리는있는 그대로 그 프레임 워크 중 하나의 크기에 불과하므로 이점을 완전히 무시합니다. 어떤 경우에는 구현이 가능한지 확인하기 위해 약간의 반사를 사용합니다 .0과 1 사이 만있을 수 있으며 외부 구성이 필요하지 않기 때문입니다.
— Roman Starkov

2

나는 우리가 지금까지 생각해 낸 가장 실용적인 옵션을 게시하여 생각이 무엇인지 알 것이라고 생각했습니다.

기본적으로, 우리는 각 컴포넌트를 참조가 0 인 라이브러리로 분리합니다. 참조가 필요한 모든 코드 #if/#endif는 적절한 이름을 가진 블록에 배치됩니다 . 예를 들어, CommandLineParser해당 핸들 ConsoleColoredString의 코드는 에 배치됩니다 #if HAS_CONSOLE_COLORED_STRING.

CommandLineParser더 이상의 의존성이 없기 때문에 단지 포함하기를 원하는 솔루션 은 쉽게 그렇게 할 수 있습니다. 그러나 솔루션에 ConsoleColoredString프로젝트 도 포함되어 있으면 프로그래머는 이제 다음 옵션을 선택할 수 있습니다.

  • 에서 참조 추가 CommandLineParser로를ConsoleColoredString
  • 프로젝트 파일에 HAS_CONSOLE_COLORED_STRING정의를 추가 CommandLineParser하십시오.

관련 기능을 사용할 수있게됩니다.

이것에는 몇 가지 문제가 있습니다.

  • 이것은 소스 전용 솔루션입니다. 라이브러리의 모든 소비자는 소스 코드로 라이브러리를 포함해야합니다. 그들은 단지 바이너리를 포함 할 수는 없다 (그러나 이것은 우리에게 절대적인 요구 사항은 아니다).
  • 라이브러리 의 라이브러리 프로젝트 파일에는 몇 가지 솔루션 별 편집 내용이 포함되어 있으며 이러한 변경 사항이 SCM에 어떻게 적용되는지는 분명하지 않습니다.

오히려 예쁘지 않지만 여전히 이것은 우리가 생각해 낸 가장 가까운 것입니다.

우리가 생각한 또 다른 아이디어는 사용자가 라이브러리 프로젝트 파일을 편집하도록 요구하지 않고 프로젝트 구성을 사용하는 것입니다. 그러나 VS2010 에서는 모든 프로젝트 구성을 원치 않는 방식으로 솔루션에 추가 하기 때문에이 기능을 사용할 수 없습니다 .


1

나는 .Net에서 Brownfield Application Development 책을 추천 할 것 입니다. 직접 관련된 두 개의 장은 8 & 9입니다. 8 장에서는 응용 프로그램 릴레이에 대해 이야기하고 9 장에서는 길들이기 종속성, 제어 역전 및 이것이 테스트에 미치는 영향에 대해 설명합니다.


1

전체 공개, 나는 자바 사람입니다. 따라서 여기서 언급 할 기술을 찾지 않을 것입니다. 그러나 문제는 동일하므로 올바른 방향으로 안내 할 것입니다.

Java에는 빌드 된 "아티팩트"를 포함하는 중앙 집중식 아티팩트 저장소의 아이디어를 지원하는 많은 빌드 시스템이 있습니다. 내 지식으로는 이것이 .NET의 GAC와 다소 유사합니다. 그러나 특정 시점에서 독립적 인 반복 가능한 빌드를 생성하는 데 사용되기 때문에 그 이상입니다.

어쨌든 (예를 들어 Maven에서) 지원되는 또 다른 기능은 특정 버전 또는 범위에 따라 전 이적 종속성을 제외하고 OPTIONAL 종속성에 대한 아이디어입니다. 이것은 당신이 찾고있는 것처럼 들리지만 잘못 될 수 있습니다. Java를 알고있는 친구와 Maven의 종속성 관리 에 대한 이 소개 페이지를 보고 문제가 익숙한 지 확인하십시오. 이를 통해 응용 프로그램을 빌드하고 이러한 종속성을 사용하거나 사용하지 않고 만들 수 있습니다.

동적이고 플러그 가능한 아키텍처가 필요한 경우에도 구성이 있습니다. 이러한 형태의 런타임 의존성 해결을 다루는 기술 중 하나는 OSGI입니다. 이것이 Eclipse 플러그인 시스템의 엔진 입니다. 선택적 종속성과 최소 / 최대 버전 범위를 지원할 수 있음을 알 수 있습니다. 이 수준의 런타임 모듈성은 사용자와 개발 방법에 많은 제약을가합니다. 대부분의 사람들은 Maven이 제공하는 모듈성의 정도를 얻을 수 있습니다.

구현하기가 훨씬 간단 할 수도있는 또 다른 가능한 아이디어는 파이프 및 필터 스타일의 아키텍처를 사용하는 것입니다. 이것이 바로 UNIX를 반세기 동안 살아남아 온 오랜 지속되고 성공적인 생태계로 만든 이유입니다. 프레임 워크에서 이러한 종류의 패턴을 구현하는 방법에 대한 아이디어 는 .NET의 파이프 및 필터에 대한 이 기사를 참조하십시오 .


0

John Lakos의 "대규모 C ++ 소프트웨어 디자인"이라는 책이 유용 할 것입니다 (물론 C #과 C ++는 같지 않지만 책에서 유용한 기술을 익힐 수 있습니다).

기본적으로 둘 이상의 라이브러리를 사용하는 기능을 이러한 라이브러리에 의존하는 별도의 구성 요소로 리팩터링하고 이동하십시오. 필요한 경우 불투명 한 유형 등과 같은 기술을 사용하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.