IoC 용 인터페이스 대신 Func 사용


15

컨텍스트 : C #을 사용하고 있습니다.

나는 수업을 설계했고, 수업을 분리하고, 단위 테스트를 더 쉽게하기 위해 모든 의존성을 전달하고 있습니다. 내부적으로 객체 인스턴스화가 없습니다. 그러나 필요한 데이터를 얻기 위해 인터페이스를 참조하는 대신 범용 Func를 참조하여 필요한 데이터 / 동작을 반환합니다. 의존성을 주입 할 때 람다 식으로 할 수 있습니다.

나에게 이것은 단위 테스트 중에 지루한 조롱을 할 필요가 없기 때문에 더 나은 접근법처럼 보입니다. 또한 주변 구현에 근본적인 변화가 있다면 팩토리 클래스 만 변경하면됩니다. 로직을 포함하는 클래스의 변경은 필요하지 않습니다.

그러나, 나는 IoC가 이런 식으로 한 일을 본 적이 없어서, 내가 놓칠 수있는 잠재적 인 함정이 있다고 생각합니다. 내가 생각할 수있는 유일한 것은 Func를 정의하지 않는 이전 버전의 C #과의 작은 비 호환성이며, 내 경우에는 문제가되지 않습니다.

보다 구체적인 인터페이스가 아니라 Func for IoC와 같은 일반적인 델리게이트 / 고차 함수 사용에 문제가 있습니까?


2
당신이 설명 하는 것은 함수형 프로그래밍 의 중요한 측면 인 고차 함수 입니다.
Robert Harvey

2
Func매개 변수의 이름을 지정할 수 있으므로 의도 대신 진술 할 수 있으므로 대리인 대신 대리인을 사용합니다 .
Stefan Hanke

1
관련 : stackoverflow : ioc-factory-pros-and-contras-for-interface-versus-delegates . @TheCatWhisperer 질문은 더 일반적이지만 stackoverflow 질문은 특별한 경우 "공장"으로 좁혀집니다
k3b

답변:


11

인터페이스에 하나 이상의 함수 만 포함되어 있고 두 개의 이름 (인터페이스 내부에 인터페이스 이름 함수 이름) 을 도입해야 할 이유가없는 경우 Func대신 사용하면 불필요한 상용구 코드를 피할 수 있으며 대부분의 경우 바람직합니다. DTO 설계를 시작하고 하나의 멤버 속성 만 필요하다는 것을 인식 할 때와 같습니다.

interfaces의존성 주입과 IoC가 인기를 얻었을 때 FuncJava 또는 C ++ 의 클래스 와 실질적으로 같지 않았기 때문에 많은 사람들이 사용하는 데 더 익숙 하다고 생각합니다 ( FuncC #에서 해당 시간에 사용 가능한지 확실하지 않습니다) ). interface사용하기 Func가 더 우아 하더라도 많은 튜토리얼, 예제 또는 교과서는 여전히 양식을 선호합니다 .

인터페이스 분리 원리와 Ralf Westphal의 Flow Design 접근 방식에 대한 이전 답변을 살펴볼 수 있습니다 . 이 패러 다마는 Func매개 변수를 사용 하여 DI를 독점적으로 구현 합니다. 보시다시피, 당신의 생각은 실제로 새로운 것이 아니며, 그 반대입니다.

그리고 예, 생산 코드, 각 단계에 대한 단위 테스트를 포함하여 여러 중간 단계가있는 파이프 라인 형태로 데이터를 처리 해야하는 프로그램 에이 접근법을 직접 사용했습니다. 그 점에서 저는 여러분에게 직접적인 경험을 제공 할 수 있습니다.


이 프로그램은 인터페이스 분리 원칙을 염두에두고 설계되지 않았으며 기본적으로 추상 클래스로만 사용됩니다. 이것이 내가이 접근법을 사용하는 또 다른 이유입니다. 인터페이스는 잘 생각되지 않으며 어쨌든 하나의 클래스 만 구현하므로 쓸모가 없습니다. 인터페이스 분리 원리는 특히 Func가 있기 때문에 극한으로 취해 져야 할 필요는 없지만 제 생각에는 여전히 매우 중요합니다.
TheCatWhisperer

1
이 프로그램은 인터페이스 분리 원칙을 염두에두고 설계되지도 않았습니다. 설명에 따르면이 용어가 설계에 적용될 수 있다는 것을 알지 못했을 것입니다.
Doc Brown

박사님, 나는 전임자가 만든 코드를 언급하고 있는데, 리팩토링 중이며 새로운 클래스가 어느 정도 의존하고 있습니다. 내가이 접근 방식을 선택한 이유 중 하나는이 클래스의 종속성을 포함하여 향후 프로그램의 다른 부분을 크게 변경할 계획이기 때문입니다.
TheCatWhisperer

1
"... 종속성 주입과 IoC가 대중화 될 당시에는 Func 클래스에 해당하는 요소가 없었습니다."Doc는 제가 거의 대답했지만 실제로는 충분히 자신감을 느끼지 못했습니다! 저의 의심을 확인해 주셔서 감사합니다.
Graham

9

IoC에서 찾은 주요 이점 중 하나는 인터페이스 이름을 지정하여 모든 종속성의 이름을 지정할 수 있으며 컨테이너는 유형 이름과 일치하여 생성자에게 제공 할 항목을 알 수 있다는 것입니다. 이것은 편리하며보다 설명적인 의존성 이름을 허용 Func<string, string>합니다.

또한 하나의 간단한 의존성으로도 때로는 둘 이상의 함수가 필요하다는 것을 알았습니다. 인터페이스를 사용하면 자체 문서화 방식으로 이러한 함수를 그룹화 할 수 있습니다 Func<string, string>. Func<string, int>.

의존성으로 전달되는 대리자를 갖는 것이 유용한 경우가 있습니다. 멤버를 거의 사용하지 않고 대리자를 사용하는 경우에 대한 판단 요청입니다. 논증의 목적이 무엇인지가 분명하지 않다면, 나는 보통 자기 문서화 코드를 만드는 측면에서 실수를 할 것입니다. 즉. 인터페이스 작성


원래 구현자가 더 이상 존재하지 않을 때 누군가의 코드를 디버깅해야했으며 스택에서 많은 작업과 실제로 실망스러운 프로세스가있는 곳에서 어떤 람다가 실행되는지 알아내는 첫 경험에서 알 수 있습니다. 원래 구현자가 인터페이스를 사용했다면 내가 찾고있는 버그를 찾는 것이 바람이했을 것입니다.
cwap

cwap, 나는 당신의 고통을 느낍니다. 그러나 내 특정 구현에서 람다는 모두 단일 함수를 가리키는 하나의 라이너입니다. 또한 단일 장소에서 생성됩니다.
TheCatWhisperer

내 경험상 이것은 툴링의 문제 일뿐입니다. ReSharpers Inspect-> Incoming Calls는 funcs / lambdas의 경로를 정리하는 데 효과적입니다.
기독교

3

보다 구체적인 인터페이스가 아니라 Func for IoC와 같은 일반적인 델리게이트 / 고차 함수 사용에 문제가 있습니까?

실제로는 아닙니다. Func는 자체 인터페이스 종류입니다 (C # 의미가 아닌 영어 의미). "이 매개 변수는 요청시 X를 제공하는 것입니다." Func은 필요한 경우에만 정보를 게으르게 제공 할 수 있다는 이점도 있습니다. 나는 이것을 조금하고 적당히 권장합니다.

단점은 다음과 같습니다.

  • IoC 컨테이너는 종종 종속 방식을 계단식으로 연결하기 위해 약간의 마법을 수행하며 T, 어떤 것이 있고 어떤 것이 있을 때는 잘 재생되지 않을 것입니다 Func<T>.
  • 펑크에는 간접적 인 것이 있기 때문에 추론하고 디버그하기가 조금 더 어려울 수 있습니다.
  • 함수는 인스턴스화를 지연시킵니다. 즉, 테스트 중에 런타임 오류가 이상한 시간에 표시되거나 전혀 표시되지 않을 수 있습니다. 또한 작동 순서 문제의 가능성을 높이고 사용에 따라 초기화 순서의 교착 상태를 증가시킬 수 있습니다.
  • Func에 전달하는 것은 약간의 오버 헤드와 그로 인한 합병증으로 폐쇄 될 수 있습니다.
  • Func 호출은 객체에 직접 액세스하는 것보다 약간 느립니다. (사소하지 않은 프로그램에서 알 수는 없지만 충분합니다)

1
IoC 컨테이너를 사용하여 전통적인 방식으로 팩토리를 만든 다음 팩토리는 인터페이스 메소드를 람다로 패키지하여 첫 번째 포인트에서 언급 한 문제를 해결합니다. 좋은 지적입니다.
TheCatWhisperer

1

간단한 예를 들어 보자. 아마도 로깅 수단을 주입하고있을 것이다.

수업 주입

class Worker: IWorker
{
    ILogger _logger;

    Worker(ILogger logger)
    {
        _logger = logger;
    }
    void SomeMethod()
    {
        _logger.Debug("This is a debug log statement.");
    }
}        

나는 그것이 무슨 일이 일어나고 있는지 분명하다고 생각합니다. 또한 IoC 컨테이너를 사용하는 경우 명시 적으로 아무것도 주입하지 않아도 컴포지션 루트에 추가하기 만하면됩니다.

container.RegisterType<ILogger, ConcreteLogger>();
container.RegisterType<IWorker, Worker>();
....
var worker = container.Resolve<IWorker>();

디버깅 할 때 Worker개발자는 컴포지션 루트를 참조하여 사용중인 콘크리트 클래스를 결정하면됩니다.

개발자가 더 복잡한 논리를 필요로하는 경우 다음과 같이 작업 할 수있는 전체 인터페이스가 있습니다.

    void SomeMethod()
    { 
       if (_logger.IsDebugEnabled) {
           _logger.Debug("This is a debug log statement.");
       }
    }

방법 주입

class Worker
{
    Action<string> _methodThatLogs;

    Worker(Action<string> methodThatLogs)
    {
        _methodThatLogs = methodThatLogs;
    }
    void SomeMethod()
    {
        _methodThatLogs("This is a logging statement");
    }
}        

먼저 생성자 매개 변수의 이름이 더 길어졌습니다 methodThatLogs. 해야 할 일을 알 수 없기 때문에이 작업이 필요합니다 Action<string>. 인터페이스를 사용하면 완전히 명확 해졌지만 여기서는 매개 변수 이름 지정에 의존해야합니다. 이것은 빌드하는 동안 본질적으로 신뢰성이 떨어지고 시행하기가 더 어려워 보입니다.

이제이 방법을 어떻게 주입합니까? 글쎄, IoC 컨테이너는 당신을 위해 그것을하지 않을 것입니다. 따라서 인스턴스화 할 때 명시 적으로 주입합니다 Worker. 이로 인해 몇 가지 문제가 발생합니다.

  1. 인스턴스화하는 것이 더 많은 일입니다. Worker
  2. 디버깅 Worker을 시도하는 개발자 는 구체적인 인스턴스가 무엇인지 파악하기가 더 어렵다는 것을 알게 될 것입니다. 그들은 단지 구성 루트를 상담 할 수 없습니다. 코드를 통해 추적해야합니다.

더 복잡한 논리가 필요한 경우는 어떻습니까? 귀하의 기술은 하나의 방법 만 노출합니다. 이제 복잡한 것들을 람다로 구울 수 있다고 가정합니다.

var worker = new Worker((s) => { if (log.IsDebugEnabled) log.Debug(s) } );

그러나 단위 테스트를 작성할 때 해당 람다 식을 어떻게 테스트합니까? 익명이므로 단위 테스트 프레임 워크에서 직접 인스턴스화 할 수 없습니다. 어쩌면 당신은 그것을 할 수있는 영리한 방법을 알아낼 수는 있지만 아마도 인터페이스를 사용하는 것보다 더 큰 PITA 일 것입니다.

차이점 요약 :

  1. 메소드 만 주입하면 목적을 추론하기가 더 어려우며 인터페이스는 목적을 명확하게 전달합니다.
  2. 메소드 만 주입하면 주입을받는 클래스에 기능이 덜 노출됩니다. 오늘 필요하지 않더라도 내일 필요할 수도 있습니다.
  3. IoC 컨테이너를 사용하는 메소드 만 자동으로 주입 할 수 없습니다.
  4. 컴포지션 루트에서 특정 인스턴스에서 어떤 구체적인 클래스가 작동하는지 알 수 없습니다.
  5. 람다 식 자체를 단위 테스트하는 것은 문제가됩니다.

위의 모든 내용이 정상이면 분석법 만 주입해도됩니다. 그렇지 않으면 전통을 고수하고 인터페이스를 주입하는 것이 좋습니다.


내가 지정한 클래스는 프로세스 논리 클래스입니다. 정보에 입각 한 결정을 내리는 데 필요한 데이터를 얻기 위해 외부 클래스에 의존하고 있으며, 로거와 같은 클래스를 유발하는 부작용은 사용되지 않습니다. 귀하의 예에서 문제는 특히 IoC 컨테이너를 사용하는 상황에서 OO가 좋지 않다는 것입니다. 클래스에 복잡성을 더하는 if 문을 사용하는 대신 단순히 비활성 로거에 전달해야합니다. 또한 람다에 논리를 도입하면 실제로 테스트가 더 어려워집니다. 사용 목적을 어길 수 있기 때문에 수행되지 않습니다.
TheCatWhisperer

람다는 응용 프로그램의 인터페이스 방법을 가리키고 단위 테스트에서 임의의 데이터 구조를 구성합니다.
TheCatWhisperer

사람들은 왜 명백하게 임의의 예가 무엇인지에 초점을 둔 이유는 무엇입니까? 적절한 로깅에 대해 이야기하고 싶다면 비활성 로거는 어떤 경우에 끔찍한 아이디어가 될 것입니다.
John Wu

"lambdas가 Interface 메서드를 가리킨다"는 무슨 뜻인지 잘 모르겠습니다. 대리인 서명과 일치하는 메서드 구현 을 주입해야한다고 생각합니다 . 메소드가 인터페이스에 속하는 경우에는 부수적입니다. 컴파일 또는 런타임 검사가 수행되지 않습니다. 내가 오해하고 있습니까? 게시물에 코드 예제를 포함시킬 수 있습니까?
John Wu

나는 당신의 결론에 동의한다고 말할 수 없습니다. 우선, 클래스를 최소한의 종속성으로 연결해야하므로 람다를 사용하면 주입 된 각 항목이 인터페이스에서 여러 항목의 상관 관계가 아닌 정확히 하나의 종속성이됩니다. 또한 Worker한 번 에 실행 가능한 하나의 종속성 을 빌드하는 패턴을 지원하여 모든 종속성이 충족 될 때까지 각 람다를 독립적으로 주입 할 수 있습니다. 이는 FP의 부분 응용 프로그램과 유사합니다. 또한 람다를 사용하면 암시 적 상태와 종속성 간의 숨겨진 커플 링을 제거 할 수 있습니다.
Aaron M. Eshbach

0

오래 전에 작성한 다음 코드를 고려하십시오.

public interface IPhysicalPathMapper
{
    /// <summary>
    /// Gets the physical path represented by the relative URL.
    /// </summary>
    /// <param name="relativeURL"></param>
    /// <returns></returns>
    String GetPhysicalPath(String relativeURL);
}

public class EmailBuilder : IEmailBuilder
{
    public IPhysicalPathMapper PhysicalPathMapper { get; set; }
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templateRelativeURL, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(this.PhysicalPathMapper.GetPhysicalPath(templateRelativeURL));

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

템플릿 파일의 상대 위치를 가져 와서 메모리에로드하고 메시지 본문을 렌더링하며 전자 메일 개체를 어셈블합니다.

당신은 볼 수 IPhysicalPathMapper와 생각 "하나의 기능이있다. 그것은이 될 수 있습니다 Func." 그러나 실제로 여기서 문제는 IPhysicalPathMapper존재하지 않아야한다는 것입니다. 훨씬 더 나은 해결책은 경로를 매개 변수화하는 것입니다 .

public class EmailBuilder : IEmailBuilder
{
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templatePath, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(templatePath);

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

이것은이 코드를 개선하기 위해 다른 많은 질문을 제기합니다. 예를 들어, 그냥을 수락 한 EmailTemplate다음 미리 렌더링 된 템플릿을 수락 한 다음 인라인 상태 여야합니다.

이것이 내가 통제 역전을 만연한 패턴으로 싫어하는 이유입니다. 일반적으로 모든 코드를 작성하는이 신과 같은 솔루션으로 유지됩니다. 그러나 실제로는 (만약에 비해) 광범위 하게 사용하면 완전히 뒤로 사용되는 불필요한 인터페이스를 많이 도입 하여 코드가 훨씬 나빠 집니다. (거꾸로 호출자는 클래스 자체가 호출을 호출하는 대신 이러한 종속성을 평가하고 결과를 전달해야합니다.)

인터페이스는 드물게 사용해야하며 제어 및 의존성 주입의 반전도 드물게 사용해야합니다. 많은 양의 코드가 있으면 코드를 해독하기가 훨씬 어려워집니다.

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