2012-05-24 7 views
8

매우 큰 레거시 프로젝트를 테스트하려고합니다.DI 컨테이너 대신 ThreadStatic을 사용하는 것이 잘못된 이유

우리는 대부분의 코드에서 사용하는 정적으로 사용할 수있는 서비스가 많이 있습니다. 문제는 이들을 조롱하기가 어렵다는 것입니다. 그들은 싱글 톤이었습니다. 이제는 가짜 단일체 - 동일한 정적 인터페이스이지만 함수는 전환 할 수있는 인스턴스 객체에 위임합니다. 이처럼 내 단위 테스트에서 지금

class ServiceEveryoneNeeds 
{ 
    public static IImplementation _implementation = new RealImplementation(); 

    public IEnumerable<FooBar> GetAllTheThings() { return _implementation.GetAllTheThings(); } 
} 

:

void MyTest() 
{ 
    ServiceEveryoneNeeds._implementation = new MockImplementation(); 
} 

지금까지 너무 좋아. 자극적 인면에서 우리는 단지 하나의 구현만을 필요로합니다. 그러나 테스트를 병렬로 실행하고 다른 모의 객체를해야 할 수도 있습니다, 그래서이 한 :

class Dependencies 
{ 
    //set this in prod to the real impl 
    public static IImplementation _realImplementation; 

    //unit tests set these 
    [ThreadStatic] 
    public static IImplementation _mock; 

    public static IImplementation TheImplementation 
    { get {return _realImplementation ?? _mock; } } 

    public static void Cleanup() { _mock = null; } 
} 

그리고 다음을 : 그것은 모든 클래스에 이러한 서비스를 주입 생성자에 대규모 프로젝트이기 때문에 우리는이 길을 갔다

class ServiceEveryoneNeeds 
{ 
    static IImplementation GetImpl() { return Dependencies.TheImplementation; } 

    public static IEnumerable<FooBar> GetAllTheThings() {return GetImpl().GetAllTheThings(); } 

} 

//and 
void MyTest() 
{ 
    Dependencies._mock = new BestMockEver(); 
    //test 
    Dependencies.Cleanup(); 
} 

그것은 그들을 필요로합니다. 동시에, 이들은 대부분의 기능이 의존하는 우리의 코드베이스 내의 보편적 인 서비스입니다.

이 패턴은 의존성을 숨기는 개념으로, 의존성을 명시 적으로 만드는 생성자 삽입과 반대되는 것으로 알고 있습니다.

혜택은 그러나 :
- 우리가 바로 단위 테스트를 시작, 대 3 개월 리팩터링하고 단위 테스트를 수행 할 수 있습니다.
- 우리는 여전히 전역 변수를 가지고 있지만 이것이 우리가 있었던 곳보다 엄격하게 더 나은 것처럼 보입니다.

우리의 의존성은 여전히 ​​암묵적이지만, 나는이 접근법이 우리가 가진 것보다 엄격하게 우수하다고 주장 할 것입니다. 숨겨진 의존성을 제외하고, 적절한 DI 컨테이너를 사용하는 것보다 더 나쁜가? 어떤 문제에 빠지게 될까요?

답변

4

해당 a service locator which is bad입니다. 그러나 당신은 이미 그것을 알고 있습니다. 코드 기반이 엄청나다면 부분 마이그레이션을 시작하십시오. 컨테이너에 싱글 톤 인스턴스를 등록하고 코드에서 클래스를 터치 할 때마다 생성자를 삽입하십시오. 그런 다음 대부분의 부품을 (잘하면) 작동 상태로 유지하고 어디에서나 DI의 이점을 얻을 수 있습니다.

DI가없는 부품은 이상적으로 시간이 지나면 줄어들 수 있습니다. 그리고 바로 테스트를 시작할 수 있습니다.

+3

아니요, 서비스 로케이터가 아닙니다. 이것은 주변 환경입니다. 사소한 차이점은 ServiceLocator가 모든 유형을 제공하도록 설계된 경우 유형이 잘 정의되어 있다는 점입니다.그리고 로컬 기본값이 있으므로 코드가 기본 동작없이 구성없이 실행되지만 로컬 기본 값을 바꿔 다른 동작을 얻을 수 있습니다. 올바른 상황에서 사용하면 좋지 않습니다. 상황이 매우 드물기는하지만. –

+1

예, 기술적으로 당신은 절대적으로 옳습니다. 그러나 개인적으로 나는 주변 상황으로서 완전한 서비스를 언급하지 않을 것이다. Mark Seemann의'TimeProvider' 샘플은 작고 잘 정의 된 범위를 가지고 있습니다. 'ServiceEveryOneNeeds'는 ... 그렇게별로 들리지 않습니다. –

1

DI 컨테이너를 사용하는 종속성 주입과 주입은 실제로는 별개이지만 다른 하나는 자연스럽게 이어집니다. DI 컨테이너를 사용하면 코드에 특정 구조가 있음을 의미합니다. 이러한 구조는 읽기 쉽고 숨겨진 종속성에 대한 깊은 지식 없이도 쉽게 작업 할 수 있으므로 유지 관리가 용이합니다.

이제 Concretion에 의존하지 않으므로 제어 반전의 양식을 구현했습니다. 나는 그것이 더 나은 디자인이라고 생각하며, 코드를 더 테스트 가능하게 만들기위한 좋은 출발점을 제시한다. 이 단계에서 즉각적인 가치를 얻은 것 같습니다.

암시 적 종속성 (즉, DI 대 주변 환경)보다 명시 적 종속성을 갖는 것이 더 좋습니까? 나는 그렇다고 말하는 경향이 있지만 비용 대비 이익에 달려 있습니다. 이점은 버그를 도입하는 비용, 코드에서 확인할 가능성이 얼마나 많은 것, 디버그하는 것이 얼마나 어려운지, 누가 유지 관리 할 것인지, 예상 수명을 어떻게 유지할 것인가 등에 달려 있습니다.

전역 가변 정적 상태는 항상 나쁜 것입니다. 영리한 영혼 중 일부는 전화를 걸면 글로벌 서비스 구현을 바꿔야한다고 결정한 다음 나중에 교체해야합니다. 나중에 정리하지 않으면 잘못 될 수 있습니다. 그것은 바보 같은 예일 수 있지만 그러한 의도하지 않은 부작용은 언제나 나쁘다. 따라서 완전히 의도적으로 제거하는 것이 낫다. 훈계와 경계심으로 그들을 막을 수는 있지만 더 힘들어집니다.

4

ambient context이라고합니다. 주변 환경을 올바르게 사용하고 사용하면 문제가 없습니다. 주변 컨텍스트를 사용할 수있다 몇 가지 전제 조건이 있습니다

  1. 그것은
  2. 당신은 당신이 null 될 수 있는지 확인해야합니다
  3. 로컬 기본을 할 몇 가지 값을 반환하는 교차 절단 우려해야 할당 됨. 대신 Null implementation을 사용하십시오.

값을 반환하지 않는 교차 절단 관련 문제에 대해 예를 들어. 로깅하면 차단을 선호해야합니다. 교차 컷 관계가 아닌 다른 종속성의 경우 생성자 주입을 수행해야합니다.

구현에는 몇 가지 문제가 있습니다 (null 할당, 명명되지 않음, 기본값 없음). 여기 당신이 그것을 구현할 수있는 방법은 다음과 같습니다

public class SomeCrossCuttingConcern 
{ 
    private static ISomeCrossCuttingConcern default = new DefaultSomeCrossCuttingConcern(); 

    [ThreadStatic] 
    private static ISomeCrossCuttingConcern current; 

    public static ISomeCrossCuttingConcern Default 
    { 
     get { return default; } 
     set 
     { 
      if (value == null) 
       throw new ArgumentNullException(); 
      default = value; 
     } 
    } 

    public static ISomeCrossCuttingConcern Current 
    { 
     get 
     { 
      if (current == null) 
       current = default; 
      return current; 
     } 

     set 
     { 
      if (value == null) 
       throw new ArgumentNullException(); 
      current = value; 
     } 
    } 

    public static void ResetToDefault() { current = null; } 
} 

앰비언트 컨텍스트는 크로스 커팅 문제에 대한 귀하의 API를 오염시키지 않는 장점이있다.

그러나 테스트와 관련하여 테스트가 종속 될 수 있습니다. 예 : 한 모의 테스트를 위해 모의문을 설치하는 것을 잊어 버린 경우 모의 테스트가 전에 다른 테스트로 설정 되었다면 제대로 실행됩니다. 그러나 독립 실행 형 또는 다른 순서로 실행되면 실패합니다. 그것은 테스트를 더욱 어렵게 만듭니다.

1

당신이하고있는 일이 나쁘지 않다고 생각합니다. 당신은 코드 기반을 테스트 가능하게하려고 노력하고 있으며, 그 트릭은 작은 단계로 그렇게하는 것입니다. Working Effectively With Legacy Code을 읽을 때도 이와 동일한 조언을 얻습니다. 그러나 당신이하는 일의 단점은 depedency injection을 사용하기 시작하면 다시 코드베이스를 리팩터링해야한다는 것입니다. 하지만 더 중요한 것은 많은 테스트 코드를 변경해야한다는 것입니다.

나는 Alex에 동의합니다. 앰비언트 컨텍스트 대신 생성자 주입을 사용하는 것이 좋습니다. 이것에 대한 코드베이스를 직접적으로 리팩터링 할 필요는 없지만 생성자 삽입은 호출 스택을 '버블 링 (bubble)'할 것이므로 버블 링을 방지하기 위해 어딘가에 '잘라 내기'를해야합니다. 코드베이스 전반에 걸쳐 많은 변화가있었습니다.

현재 레거시 코드 기반으로 작업 중이며 DI 컨테이너 (통증)를 사용할 수 없습니다. 아직도 내가 할 수있는 곳에 컨스트럭터 인젝션을 사용하는데, 때때로 어떤 타입에서는 poor mans dependency injection을 사용하여 되돌려 야한다는 것을 의미한다. 이것은 '생성자 주입 버블'을 멈추는 데 사용하는 트릭입니다. 여전히 이것은 주변 환경을 사용하는 것보다 훨씬 낫습니다. 가난한 사람의 DI는 하위 수준이지만 여전히 적절한 단위 테스트를 작성할 수 있으며 나중에 나중에 기본 생성자를 제거하는 것이 훨씬 쉬워집니다.

관련 문제