퍼블릭 API 디자인으로 의존성 주입 균형 조정


13

간단한 고정 공개 API를 제공하면서 종속성 주입을 사용하여 테스트 가능한 디자인의 균형을 맞추는 방법을 고민했습니다. 내 딜레마는 사람들은 무언가를하고 싶을 수도 var server = new Server(){ ... }있고 많은 의존성과 의존성 그래프를 만드는 것에 대해 걱정할 필요 Server(,,,,,,)가 없다는 것입니다. 개발하는 동안 IoC / DI 프레임 워크를 사용하여 모든 것을 처리하기 때문에 너무 걱정하지 않아도됩니다 (모든 컨테이너의 수명주기 관리 측면을 사용하지 않으므로 더 복잡합니다).

이제 종속성이 다시 구현되지 않을 것입니다. 이 경우 컴포넌트 화는 확장 등을위한 이음새를 만드는 것이 아니라 테스트 가능성 (그리고 괜찮은 디자인)을위한 것입니다. 사람들은 99.999 %의 시간을 기본 구성으로 사용하려고합니다. 그래서. 종속성을 하드 코딩 할 수 있습니다. 그렇게하고 싶지 않다면 테스트를 잃게됩니다! 하드 코딩 된 종속성과 종속성을 취하는 기본 생성자를 제공 할 수 있습니다. 그것은 ... 지저분하고 혼란 스럽지만 실행 가능할 것입니다. 종속성 수신 생성자를 내부로 만들고 단위 테스트를 친구 어셈블리 (C # 가정)로 만들 수 있습니다. 공용 API를 정리하지만 유지 관리를 위해 숨겨져있는 숨겨진 함정을 남겨 둡니다. 명시 적으로보다는 암시 적으로 연결된 두 개의 생성자를 갖는 것은 내 책에서 일반적으로 나쁜 디자인입니다.

지금은 내가 생각할 수있는 최소한의 악에 관한 것입니다. 의견? 지혜?

답변:


11

의존성 주입은 잘 사용될 때 강력한 패턴이지만, 종종 그 실무자들은 외부 프레임 워크에 의존합니다. 결과 API는 XML 덕트 테이프와 함께 앱을 느슨하게 묶고 싶지 않은 사람들에게는 매우 고통 스럽습니다. POO (Plain Old Objects)를 잊지 마십시오.

먼저 프레임 워크없이 누군가가 API를 사용하는 방법을 고려하십시오.

  • 서버를 어떻게 인스턴스화합니까?
  • 서버를 어떻게 확장합니까?
  • 외부 API에서 무엇을 노출 하시겠습니까?
  • 외부 API에서 무엇을 숨기고 싶 습니까?

구성 요소에 기본 구현을 제공한다는 아이디어와 이러한 구성 요소가 있다는 사실이 마음에 듭니다. 다음 접근법은 완벽하게 유효하고 적절한 OO 실습입니다.

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

API 문서에 컴포넌트의 기본 구현이 무엇인지 문서화하고 모든 설정을 수행하는 하나의 생성자를 제공하는 한 괜찮습니다. 설정 코드가 하나의 마스터 생성자에서 발생하는 한 계단식 생성자는 취약하지 않습니다.

기본적으로 모든 공개 생성자는 노출하려는 구성 요소의 조합이 다릅니다. 내부적으로 기본값을 채우고 설정을 완료하는 개인 생성자를 연기합니다. 본질적으로 이것은 약간의 DRY를 제공하며 테스트하기가 매우 쉽습니다.

friend테스트를 위해 서버 개체를 격리하기 위해 서버 개체를 설정하기 위해 테스트 에 대한 액세스 권한 을 제공해야하는 경우 계속 진행하십시오. 공개 API의 일부가 아니므로주의해야합니다.

API 소비자에게 친절하고 라이브러리를 사용하기 위해 IoC / DI 프레임 워크가 필요 하지 않습니다 . 당신이 겪고있는 고통을 느끼기 위해 단위 테스트가 IoC / DI 프레임 워크에 의존하지 않는지 확인하십시오. (개인용 애완 동물이지만 프레임 워크를 도입하자마자 더 이상 단위 테스트가 아니며 통합 테스트가됩니다).


나는 동의한다. 그리고 나는 IoC 구성 수프의 팬이 아니다-XML 구성은 IoC가 관련된 모든 곳에서 사용하는 것이 아니다. 확실히 IoC 사용을 요구하는 것은 내가 피하는 것입니다. 공공 API 디자이너가 결코 할 수없는 일종의 가정입니다. 의견에 감사드립니다, 그것은 내가 생각하고 있던 선을 따라 왔으며 계속해서 위생 검사를받는 것이 안심입니다!
kolektiv

-1이 답변은 기본적으로 "종속성 주입을 사용하지 마십시오"라고 말하고 부정확 한 가정을 포함합니다. 예제에서 HttpListener생성자가 변경되면 Server클래스도 변경해야합니다. 그들은 결합되어 있습니다. 더 나은 해결책은 @Winston Ewert가 아래에 말한 것입니다. 이는 라이브러리의 API 외부에 미리 지정된 종속성을 가진 기본 클래스를 제공하는 것입니다. 그런 다음 프로그래머가 결정을 내리기 때문에 모든 종속성을 설정하지 않아도되지만 나중에 변경할 수있는 유연성이 있습니다. 타사 종속성이있는 경우 이는 큰 문제입니다.
M. Dudley

제공된 예를 "바스타드 주입"이라한다. stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley

1
@MichaelDudley, OP의 질문을 읽었습니까? 모든 "가정"은 OP가 제공 한 정보를 기반으로합니다. 한동안, 당신이 호출 한 "자식 주입"은 선호되는 의존성 주입 형식이었습니다. 특히 런타임 중에 구성이 변경되지 않는 시스템에 적합합니다. 세터와 게터는 또 다른 형태의 의존성 주입입니다. OP가 제공 한 것을 기반으로 예제를 사용했습니다.
베린 로리 치

4

이러한 경우 Java에서는 팩토리에서 빌드 한 "기본"구성을 갖는 것이 일반적이며 실제 "적절한"동작 서버와 독립적으로 유지됩니다. 따라서 사용자는 다음과 같이 씁니다.

var server = DefaultServer.create();

그동안 Server의 생성자는 여전히 모든 종속성을 허용하고 깊은 사용자 정의에 사용할 수 있습니다.


SRP +1 치과 의사 사무실의 책임은 치과 의사 사무실을 짓는 것이 아닙니다. 서버의 책임은 서버를 구축하지 않는 것입니다.
R. Schmitz

2

"public api"를 제공하는 서브 클래스를 제공하는 것은 어떻습니까?

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

사용자는 new StandardServer()자신의 길을 가고 있습니다. 또한 서버 기능에 대한 제어를 강화하려는 경우 서버 기본 클래스를 사용할 수도 있습니다. 이것은 내가 사용하는 접근법입니다. (아직 요점을 보지 못했기 때문에 프레임 워크를 사용하지 않습니다.)

이것은 여전히 ​​내부 API를 노출하지만 당신이해야한다고 생각합니다. 개체를 독립적으로 작동해야하는 다른 유용한 구성 요소로 분할했습니다. 타사가 구성 요소 중 하나를 단독으로 사용하고 싶을 때는 알 수 없습니다.


1

종속성 수신 생성자를 내부로 만들고 단위 테스트를 친구 어셈블리 (C # 가정)로 만들 수 있습니다. 공용 API를 정리하지만 유지 관리를 위해 숨겨져있는 숨겨진 함정을 남겨 둡니다.

그것은 "나쁜 숨겨진 함정"처럼 보이지 않습니다. 퍼블릭 생성자는 "기본"종속성으로 내부 생성자를 호출해야합니다. 퍼블릭 생성자를 수정해서는 안되는 것이 확실하다면 모든 것이 정상입니다.


0

나는 당신의 의견에 전적으로 동의합니다. 단위 테스트 목적으로 만 구성 요소 사용 경계를 오염시키고 싶지 않습니다. 이것이 DI 기반 테스트 솔루션의 모든 문제입니다.

InjectableFactory / InjectableInstance 패턴 을 사용하는 것이 좋습니다 . InjectableInstance변경 가능한 값 홀더 인 재사용이 가능한 간단한 유틸리티 클래스입니다 . 컴포넌트 인터페이스에는 기본 구현으로 초기화 된이 값 홀더에 대한 참조가 포함됩니다. 인터페이스는 값 홀더에 위임 하는 단일 메소드 get () 을 제공합니다 . 컴포넌트 클라이언트는 직접 의존성을 피하기 위해 구현 인스턴스를 얻기 위해 new 대신 get () 메소드를 호출합니다 . 테스트 시간 동안 선택적으로 기본 구현을 모의로 바꿀 수 있습니다. 다음은 TimeProvider 예를 하나 사용하겠습니다.클래스는 Date () 의 추상화 이므로 단위 테스트로 주입하고 조롱하여 다른 순간을 시뮬레이션 할 수 있습니다. Java에 대해 더 잘 알고 있으므로 여기에서 java를 사용합니다. C #은 비슷해야합니다.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance는 구현하기가 쉽고 Java 로 참조 구현 이 있습니다. 자세한 내용은 내 블로그 게시물 인젝터 블 팩토리의 의존성 간접 참조를 참조하십시오.


그래서 ... 의존성 주입을 가변 싱글 톤으로 대체합니까? 끔찍하게 들립니다. 독립적 인 테스트를 어떻게 수행 하시겠습니까? 각 테스트마다 새로운 프로세스를 열거 나 특정 순서로 강제 실행합니까? Java는 나의 강력한 소송이 아닙니다. 내가 잘못 이해 했습니까?
nvoigt

Java maven 프로젝트에서 junit 엔진은 기본적으로 다른 클래스 로더에서 각 테스트를 실행하기 때문에 공유 인스턴스는 문제가되지 않지만 동일한 클래스 로더를 재사용 할 수 있으므로 일부 IDE에서는이 문제가 발생합니다. 이 문제를 해결하기 위해 클래스는 각 테스트 후 원래 상태로 재설정하기 위해 호출되는 재설정 메소드를 제공합니다. 실제로 다른 테스트에서 다른 모의를 사용하려는 경우는 거의 없습니다. 우리는 수년간 대규모 프로젝트에이 패턴을 적용했습니다. 모든 것이 순조롭게 실행되며, 이식성과 테스트 가능성을 희생하지 않고 읽기 쉽고 깨끗한 코드를 즐겼습니다.
Jianwu Chen
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.