이 코드를 작성하는 방법은 테스트 가능하지만 누락 된 것이 있습니까?


13

라는 인터페이스가 IContext있습니다. 이를 위해 다음을 제외하고는 실제로 어떤 일을하는지는 중요하지 않습니다.

T GetService<T>();

이 방법은 응용 프로그램의 현재 DI 컨테이너를보고 종속성을 해결하려고 시도합니다. 상당히 표준이라고 생각합니다.

내 ASP.NET MVC 응용 프로그램에서 생성자는 다음과 같습니다.

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetService<ISomeService>();
    AnotherService = ctx.GetService<IAnotherService>();
}

따라서 각 서비스의 생성자에 여러 매개 변수를 추가하는 대신 (응용 프로그램을 확장하는 개발자에게는 실제로 성 가시고 시간이 많이 걸리기 때문에)이 방법을 사용하여 서비스를 받고 있습니다.

지금, 그것은 잘못 느낀다 . 그러나, 내가 현재 내 머릿속에서 그것을 정당화하는 방법은 이것 입니다. 나는 그것을 조롱 할 수 있습니다 .

저 할 수 있어요. IContext컨트롤러를 테스트 하기 위해 조롱하는 것은 어렵지 않습니다 . 어쨌든 나는 :

public class MyMockContext : IContext
{
    public T GetService<T>()
    {
        if (typeof(T) == typeof(ISomeService))
        {
            // return another mock, or concrete etc etc
        }

        // etc etc
    }
}

그러나 내가 말했듯이, 그것은 잘못 느낍니다. 모든 생각 / 학대 환영.


8
이것을 Service Locator 라고 하며 마음에 들지 않습니다. 주제에 대해 많은 글을 썼다 . 초보자는 martinfowler.com/articles/injection.htmlblog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern 을 참조하십시오 .
Benjamin Hodgson

Martin Fowler 기사에서 : "이러한 서비스 로케이터는 구현을 대신 할 수 없기 때문에 테스트 할 수 없기 때문에 이러한 종류의 서비스 로케이터가 좋지 않다는 불만을 자주 들었습니다. 이 경우 서비스 로케이터 인스턴스는 단순한 데이터 홀더 일뿐입니다. 내 서비스의 테스트 구현으로 로케이터를 쉽게 만들 수 있습니다. " 왜 마음에 들지 않습니까? 아마도 대답에?
LiverpoolsNumber9 9

8
그는 맞습니다, 이것은 나쁜 디자인입니다. 쉽다 : public SomeClass(Context c). 이 코드는 분명하지 않습니까? 에 that SomeClass따라 다릅니다 Context. 어르지만 기다리지 마라! X컨텍스트에서 얻는 종속성에만 의존합니다 . 즉, 때마다 당신을 변경하기 Context있었다 휴식 SomeObject만 변경하더라도 ContextY. 하지만 그래, 당신은 당신 만 변경 알고 Y있지 X때문에, SomeClass괜찮습니다. 그러나 좋은 코드를 작성하는 것은 사용자 가 아는 것이 아니라 새로운 직원이 코드를 처음 볼 때 아는 것입니다.
valenterry

@DocBrown 그것은 나에게 정확히 내가 말한 것입니다-나는 여기서 차이점을 보지 못합니다. 더 설명해 주시겠습니까?
valenterry

1
@DocBrown 나는 당신의 요점을 지금 본다. 예, 그의 컨텍스트가 모든 종속성의 묶음이라면 이것은 나쁜 디자인이 아닙니다. 그러나 이름이 잘못되었을 수도 있지만 이는 가정에 불과합니다. 상황에 대한 더 많은 방법 (내부 개체)이 있는지 OP가 명확히해야합니다. 또한 코드를 논의하는 것은 좋지만 이것은 programmers.stackexchange이므로 OP를 개선하기 위해 "뒤에"있는 것도보아야합니다.
valenterry

답변:


5

생성자에 많은 매개 변수 대신 하나를 갖는 것이이 디자인의 문제가되는 부분아닙니다 . 언제 까지나 당신 같은 IContext클래스가 아무것도하지만없는 서비스 외관 specificially에 사용 종속 제공, MyControllerBase, 그리고 전체 코드에서 사용되는 일반적인 서비스 로케이터는, 코드의 일부는 이럴 확인합니다.

첫 번째 예가

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetSomeService();
    AnotherService = ctx.GetAnotherService();
}

그것의 실질적인 디자인 변경은 아닙니다 MyControllerBase. 이 디자인이 좋고 나쁘다면 당신이 원하는 경우에만 사실에 달려 있습니다

  • 확인 TheContext, SomeService그리고 AnotherService항상 모든 mock 객체로 초기화하거나 그들 모두 실물로
  • 또는 3 개의 객체를 다르게 조합하여 초기화 할 수 있습니다 (이 경우 매개 변수를 개별적으로 전달해야 함)

따라서 생성자에서 3 대신에 하나의 매개 변수 만 사용하는 것이 합리적 일 수 있습니다.

문제가되는 것은입니다 IContext노출 GetService공공 장소에서 방법을. IMHO 당신은 이것을 피해야하며, 대신 "공장 방법"을 명시 적으로 유지하십시오. 서비스 로케이터를 사용하여 예제에서 GetSomeServiceand GetAnotherService메소드 를 구현해도 괜찮 습니까? IMHO에 달려 있습니다. 오랫동안 같이 IContext클래스가 바로 이럴 허용 서비스 객체의 명시 적 목록을 제공하는 특정 목적에 대한 간단한 추상 공장을 beeing는 유지합니다. 추상 팩토리는 일반적으로 "접착제"코드이므로 단위 테스트 자체가 필요하지 않습니다. 그럼에도 불구하고 같은 메소드의 맥락에서, GetSomeService실제로 서비스 로케이터가 필요한지, 명시적인 생성자 호출이 더 간단하지 않은지 스스로에게 문의 해야합니다.

따라서 IContext구현이 공개적이고 일반적인 GetService메소드 주위의 래퍼 인 디자인을 고수 할 때 임의의 클래스로 임의의 의존성을 해결하면 모든 것이 @ BenjaminHodgson이 그의 답변에 쓴 것을 적용합니다.


동의합니다. 예제의 문제점은 일반적인 GetService방법입니다. 명시 적으로 명명되고 형식화 된 메소드로 리팩토링하는 것이 좋습니다. IContext생성자에서 명시 적으로 구현 의 종속성을 설명하는 것이 더 좋습니다.
Benjamin Hodgson

1
@ BenjaminHodgson : 때로는 더 좋을 수도 있지만 항상 더 나은 것은 아닙니다 . ctor 매개 변수 목록이 점점 길어지면 코드 냄새가납니다. 여기에 내 이전 답변을 참조하십시오 : programmers.stackexchange.com/questions/190120/…
Doc Brown

@DocBrown 생성자 초과 주입의 "코드 냄새"는 실제 문제인 SRP 위반을 나타냅니다. 단순히 여러 서비스를 Facade 클래스로 래핑하고 속성으로 만 노출하기 만하면 문제의 원인을 해결할 수 없습니다 . 따라서 Facade는 다른 구성 요소를 둘러싼 단순한 래퍼가 아니어야하지만, 이상적으로 소비 할 수있는 간단한 API를 제공해야합니다 (또는 다른 방식으로 단순화해야 함).
AlexFoxGill

... 생성자 초과 주입과 같은 "코드 냄새" 는 그 자체로는 문제 가 되지 않습니다 . 문제가 확인되면 코드를 리팩토링하여 보통 해결할 수있는 코드에 더 깊은 문제가 있다는 힌트
일뿐입니다.

마이크로 소프트가 이것을 구 웠을 때 어디에 있었 IValidatableObject습니까?
RubberDuck

15

이 디자인은 Service Locator * 로 알려져 있으며 마음에 들지 않습니다. 그것에 대해 많은 논쟁이 있습니다.

Service Locator는 컨테이너에 연결 합니다. 일반적인 의존성 주입 (생성자가 의존성을 명시 적으로 철자하는 곳)을 사용하면 컨테이너를 다른 것으로 간단하게 교체하거나 new-expressions 로 돌아갈 수 있습니다. 그것으로 IContext정말 불가능합니다.

서비스 로케이터는 종속성을 숨 깁니다 . 클라이언트는 클래스의 인스턴스를 구성하는 데 필요한 것을 말하기가 매우 어렵습니다. 당신은 어떤 종류의 필요 IContext,하지만 당신은 또한 만들기 위해 올바른 개체를 반환하는 컨텍스트를 설정해야 MyControllerBase일을. 이것은 생성자의 서명에서 전혀 분명하지 않습니다. 일반 DI로 컴파일러는 필요한 것을 정확하게 알려줍니다. 수업에 많은 의존성이 있다면 리팩토링을 자극하기 때문에 고통을 느껴야합니다 . Service Locator는 잘못된 설계 문제를 숨 깁니다.

서비스 로케이터는 런타임 오류를 발생시킵니다 . GetService잘못된 유형 매개 변수를 사용하여 호출 하면 예외가 발생합니다. 다시 말해, GetService함수는 총 함수가 아닙니다. (총 함수는 FP 세계의 아이디어이지만 기본적으로 함수는 항상 값을 반환해야 함을 의미합니다.) 컴파일러가 도움을 받고 종속성이 잘못되었을 때 알려주는 것이 좋습니다.

서비스 로케이터가 Liskov 대체 원칙을 위반합니다 . 동작은 type 인수에 따라 다르므로 Service Locator는 인터페이스에 무한한 수의 메소드가있는 것처럼 볼 수 있습니다! 이 주장은 여기 에 자세히 설명되어 있습니다 .

Service Locator는 테스트하기가 어렵습니다 . IContext테스트 에 대한 가짜 예를 제시 했지만, 처음에는 해당 코드를 작성하지 않는 것이 좋습니다. 서비스 로케이터를 거치지 않고 가짜 의존성을 직접 주입하십시오.

요컨대, 하지 마십시오 . 많은 의존성을 가진 클래스의 문제에 대한 매혹적인 해결책처럼 보이지만 장기적으로는 인생을 비참하게 만들 것입니다.

* Resolve<T>임의의 종속성을 해결할 수 있고 컴포지션 루트뿐만 아니라 코드베이스 전체에서 사용되는 일반적인 방법 으로 Service Locator를 객체로 정의 하고 있습니다. 이것은 Service Facade (작은 알려진 종속성 집합을 묶는 객체) 또는 Abstract Factory (단일 유형의 인스턴스를 만드는 객체-Abstract Factory의 유형 은 일반적이지만 방법 은 아닙니다)와 동일하지 않습니다. .


1
귀하는 서비스 로케이터 패턴에 동의합니다 (동의합니다). 그러나 실제로 OP의 예에서는 MyControllerBase특정 DI 컨테이너에 연결되어 있지 않으며 Service Locator 안티 패턴의 예가 아닙니다.
Doc Brown

@DocBrown 동의합니다. 내 인생이 더 쉬워지기 때문이 아니라 위에 주어진 대부분의 예제가 내 코드와 관련이 없기 때문에.
LiverpoolsNumber9 9

2
나에게, Service Locator 안티 패턴의 특징은 일반적인 GetService<T>방법입니다. 임의의 의존성을 해결하는 것은 실제 냄새이며 OP의 예에서 존재하고 정확합니다.
Benjamin Hodgson

1
서비스 로케이터를 사용할 때의 또 다른 문제점은 유연성이 저하된다는 것입니다. 각 서비스 인터페이스는 하나만 구현할 수 있습니다. IFrobnicator에 의존하는 두 개의 클래스를 빌드하지만 나중에 하나는 원래 DefaultFrobnicator 구현을 사용해야한다고 결정하지만 다른 하나는 실제로 주변에 CacheingFrobnicator 데코레이터를 사용해야한다면 기존 코드를 변경해야합니다. 종속성을 직접 주입하면 설정 코드 (또는 DI 프레임 워크를 사용하는 경우 구성 파일) 만 변경하면됩니다. 따라서 이것은 OCP 위반입니다.
Jules

1
@DocBrown이 GetService<T>()메소드는 임의의 클래스를 요청할 수 있도록 허용합니다. "이 메소드가하는 것은 어플리케이션의 현재 DI 컨테이너를보고 의존성을 해결하려고 시도하는 것입니다. 상당히 표준입니다." . 이 답변 맨 위에 귀하의 의견에 답변하고있었습니다. 이것은 100 % 서비스 로케이터
AlexFoxGill

5

Service Locator 안티 패턴에 대한 가장 좋은 주장은 Mark Seemann에 의해 명확 하게 언급되어 있기 때문에 이것이 왜 나쁜 생각인지에 대해서는 너무 많이 언급 하지 않을 것입니다. 또한 Mark 's book을 추천 합니다.

질문에 대답하기 위해 확인하십시오- 실제 문제를 다시 말씀 드리겠습니다 .

따라서 각 서비스의 생성자에 여러 매개 변수를 추가하는 대신 (응용 프로그램을 확장하는 개발자에게는 실제로 성 가시고 시간이 많이 걸리기 때문에)이 방법을 사용하여 서비스를 받고 있습니다.

StackOverflow 에서이 문제를 해결하는 질문이 있습니다. 의견 중 하나에서 언급했듯이 :

가장 좋은 설명 : "생성자 주입의 놀라운 이점 중 하나는 단일 책임 원칙을 위반하는 것이 명백하게 드러난다는 것입니다."

문제의 해결책을 찾기 위해 잘못된 곳을 찾고 있습니다. 수업이 너무 많은시기를 아는 것이 중요합니다. 귀하의 경우에는 내가 강하게 의심 에 "자료 컨트롤러"에 대한 필요가 없다. 실제로 OOP에서는 거의 항상 상속이 필요하지 않습니다 . 행동과 공유 기능의 변화는 인터페이스를 적절히 사용하여 완전히 달성 할 수 있으며, 이는 일반적으로 더 잘 팩토링되고 캡슐화 된 코드를 생성하며, 의존성을 슈퍼 클래스 생성자에게 전달할 필요가 없습니다.

나는 자료 컨트롤러가 위치에 작업 한 모든 프로젝트에서는, 같은 편리한 프로퍼티와 메소드, 공유의 목적을 위해 순수하게 이루어졌다 IsUserLoggedIn()GetCurrentUserId(). 중지하십시오 . 이것은 상속의 끔찍한 오용입니다. 대신, 이러한 메소드를 노출하는 컴포넌트를 작성하여 필요한 곳에 의존하십시오. 이러한 방식으로 구성 요소는 테스트 가능한 상태로 유지되며 해당 종속성이 분명해집니다.

다른 것 외에도 MVC 패턴을 사용할 때는 항상 스키니 컨트롤러를 권장 합니다. 여기에서 더 자세히 읽을 수 있지만 패턴의 본질은 간단합니다. MVC의 컨트롤러는 한 가지만 수행해야합니다. 이것은 다시 직장에서의 단일 책임 원칙입니다.

더 정확한 판단을 내리려면 유스 케이스를 아는 것이 실제로 도움이 될 것입니다. 그러나 솔직히 기본 클래스가 팩터링 된 종속성보다 선호되는 시나리오는 생각할 수 없습니다.


+1-이것은 다른 답변들 중 실제로 해결되지 않은 새로운 질문입니다
Benjamin Hodgson

1
"제작자 주입의 놀라운 이점 중 하나는 단일 책임 원칙을 위반하는 것이 눈에 띄게 있다는 것입니다." 정말 좋은 대답입니다. 모든 것에 동의하지 마십시오. 내 유스 케이스는 재미있게도 100 개 이상의 컨트롤러가있는 시스템에서 코드를 복제 할 필요가 없습니다. 그러나 re SRP-각각의 주입 된 서비스에는 단일 책임이 있습니다.
LiverpoolsNumber9 9

1
@ LiverpoolsNumber9 핵심은 기능을 BaseController에서 종속성으로 옮기는 것입니다. BaseController에 protected새 구성 요소에 대한 한 줄 위임을 제외하고는 아무것도 남지 않을 때까지 입니다. 그런 다음 BaseController를 잃고 해당 protected메소드를 종속성에 대한 직접 호출로 바꿀 수 있습니다. 이렇게하면 모델이 단순화되고 모든 종속성이 명시 적으로 나타납니다 (100 개의 컨트롤러가있는 프로젝트에서 매우 좋은 일입니다!)
AlexFoxGill

@ LiverpoolsNumber9 - 나는 몇 가지 구체적인 제안을 할 수 페이스트 빈 당신은 당신의 BaseController의 일부를 복사 할 수있는 경우
AlexFoxGill

-2

다른 사람의 기여를 바탕으로 이에 대한 답변을 추가하고 있습니다. 정말 고마워요 우선 여기에 제 대답이 있습니다 : "아니오, 아무 문제가 없습니다".

Doc Brown의 "Service Facade"답변 이 답변은 내가 찾은 것 (답이 "아니오"인 경우)이 내가하고있는 일에 대한 예 또는 일부 확장 이었기 때문에이 답변 을 수락했습니다. 그는 A) 이름이 있고 B)이를 수행하는 더 좋은 방법이 있다고 제안하면서 이것을 제공했습니다.

Benjamin Hodgson의 "서비스 로케이터"답변 여기에서 얻은 지식에 감사하는 한, "서비스 로케이터"가 아닙니다. "서비스 외관"입니다. 이 답변의 모든 것은 정확하지만 내 상황에는 맞지 않습니다.

USR의 답변

좀 더 자세히 다루겠습니다.

이런 식으로 많은 정적 정보를 포기합니다. 많은 동적 언어와 마찬가지로 런타임에 대한 결정을 연기하고 있습니다. 이렇게하면 정적 검증 (안전), 문서 및 툴링 지원 (자동 완성, 리팩토링, 용도 찾기, 데이터 흐름)을 잃게됩니다.

툴링을 잃지 않고 "정적"타이핑을 잃지 않습니다. 서비스 파사드는 DI 컨테이너에서 구성한 것을 반환합니다 default(T). 그리고 그것이 반환하는 것은 "typed"입니다. 반사가 캡슐화됩니다.

추가 서비스를 생성자 인수로 추가하는 것이 왜 큰 부담이되는지 모르겠습니다.

확실히 "드문"이 아닙니다. 기본 컨트롤러를 사용하고 있으므로 생성자를 변경해야 할 때마다 10, 100, 1000 다른 컨트롤러를 변경해야 할 수도 있습니다.

의존성 주입 프레임 워크를 사용하면 매개 변수 값을 수동으로 전달할 필요조차 없습니다. 그런 다음 다시 한 번 정적 이점을 잃지 만 많은 것은 아닙니다.

의존성 주입을 사용하고 있습니다. 그게 요점입니다.

마지막으로 Benjamin의 답변에 대한 Jules의 의견은 유연성을 잃지 않습니다. 그건 서비스 외관. GetService<T>DI 컨테이너를 구성 할 때와 마찬가지로 다른 구현을 구별하기 위해 많은 매개 변수를 추가 할 수 있습니다 . 그래서 예를 들어, 내가 바꿀 수 GetService<T>()하기 위해 GetService<T>(string extraInfo = null)이 "잠재적 인 문제"를 해결하기 위해.


어쨌든 모두에게 다시 한번 감사드립니다. 그것은이었다 정말 유용합니다. 건배.


4
여기에 서비스 외관이 있다는 데 동의하지 않습니다. 이 GetService<T>()방법은 임의의 의존성을 해결할 수 있습니다. 이것은 내가 대답 한 각주에서 설명했듯이 서비스 외관이 아닌 서비스 로케이터가 됩니다. @DocBrown이 제안한 것처럼 작은 세트 GetServiceX()/ GetServiceY()메소드로 바꾸면 Facade가 될 것입니다.
Benjamin Hodgson

2
이 기사 의 마지막 부분 인 Abstract Service Locator 에 대해 읽고주의를 기울여야합니다.이 부분 은 본질적으로 수행중인 작업입니다. 견적에 지불 특히주의 - 나는 손상이 방지 패턴 전체 프로젝트 보았다 "더 악화 당신이 모든 변경 사항의 의미를 파악하기 위해 뇌 전력의 상당한 양을 사용해야하므로 유지 보수 개발자로 당신의 인생을 만들 것 "
AlexFoxGill

3
정적 타이핑에서는 요점이 없습니다. DI를 사용하여 클래스에 필요한 생성자 매개 변수를 제공하지 않는 코드를 작성하려고하면 컴파일되지 않습니다. 그것은 문제에 대한 정적 컴파일 타임 안전입니다. 코드를 작성하고 생성자에게 실제로 필요한 인수를 제공하도록 올바르게 구성되지 않은 IContext를 제공하면 런타임에 컴파일되고 실패합니다.
Ben Aaronson

3
@ LiverpoolsNumber9 그렇다면 왜 강력한 형식의 언어를 사용합니까? 컴파일러 오류는 버그에 대한 첫 번째 방어선입니다. 단위 테스트는 두 번째입니다. 클래스 테스트와 종속성 사이의 상호 작용이기 때문에 단위 테스트로 선택되지 않으므로 세 번째 방어 라인 인 통합 테스트에 참여하게됩니다. 얼마나 자주 실행하는지 모르지만 이제 컴파일러 오류를 강조하는 데 IDE가 걸리는 시간 (밀리 초)이 아닌 몇 분 이상에 대한 피드백에 대해 이야기하고 있습니다.
Ben Aaronson

2
LiverpoolsNumber9의 잘못 @ : 당신의 검사 결과는 현재를 채울해야 IContext하고 주입하고, 결정적으로 : 새로운 의존성을 추가하는 경우, 코드는 여전히 컴파일 - 테스트가 런타임에 실패하더라도
AlexFoxGill
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.