올바르게 이해하면 Dependency Injection의 일반적인 메커니즘은 클래스 생성자를 통해 또는 클래스의 공용 속성 (구성원)을 통해 주입하는 것입니다.
이것은 주입되는 의존성을 드러내고 캡슐화의 OOP 원칙을 위반합니다.
이 트레이드 오프를 식별하는 것이 정확합니까? 이 문제를 어떻게 처리합니까?
아래의 내 질문에 대한 답변도 참조하십시오.
올바르게 이해하면 Dependency Injection의 일반적인 메커니즘은 클래스 생성자를 통해 또는 클래스의 공용 속성 (구성원)을 통해 주입하는 것입니다.
이것은 주입되는 의존성을 드러내고 캡슐화의 OOP 원칙을 위반합니다.
이 트레이드 오프를 식별하는 것이 정확합니까? 이 문제를 어떻게 처리합니까?
아래의 내 질문에 대한 답변도 참조하십시오.
답변:
이 문제를 살펴 보는 또 다른 방법이 있습니다.
IoC / 종속성 주입을 사용할 때는 OOP 개념을 사용하지 않습니다. 분명히 우리는 OO 언어를 '호스트'로 사용하고 있지만 IoC의 아이디어는 OO가 아닌 구성 요소 지향 소프트웨어 엔지니어링에서 비롯된 것입니다.
구성 요소 소프트웨어는 종속성 관리에 관한 것입니다. 일반적으로 사용되는 예는 .NET의 어셈블리 메커니즘입니다. 각 어셈블리는 참조하는 어셈블리 목록을 게시하므로 실행중인 응용 프로그램에 필요한 부분을 훨씬 더 쉽게 결합하고 검증 할 수 있습니다.
IoC를 통해 OO 프로그램에 유사한 기술을 적용함으로써 프로그램을보다 쉽게 구성하고 유지 관리 할 수 있습니다. 의존성 (생성자 매개 변수 등)을 게시하는 것이 이것의 핵심 부분입니다. 컴포넌트 / 서비스 지향 세계에서와 같이 캡슐화는 실제로 적용되지 않습니다. 세부 정보 유출을위한 '구현 유형'은 없습니다.
불행히도 우리의 언어는 현재 세밀하고 객체 지향적 인 개념을 거친 구성 요소 지향 개념과 분리하지 않으므로이 점을 염두에 두어야합니다. :)
그것은 좋은 질문 -하지만 어떤 점에서, 가장 순수한 형태의 캡슐 요구 객체가 의존성이 성취 한 그 어느 경우에 위반 될 수 있습니다. 종속성의 일부 제공자는 있어야 해당 개체가를 필요로 둘 것을 알고 Foo
, 그리고 공급자가 제공하는 방법이하는 Foo
개체에 있습니다.
일반적 으로이 후자의 경우는 생성자 인수 또는 setter 메소드를 통해 처리됩니다. 그러나 이것이 반드시 사실은 아닙니다. 예를 들어 Java의 최신 버전의 Spring DI 프레임 워크는 개인 필드에 주석을 달도록하고 (예 :로 @Autowired
) 종속성을 공개하지 않고도 리플렉션을 통해 종속성을 설정합니다. 공개 메소드 / 생성자 클래스 이것은 당신이 찾고있는 일종의 해결책 일 수 있습니다.
즉, 생성자 주입이 큰 문제라고 생각하지 않습니다. 나는 항상 생성 후에 객체가 완전히 유효해야한다고 느꼈다. 그래서 그들의 역할을 수행하기 위해 필요한 것 (즉, 유효한 상태에 있음)은 어쨌든 생성자를 통해 공급되어야한다. 공동 작업자가 작동 해야하는 객체가있는 경우 생성자 가이 요구 사항을 공개적으로 알리고 클래스의 새 인스턴스를 만들 때이 요구 사항을 충족시키는 것이 좋습니다.
이상적으로 객체를 다룰 때 인터페이스를 통해 객체와 상호 작용하고, 더 많은 작업을 수행할수록 (DI를 통해 종속성이 연결됨) 실제로 생성자를 직접 처리 할 필요가 없습니다. 이상적인 상황에서 코드는 클래스의 구체적인 인스턴스를 처리하지 않으며 심지어는 클래스를 작성하지도 않습니다. 따라서 IFoo
생성자 FooImpl
가 자신의 작업을 수행해야한다는 것을 걱정 하지 않고 실제로 FooImpl
존재 여부를 알지 못하고 DI를 통해 전달 됩니다. 이 관점에서 캡슐화는 완벽합니다.
이것은 물론 의견이지만, 제 생각에는 DI가 반드시 캡슐화를 위반하지는 않으며 실제로 내부에 필요한 모든 지식을 한 곳에 집중시켜 도움을 줄 수 있습니다. 이것은 그 자체로 좋은 일 일뿐 만 아니라 더 좋은 곳은 자신의 코드베이스 외부에 있으므로 작성하는 코드 중 어느 것도 클래스의 종속성에 대해 알 필요가 없습니다.
이것은 주입되는 의존성을 드러내고 캡슐화의 OOP 원칙을 위반합니다.
솔직히 말하면 모든 것이 캡슐화를 위반합니다. :) 일종의 부드러운 원칙으로 잘 다루어야합니다.
그렇다면 캡슐화를 위반하는 것은 무엇입니까?
상속 은 않습니다 .
상속은 서브 클래스가 부모 구현의 세부 사항에 서브 클래스를 노출시키기 때문에 종종 '상속이 캡슐화를 깨뜨린 다'고 말합니다. (Gang of Four 1995 : 19)
측면 지향 프로그래밍 은 . 예를 들어 onMethodCall () 콜백을 등록하면 이상한 부작용 등을 추가하여 정상적인 메소드 평가에 코드를 삽입 할 수 있습니다.
C ++에서 친구 선언은 않습니다 .
루비의 클래스 확장은 않습니다 . 문자열 클래스가 완전히 정의 된 후 어딘가에 문자열 메소드를 재정의하십시오.
음, 물건을 많이는 않습니다 .
캡슐화는 좋고 중요한 원칙입니다. 그러나 유일한 것은 아닙니다.
switch (principle)
{
case encapsulation:
if (there_is_a_reason)
break!
}
예, DI는 캡슐화 ( "정보 숨기기"라고도 함)를 위반합니다.
그러나 실제 문제는 개발자가 KISS (짧고 간결하게 유지) 및 YAGNI (You Ai n't Gonna Need It) 원칙을 위반하는 변명으로 사용하는 경우에 발생합니다.
개인적으로 저는 간단하고 효과적인 솔루션을 선호합니다. 나는 주로 "new"연산자를 사용하여 필요할 때마다 언제 어디서나 상태 저장 종속성을 인스턴스화합니다. 간단하고 캡슐화되어 있고 이해하기 쉽고 테스트하기 쉽습니다. 그래서 왜 안돼?
좋은 depenancy injection container / system은 생성자 주입을 허용합니다. 종속 개체는 캡슐화되며 공개적으로 노출 될 필요가 없습니다. 또한 DP 시스템을 사용하면 코드를 작성하는 개체를 포함하여 개체의 구성 방법에 대한 세부 정보를 "알지"않습니다. 거의 모든 코드가 캡슐화 된 객체에 대한 지식으로부터 보호 될뿐만 아니라 객체 구성에도 참여하지 않기 때문에이 경우 더 많은 캡슐화가 있습니다.
이제 생성 된 객체가 생성자에서 캡슐화 된 객체를 만드는 경우와 비교한다고 가정합니다. DP에 대한 나의 이해는 우리가이 책임을 대상으로부터 멀리하고 다른 사람에게주고 싶다는 것입니다. 이를 위해 DP 컨테이너 인 "다른 사람"은 캡슐화를 "위반"하는 친밀한 지식을 가지고 있습니다. 이점은 지식을 대상에서 벗어나는 것입니다. 누군가 가지고 있어야합니다. 나머지 응용 프로그램은 그렇지 않습니다.
의존성 주입 컨테이너 / 시스템은 캡슐화를 위반하지만 코드는 그렇지 않습니다. 사실, 코드는 그 어느 때보 다 "캡슐화"되어 있습니다.
Jeff Sternal이 질문에 대한 의견에서 지적했듯이 답변은 캡슐화 를 정의하는 방법에 전적으로 달려 있습니다 .
캡슐화가 의미하는 두 가지 주요 캠프가있는 것 같습니다.
File
객체에 방법이있을 수 있습니다 Save
, Print
, Display
, ModifyText
, 등이 두 정의는 서로 모순됩니다. File
개체 자체가 인쇄 될 수 있으면 프린터 동작에 크게 의존합니다. 다른 한편으로, 인쇄 할 수있는 것 ( 또는 그러한 인터페이스)에 대해서만 알고 있다면 IFilePrinter
, File
객체는 인쇄에 대해 아무것도 알 필요가 없으므로 작업을하면 객체에 대한 의존성이 줄어 듭니다.
따라서 첫 번째 정의를 사용하면 종속성 주입이 캡슐화를 중단합니다. 그러나 솔직히 나는 첫 번째 정의를 좋아하는지 모르겠습니다. 확실히 확장되지 않습니다 (그렇다면 MS Word는 하나의 큰 클래스 일 것입니다).
반면 에 캡슐화의 두 번째 정의를 사용하는 경우 종속성 주입은 거의 필수 입니다.
캡슐화를 위반하지 않습니다. 공동 작업자를 제공하고 있지만 수업에서 사용 방법을 결정합니다. 당신이 따르는 한 Tell은 묻지 말고 괜찮습니다. 나는 생성자 주입이 바람직하다고 생각하지만, 세터는 똑똑하고 좋은 한 괜찮을 수 있습니다. 즉, 클래스가 나타내는 불변량을 유지하는 논리가 포함되어 있습니다.
이것은 upvoted 답변과 비슷하지만 크게 생각하고 싶습니다. 아마도 다른 사람들도 이런 방식으로 볼 수 있습니다.
클래식 OO는 생성자를 사용하여 클래스 소비자에 대한 공개 "초기화"계약을 정의합니다 (모든 구현 세부 사항 숨기기, 일명 캡슐화). 이 계약은 인스턴스화 후 즉시 사용할 수있는 객체 (즉, 사용자가 기억해야 할 추가 초기화 단계가 없음)를 보장 할 수 있습니다.
(생성자) DI는 이 공개 생성자 인터페이스를 통해 구현 세부 정보 를 유출 하여 캡슐화를 부인할 수 없게 만듭니다. 사용자에 대한 초기화 계약을 정의하는 공용 생성자를 여전히 고려하는 한 끔찍한 캡슐화 위반을 만들었습니다.
이론적 예 :
클래스 Foo 에는 4 개의 메소드가 있으며 초기화를 위해 정수가 필요하므로 해당 생성자는 Foo (int size) 처럼 보이고 Foo가 작동하기 위해서는 인스턴스화시 크기 를 제공해야한다는 Foo 클래스 사용자에게 즉시 분명 합니다 .
Foo를 구현하려면 IWidget 이 필요할 수 있습니다 . 이 의존성을 생성자에 주입하면 Foo (int size, IWidget widget) 와 같은 생성자를 만들 수 있습니다.
무엇 이것에 대해 저를 irks 것은 지금 우리의 생성자가 혼합 종속성 초기화 데이터를 - 하나 개의 입력 클래스 (의 사용자에게 관심의 크기가 다른 경우에만 사용자를 혼동하는 역할을한다는 내부 의존성이다)와 구현 세부 사항 ( widget ).
size 매개 변수는 종속성이 아니며 인스턴스 별 초기화 값입니다. IoC는 외부 의존성 (위젯과 같은)에 적합하지만 내부 상태 초기화에는 적합하지 않습니다.
더 나쁜 것은 위젯이이 클래스의 4 가지 메소드 중 2 개에만 필요한 경우는 어떨까요? 위젯을 사용하지 않아도 위젯에 대한 인스턴스화 오버 헤드가 발생할 수 있습니다!
이것을 타협 / 조정하는 방법?
한 가지 접근 방식은 운영 계약을 정의하기 위해 인터페이스로만 전환하는 것입니다. 사용자가 생성자를 사용하지 않습니다. 일관성을 유지하려면 모든 객체는 인터페이스를 통해서만 액세스하고 IOC / DI 컨테이너와 같은 특정 형태의 리졸버를 통해서만 인스턴스화해야합니다. 컨테이너 만 물건을 인스턴스화합니다.
위젯 의존성을 처리하지만 Foo 인터페이스에서 별도의 초기화 방법을 사용하지 않고 어떻게 "크기"를 초기화합니까? 이 솔루션을 사용하면 인스턴스를 가져올 때 Foo 인스턴스가 완전히 초기화 될 수 없었습니다. Bummer, 나는 생성자 주입 의 아이디어와 단순함 을 정말로 좋아하기 때문 입니다.
초기화가 외부 의존성 이상일 때이 DI 세계에서 어떻게 초기화를 보장합니까?
순수한 캡슐화는 결코 달성 할 수없는 이상입니다. 모든 종속성이 숨겨져 있으면 DI가 전혀 필요하지 않습니다. 예를 들어 자동차 객체의 속도의 정수 값과 같이 객체 내에 내재화 할 수있는 개인 값이 있다면 외부 의존성이 없으며 해당 종속성을 반전하거나 주입 할 필요가 없습니다. 개인 함수에 의해 순수하게 작동하는 이러한 종류의 내부 상태 값은 항상 캡슐화하려는 것입니다.
그러나 특정 종류의 엔진 객체를 원하는 자동차를 제작하는 경우 외부 의존성이 있습니다. 자동차 객체의 생성자 내에서 해당 엔진 (예 : 새 GMOverHeadCamEngine ())을 인스턴스화하여 캡슐화를 유지하면서 콘크리트 클래스 GMOverHeadCamEngine에 훨씬 더 교묘 한 커플 링을 생성하거나 주입하여 Car 객체를 작동시킬 수 있습니다 예를 들어 구체적인 종속성이없는 인터페이스 IEngine과 무관하게 (그리고 훨씬 강력하게). IOC 컨테이너를 사용하든 간단한 DI를 사용하든 이것이 요점이 아닙니다. 요점은 많은 종류의 엔진을 결합하지 않고도 많은 종류의 엔진을 사용할 수있는 자동차를 보유하고 있기 때문에 코드베이스를보다 유연하고 부작용이 적습니다.
DI는 캡슐화 위반이 아니며, 사실상 모든 OOP 프로젝트 내에서 캡슐화가 반드시 깨질 때 커플 링을 최소화하는 방법입니다. 인터페이스에 외부로 의존성을 주입하면 커플 링 부작용을 최소화하고 클래스가 구현에 대해 무시할 수 있습니다.
의존성이 실제로 구현 세부 사항인지 또는 클라이언트가 어떤 식 으로든 알고 싶어하거나 필요로하는 것인지에 달려 있습니다. 관련된 한 가지는 클래스가 어떤 추상화 수준을 목표로 삼고 있는지입니다. 여기 몇 가지 예가 있어요.
후드 아래에서 캐싱을 사용하여 호출 속도를 높이는 메소드가있는 경우 캐시 오브젝트는 싱글 톤 또는 그 외의 것이어야 하며 주입 되어서는 안됩니다 . 캐시가 전혀 사용되지 않는다는 사실은 클래스의 클라이언트가 신경 쓸 필요가없는 구현 세부 사항입니다.
클래스가 데이터 스트림을 출력해야하는 경우 클래스가 결과를 배열, 파일 또는 다른 사람이 데이터를 보내려는 다른 위치로 쉽게 출력 할 수 있도록 출력 스트림을 삽입하는 것이 좋습니다.
회색 영역의 경우 몬테 카를로 시뮬레이션을 수행하는 클래스가 있다고 가정 해 봅시다. 임의의 근원이 필요합니다. 한편으로, 이것이 필요하다는 사실은 클라이언트가 무작위의 출처를 정확히 신경 쓰지 않는다는 점에서 구현 세부 사항입니다. 반면에, 실제 난수 생성기는 클라이언트가 제어하고 싶을 수있는 임의의 정도, 속도 등을 절충하고 클라이언트는 반복 가능한 동작을 얻기 위해 시드를 제어하기를 원할 수 있으므로 주입이 의미가있을 수 있습니다. 이 경우 난수 생성기를 지정하지 않고 클래스를 만드는 방법을 제안하고 스레드 로컬 Singleton을 기본값으로 사용하는 것이 좋습니다. 더 세밀한 제어가 필요한 경우, 무작위 소스를 주입 할 수있는 다른 생성자를 제공하십시오.
나는 단순함을 믿습니다. Domain 클래스에 IOC / Dependecy Injection을 적용해도 관계를 설명하는 외부 XML 파일을 가져 와서 코드를 훨씬 더 어렵게 만드는 것 외에는 개선되지 않습니다. EJB 1.0 / 2.0 및 struts 1.1과 같은 많은 기술은 XML에 넣은 내용을 줄이고 주석으로 코드에 넣어보십시오. 따라서 개발하는 모든 클래스에 IOC를 적용하면 코드가 의미가 없습니다.
IOC는 컴파일 타임에 종속 개체를 만들 준비가되지 않은 경우 이점이 있습니다. 이는 대부분의 인프라 추상 수준 아키텍처 구성 요소에서 발생할 수 있으며, 다른 시나리오에서 작동해야하는 공통 기본 프레임 워크를 설정하려고합니다. 그러한 장소에서는 IOC가 더 의미가 있습니다. 여전히 이것은 코드를보다 간단하고 유지 보수 할 수있게하지 않습니다.
다른 모든 기술과 마찬가지로이 기술에도 장점과 단점이 있습니다. 내 걱정은 최고의 컨텍스트 사용에 관계없이 모든 곳에서 최신 기술을 구현한다는 것입니다.
캡슐화는 클래스가 객체를 생성 할 책임이 있고 (구현 세부 사항에 대한 지식이 필요함) 클래스를 사용하는 경우에만 해당됩니다 (이 세부 사항에 대한 지식이 필요하지 않음). 이유를 설명하지만 먼저 빠른 자동차 해부학을 설명하겠습니다.
내가 1971 년 콤비를 운전할 때 가속기를 누를 수 있었고 (약간) 더 빨랐습니다. 이유를 알 필요는 없었지만 공장에서 Kombi를 만든 사람들은 그 이유를 정확히 알고있었습니다.
그러나 코딩으로 돌아갑니다. 캡슐화 는 "해당 구현을 사용하여 구현 세부 정보를 숨 깁니다". 클래스의 사용자가 모르게 구현 세부 사항을 변경할 수 있으므로 캡슐화가 좋습니다.
의존성 주입을 사용할 때, 생성자 주입은 서비스 유형 오브젝트 를 구성하는 데 사용됩니다 (상태를 모델링하는 엔티티 / 값 오브젝트와 반대). 서비스 유형 오브젝트의 모든 멤버 변수는 유출되지 않아야하는 구현 세부 사항을 나타냅니다. 소켓 포트 번호, 데이터베이스 자격 증명, 암호화를 수행하기 위해 호출 할 다른 클래스, 캐시 등
생성자는 클래스가 처음 생성 될 때 관련이 있습니다. DI 컨테이너 (또는 공장)가 모든 서비스 개체를 연결하는 동안 구성 단계에서 발생합니다. DI 컨테이너는 구현 세부 정보 만 알고 있습니다. Kombi 공장의 사람들이 점화 플러그에 대해 알고있는 것처럼 구현 세부 정보에 대해 모두 알고 있습니다.
런타임시 만들어진 서비스 개체를 실제 작업을 수행하기 위해 apon이라고합니다. 현재, 객체의 호출자는 구현 세부 사항을 전혀 모른다.
그게 내 콤비를 해변으로 몰고가는 중이 야
이제 캡슐화로 돌아갑니다. 구현 세부 사항이 변경되면 런타임에 해당 구현을 사용하는 클래스를 변경할 필요가 없습니다. 캡슐화가 깨지지 않았습니다.
새 차를 해변까지 운전할 수 있습니다. 캡슐화가 깨지지 않았습니다.
구현 세부 사항이 변경되면 DI 컨테이너 (또는 팩토리)를 변경해야합니다. 처음에는 공장에서 구현 세부 사항을 숨기려고 시도하지 않았습니다.
이 문제로 조금 더 어려움을 겪은 지금 나는 의존성 주입이 (현재) 캡슐화를 어느 정도 위반한다고 생각합니다. 그래도 오해하지 마십시오-의존성 주입을 사용하는 것이 대부분의 경우 트레이드 오프 가치가 있다고 생각합니다.
작업중인 구성 요소를 "외부"당사자에게 전달해야하는 경우 DI가 캡슐화를 위반하는 이유가 명확 해집니다 (고객을위한 라이브러리 작성 생각).
내 구성 요소가 생성자 (또는 공용 속성)를 통해 하위 구성 요소를 주입 해야하는 경우 보장 할 수 없습니다
"사용자가 구성 요소의 내부 데이터를 유효하지 않거나 일관성이없는 상태로 설정하지 못하게합니다."
동시에 그것은 말할 수 없다
"구성 요소 사용자 (다른 소프트웨어 조각)는 구성 요소의 기능 만 알면되고 구성 요소의 세부 사항에 의존 할 수 없습니다 . "
두 인용문은 모두 wikipedia 에서 온 것 입니다.
구체적인 예를 들면 다음과 같습니다. WCF 서비스 (기본적으로 원격 파사드)에 대한 통신을 단순화하고 숨기는 클라이언트 쪽 DLL을 제공해야합니다. 3 가지 WCF 프록시 클래스에 의존하기 때문에 DI 접근 방식을 사용하면 생성자를 통해 노출해야합니다. 그것으로 나는 숨기려고하는 통신 계층의 내부를 노출시킵니다.
일반적으로 나는 DI를 위해 모두입니다. 이 특별한 (극단적 인) 예에서, 그것은 나를 위험하게 만듭니다.
DI는 비공유 객체에 대한 캡슐화를 위반합니다. 공유 객체는 생성중인 객체 외부의 수명을 가지므로 생성중인 객체로 집계되어야합니다. 작성중인 오브젝트의 개인 오브젝트는 작성된 오브젝트로 작성되어야합니다. 작성된 오브젝트가 소멸되면 작성된 오브젝트를 가져옵니다. 인체를 예로 들어 봅시다. 구성 및 집계 대상 DI를 사용한다면 인체 생성자는 100 개의 객체를 가질 것입니다. 예를 들어, 많은 기관들이 (잠재적으로) 교체 가능합니다. 그러나 그들은 여전히 몸으로 구성되어 있습니다. 혈액 세포는 단백질 이외의 외부 영향없이 매일 신체에서 생성 (파괴)됩니다. 따라서 혈액 세포는 신체에 의해 내부적으로 생성됩니다-새로운 BloodCell ().
DI의 옹호자들은 개체가 절대 새로운 연산자를 사용해서는 안된다고 주장합니다. "순수한"접근 방식은 캡슐화를 위반할뿐만 아니라 개체를 생성하는 사람을위한 Liskov 대체 원칙도 위반합니다.
나는이 개념으로도 어려움을 겪었다. 처음에는 스프링과 같은 DI 컨테이너를 사용하여 후프를 통해 점프하는 것처럼 느껴지는 객체를 인스턴스화하는 '요구 사항'. 그러나 실제로는 실제로 후프가 아닙니다. 필요한 객체를 만드는 또 다른 '게시 된'방법 일뿐입니다. 물론, 캡슐화는 '파손'되어 누군가 '클래스 외부'가 필요한 것을 알고 있기 때문에 실제로는 시스템의 나머지 부분이 아니라 DI 컨테이너입니다. DI가 한 개체가 다른 개체를 필요로 '알고'있기 때문에 마술처럼 다르게 일어나는 일은 없습니다.
사실 그것은 공장과 리포지토리에 집중함으로써 더 나아질 것입니다. DI조차 전혀 알지 않아도됩니다! 그것은 뚜껑을 다시 캡슐에 넣습니다. 아휴!
추신. 의존성 주입 을 제공 한다고 해서 반드시 캡슐화를 중단 할 필요는 없습니다 . 예:
obj.inject_dependency( factory.get_instance_of_unknown_class(x) );
클라이언트 코드는 여전히 구현 세부 사항을 모릅니다.
어쩌면 이것은 순진한 생각일지도 모르지만 정수 매개 변수를 사용하는 생성자와 매개 변수로 서비스를 생성하는 생성자의 차이점은 무엇입니까? 이것은 새 객체 외부에서 정수를 정의하고 객체로 공급하면 캡슐화가 중단된다는 것을 의미합니까? 서비스가 새 객체 내에서만 사용되는 경우 캡슐화가 어떻게 중단되는지 알 수 없습니다.
또한 일종의 자동 배선 기능 (예 : C #의 경우 Autofac)을 사용하여 코드를 매우 깨끗하게 만듭니다. Autofac 빌더를위한 확장 메소드를 구축함으로써 종속성 목록이 커짐에 따라 시간이 지남에 따라 유지해야 할 많은 DI 구성 코드를 잘라낼 수있었습니다.
나는 최소한 DI가 캡슐화를 크게 약화시키는 것이 자명하다고 생각합니다. 여기에 추가로 고려해야 할 DI의 다른 단점이 있습니다.
코드를 재사용하기 어렵게 만듭니다. 클라이언트가 명시 적으로 종속성을 제공하지 않고 사용할 수있는 모듈은 클라이언트가 해당 구성 요소의 종속성이 무엇인지 발견 한 다음 사용 가능하게 만드는 모듈보다 사용하기 쉽습니다. 예를 들어 원래 ASP 응용 프로그램에서 사용하도록 만들어진 구성 요소는 클라이언트 http 요청과 관련된 수명을 개체 인스턴스에 제공하는 DI 컨테이너가 제공하는 종속성을 가질 수 있습니다. 원래 ASP 응용 프로그램과 동일한 기본 제공 DI 컨테이너와 함께 제공되지 않는 다른 클라이언트에서는 재현하기가 쉽지 않을 수 있습니다.
코드를 더 취약하게 만들 수 있습니다. 인터페이스 사양에서 제공하는 종속성은 예상치 못한 방식으로 구현 될 수있어 정적으로 해결 된 구체적인 종속성으로는 불가능한 전체 런타임 버그 클래스를 발생시킵니다.
코드의 작동 방식에 대한 선택의 폭이 줄어든다는 점에서 코드의 유연성이 떨어질 수 있습니다. 모든 클래스가 소유 인스턴스의 전체 수명 동안 존재하는 모든 종속성을 가질 필요는 없지만 많은 DI 구현에서는 다른 옵션이 없습니다.
그런 점을 염두에두고 가장 중요한 질문은 " 특정 의존성을 전혀 외부 적으로 지정해야합니까? . "라고 생각합니다. 실제로 테스트를 지원하기 위해 외부에서 종속성을 제공해야 할 필요는 거의 없었습니다.
의존성이 진정으로 외부에 제공되어야하는 경우, 이는 일반적으로 객체 간의 관계가 내부 의존성이 아니라 협업이라는 것을 암시합니다.이 경우 적절한 목표는 다음과 같이 캡슐화됩니다. 한 클래스를 다른 클래스 내부에 캡슐화하는 것이 아니라 각 클래스의 캡슐화입니다 .
내 경험상 DI 사용에 관한 주요 문제는 DI가 내장 된 응용 프로그램 프레임 워크로 시작하거나 코드베이스에 DI 지원을 추가하는지 여부입니다. 어떤 이유로 사람들은 DI 지원이 있기 때문에 정확해야한다고 가정합니다 모든 것을 인스턴스화 하는 방법 . 그들은 "이 의존성을 외부 적으로 명시 할 필요가 있는가?"라는 질문을 결코 귀찮게하지 않습니다. 그리고 더 나빠, 그들은 또한 다른 사람들이 DI 지원을 사용하도록 강요하기 시작합니다. 도 모든 것을 것 입니다.
결과적으로 코드베이스에서 인스턴스를 생성하는 데 어려움이있는 DI 컨테이너 구성이 필요하고 상태를 식별하기위한 추가 워크로드가 있기 때문에 디버깅이 두 배 더 어려운 상태로 코드베이스가 전개되기 시작합니다. 무엇을 인스턴스화했는지
그래서 질문에 대한 나의 대답은 이것입니다. DI가 자신을 위해 해결하는 실제 문제를 식별 할 수있는 곳에서는 다른 방법으로는 더 이상 해결할 수 없습니다.
DI를 극단적으로 취하면 캡슐화를 위반할 수 있다는 데 동의합니다. 일반적으로 DI는 실제로 캡슐화되지 않은 종속성을 노출합니다. 다음은 Miško Hevery의 싱글 톤이 병리학 거짓말 쟁이 에서 빌린 간단한 예입니다. .
CreditCard 테스트로 시작하여 간단한 단위 테스트를 작성하십시오.
@Test
public void creditCard_Charge()
{
CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
c.charge(100);
}
다음 달에 $ 100의 청구서를받습니다. 왜 요금이 청구 되었습니까? 단위 테스트는 프로덕션 데이터베이스에 영향을 미쳤습니다. 내부적으로 CreditCard는을 호출합니다 Database.getInstance()
. 신용 카드를 리팩터링 DatabaseInterface
하여 생성자에 포함 되도록하면 종속성이 있다는 사실이 노출됩니다. 그러나 CreditCard 클래스가 외부에서 보이는 부작용을 일으키기 때문에 종속성이 시작되도록 캡슐화되지 않았다고 주장합니다. 리팩토링없이 CreditCard를 테스트하려면 의존성을 확실히 관찰 할 수 있습니다.
@Before
public void setUp()
{
Database.setInstance(new MockDatabase());
}
@After
public void tearDown()
{
Database.resetInstance();
}
데이터베이스를 종속성으로 노출하면 캡슐화가 줄어드는 지 걱정할 필요가 없습니다. 디자인이 우수하기 때문입니다. 모든 DI 결정이 그렇게 쉬운 것은 아닙니다. 그러나 다른 답변은 반대의 예를 보여주지 않습니다.
CreditCard
클래스 외부에서 변경하지 않고도 어떻게 WebAPI에서 PayPal로 객체를 변경할 수 있습니까?
나는 그것이 범위의 문제라고 생각합니다. 캡슐화를 정의 할 때 (방법을 알려주지 않음) 캡슐화 된 기능을 정의해야합니다.
그대로 클래스 : 캡슐화하는 것은 클래스의 유일한 책임입니다. 수행 방법을 알고있는 것. 예를 들어 정렬. 주문을 위해 비교기를 주입하면 클라이언트가 캡슐화 된 것의 일부가 아닌 빠른 정렬이라고 가정 해 봅시다.
구성된 기능 : 즉시 사용 가능한 기능을 제공하려면 QuickSort 클래스를 제공하는 것이 아니라 Comparator로 구성된 QuickSort 클래스의 인스턴스를 제공하는 것입니다. 이 경우 생성 및 구성을 담당하는 코드는 사용자 코드에서 숨겨져 야합니다. 이것이 캡슐화입니다.
클래스를 프로그래밍 할 때는 클래스에 단일 책임을 구현하는 것입니다. 옵션 1을 사용하고 있습니다.
응용 프로그램을 프로그래밍 할 때 유용한 콘크리트 를 수행하는 무언가를 만드는 것입니다. 작업을 옵션 2를 반복적으로 사용합니다.
이것은 구성된 인스턴스의 구현입니다.
<bean id="clientSorter" class="QuickSort">
<property name="comparator">
<bean class="ClientComparator"/>
</property>
</bean>
이것은 다른 클라이언트 코드가 사용하는 방법입니다.
<bean id="clientService" class"...">
<property name="sorter" ref="clientSorter"/>
</bean>
구현을 변경해도 ( clientSorter
빈 정의 를 변경해도 ) 클라이언트 사용을 방해하지 않기 때문에 캡슐화됩니다 . xml 파일을 모두 함께 작성하면 모든 세부 사항을 볼 수 있습니다. 그러나 클라이언트 코드 ( ClientService
)
는 분류기에 대해 아무것도 모릅니다 .
아마 언급 할만큼 가치의 Encapsulation
다소 관점 의존한다.
public class A {
private B b;
public A() {
this.b = new B();
}
}
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
반에서 일하는 누군가의 관점 A
에서, 두 번째 예 A
에서는this.b
DI가없는 반면
new A()
vs
new A(new B())
이 코드를보고있는 사람 A
은 두 번째 예의 특성에 대해 더 잘 알고 있습니다 .
DI를 사용하면 유출 된 모든 지식이 최소한 한 곳에 있습니다.