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


26

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

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

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

각 라이브러리는 완전히 독립형으로 사용할 수 있지만 함께 사용 하면 유용한 추가 기능이 있습니다. 예를 들어, 모두 CommandLineParserXmlClassify필요 빌드 후 검사 기능을 노출 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 프레임 워크 중 하나 ( UnitySpring.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.