Programming

의존성 주입은 캡슐화를 희생해야합니까?

procodes 2020. 7. 10. 21:23
반응형

의존성 주입은 캡슐화를 희생해야합니까?


올바르게 이해하면 Dependency Injection의 일반적인 메커니즘은 클래스 생성자를 통해 또는 클래스의 공용 속성 (구성원)을 통해 주입하는 것입니다.

이것은 주입되는 의존성을 드러내고 캡슐화의 OOP 원칙을 위반합니다.

이 트레이드 오프를 식별하는 것이 정확합니까? 이 문제를 어떻게 처리합니까?

아래의 내 질문에 대한 답변도 참조하십시오.


이 문제를 살펴 보는 또 다른 방법이 있습니다.

IoC / 종속성 주입을 사용할 때는 OOP 개념을 사용하지 않습니다. 우리는 OO 언어를 '호스트'로 사용하고 있지만 IoC의 아이디어는 OO가 아닌 구성 요소 지향 소프트웨어 엔지니어링에서 비롯된 것입니다.

구성 요소 소프트웨어는 종속성 관리에 관한 것입니다. 일반적으로 사용되는 예는 .NET의 어셈블리 메커니즘입니다. 각 어셈블리는 참조하는 어셈블리 목록을 게시하므로 실행중인 응용 프로그램에 필요한 부분을 훨씬 쉽게 함께 가져 와서 검증 할 수 있습니다.

IoC를 통해 OO 프로그램에 유사한 기술을 적용함으로써 프로그램을보다 쉽게 ​​구성하고 유지 관리 할 수 ​​있습니다. 의존성 (생성자 매개 변수 등)을 게시하는 것이 이것의 핵심입니다. 컴포넌트 / 서비스 지향 세계에서와 같이 캡슐화는 실제로 적용되지 않습니다. 세부 정보 유출을위한 '구현 유형'이 없습니다.

불행히도 우리의 언어는 현재 세밀하고 객체 지향적 인 개념을 거친 구성 요소 지향 개념과 분리하지 않으므로이 점을 염두에 두어야합니다. :)


그것은 좋은 질문 -하지만 어떤 점에서, 가장 순수한 형태의 캡슐 요구 객체가 의존성이 성취 한 그 어느 경우에 위반 될 수 있습니다. 의존성의 일부 제공자는 문제의 객체가을 필요로 하고 제공자가 객체 를 제공하는 방법을 가져야 한다는 것을 알고 있어야 합니다.FooFoo

일반적 으로이 후자의 경우는 생성자 인수 또는 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이 질문에 대한 의견에서 지적했듯이 답변은 캡슐화 를 정의하는 방법에 전적으로 달려 있습니다 .

캡슐화가 의미하는 두 가지 주요 캠프가있는 것 같습니다.

  1. 객체와 관련된 모든 것은 객체에 대한 방법입니다. 그래서하는 File객체에 방법이있을 수 있습니다 Save, Print, Display, ModifyText, 등
  2. 물체는 그 자체의 작은 세계이며 외부 행동에 의존하지 않습니다.

이 두 정의는 서로 모순됩니다. 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 접근 방식을 사용하면 생성자를 통해 노출해야합니다. 그걸로 숨기려고하는 통신 계층의 내부를 노출시킵니다.

Generally I am all for DI. In this particular (extreme) example, it strikes me as dangerous.


I struggled with this notion as well. At first, the 'requirement' to use the DI container (like Spring) to instantiate an object felt like jumping thru hoops. But in reality, it's really not a hoop - it's just another 'published' way to create objects I need. Sure, encapsulation is 'broken' becuase someone 'outside the class' knows what it needs, but it really isn't the rest of the system that knows that - it's the DI container. Nothing magical happens differently because DI 'knows' one object needs another.

In fact it gets even better - by focusing on Factories and Repositories I don't even have to know DI is involved at all! That to me puts the lid back on encapsulation. Whew!


PS. By providing Dependency Injection you do not necessarily break Encapsulation. Example:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

Client code does not know implementation details still.


Maybe this is a naive way of thinking about it, but what is the difference between a constructor that takes in an integer parameter and a constructor that takes in a service as a parameter? Does this mean that defining an integer outside the new object and feeding it into the object breaks encapsulation? If the service is only used within the new object, I don't see how that would break encapsulation.

Also, by using some sort of autowiring feature (Autofac for C#, for example), it makes the code extremely clean. By building extension methods for the Autofac builder, I was able to cut out a LOT of DI configuration code that I would have had to maintain over time as the list of dependencies grew.


I think it's self evident that at the very least DI significantly weakens encapsulation. In additional to that here are some other downsides of DI to consider.

  1. It makes code harder to reuse. A module which a client can use without having to explicitly provide dependencies to, is obviously easier to use than one where the client has to somehow discover what that component's dependencies are and then somehow make them available. For example a component originally created to be used in an ASP application may expect to have its dependencies provided by a DI container that provides object instances with lifetimes related to client http requests. This may not be simple to reproduce in another client that does not come with the same built in DI container as the original ASP application.

  2. It can make code more fragile. Dependencies provided by interface specification can be implemented in unexpected ways which gives rise to a whole class of runtime bugs that are not possible with a statically resolved concrete dependency.

  3. It can make code less flexible in the sense that you may end up with fewer choices about how you want it to work. Not every class needs to have all its dependencies in existence for the entire lifetime of the owning instance, yet with many DI implementations you have no other option.

With that in mind I think the most important question then becomes, "does a particular dependency need to be externally specified at all?". In practise I have rarely found it necessary to make a dependency externally supplied just to support testing.

Where a dependency genuinely needs to be externally supplied, that normally suggests that the relation between the objects is a collaboration rather than an internal dependency, in which case the appropriate goal is then encapsulation of each class, rather than encapsulation of one class inside the other.

In my experience the main problem regarding the use of DI is that whether you start with an application framework with built in DI, or you add DI support to your codebase, for some reason people assume that since you have DI support that must be the correct way to instantiate everything. They just never even bother to ask the question "does this dependency need to be externally specified?". And worse, they also start trying to force everyone else to use the DI support for everything too.

The result of this is that inexorably your codebase starts to devolve into a state where creating any instance of anything in your codebase requires reams of obtuse DI container configuration, and debugging anything is twice as hard because you have the extra workload of trying to identify how and where anything was instantiated.

So my answer to the question is this. Use DI where you can identify an actual problem that it solves for you, which you can't solve more simply any other way.


DI violates Encapsulation for NON-Shared objects - period. Shared objects have a lifespan outside of the object being created, and thus must be AGGREGATED into the object being created. Objects that are private to the object being created should be COMPOSED into the created object - when the created object is destroyed, it takes the composed object with it. Let's take the human body as an example. What's composed and what's aggregated. If we were to use DI, the human body constructor would have 100's of objects. Many of the organs, for example, are (potentially) replaceable. But, they are still composed into the body. Blood cells are created in the body (and destroyed) everyday, without the need for external influences (other than protein). Thus, blood cells are created internally by the body - new BloodCell().

Advocators of DI argue that an object should NEVER use the new operator. That "purist" approach not only violates encapsulation but also the Liskov Substitution Principle for whoever is creating the object.


I agree that taken to an extreme, DI can violate encapsulation. Usually DI exposes dependencies which were never truly encapsulated. Here's a simplified example borrowed from Miško Hevery's Singletons are Pathological Liars:

You start with a CreditCard test and write a simple unit test.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

Next month you get a bill for $100. Why did you get charged? The unit test affected a production database. Internally, CreditCard calls Database.getInstance(). Refactoring CreditCard so that it takes a DatabaseInterface in its constructor exposes the fact that there's dependency. But I would argue that the dependency was never encapsulated to begin with since the CreditCard class causes externally visible side effects. If you want to test CreditCard without refactoring, you can certainly observe the dependency.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

I don't think it's worth worrying whether exposing the Database as a dependency reduces encapsulation, because it's a good design. Not all DI decisions will be so straight forward. However, none of the other answers show a counter example.


I think it's a matter of scope. When you define encapsulation (not letting know how) you must define what is the encapsuled functionality.

  1. Class as is: what you are encapsulating is the only responsability of the class. What it knows how to do. By example, sorting. If you inject some comparator for ordering, let's say, clients, that's not part of the encapsuled thing: quicksort.

  2. Configured functionality: if you want to provide a ready-to-use functionality then you are not providing QuickSort class, but an instance of QuickSort class configured with a Comparator. In that case the code responsible for creating and configuring that must be hidden from the user code. And that's the encapsulation.

When you are programming classes, it is, implementing single responsibilities into classes, you are using option 1.

When you are programming applications, it is, making something that undertakes some useful concrete work then you are repeteadily using option 2.

This is the implementation of the configured instance:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

This is how some other client code use it:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

It is encapsulated because if you change implementation (you change clientSorter bean definition) it doesn't break client use. Maybe, as you use xml files with all written together you are seeing all the details. But believe me, the client code (ClientService) don't know nothing about its sorter.


It's probably worth mentioning that Encapsulation is somewhat perspective dependent.

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;
    }
}

From the perspective of someone working on the A class, in the second example A knows a lot less about the nature of this.b

Whereas without DI

new A()

vs

new A(new B())

The person looking at this code knows more about the nature of A in the second example.

With DI, at least all that leaked knowledge is in one place.

참고URL : https://stackoverflow.com/questions/1005473/must-dependency-injection-come-at-the-expense-of-encapsulation

반응형