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


128

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

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

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

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


7
이것은 매우 현명한 질문입니다
dfa

5
이 질문에 대답하려면 먼저 캡슐화의 의미 에 대한 논쟁이 필요합니다 . ;)
Jeff Sternal

2
캡슐화는 인터페이스에 의해 유지됩니다. 객체의 필수 특성을 노출하고 종속성과 같은 세부 정보를 숨 깁니다. 이를 통해보다 유연한 구성을 제공 할 수 있도록 클래스를 어느 정도 '개방'할 수 있습니다.
Lawrence Wagerfield

답변:


62

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

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

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

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

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


18
캡슐화는 단순한 용어가 아닙니다. 그것은 실질적인 이점이있는 실제 일이며, 프로그램을 "컴포넌트 지향"또는 "객체 지향"으로 간주하더라도 중요하지 않습니다. 캡슐화는 객체 / 컴포넌트 / 서비스 / 상태가 예상치 못한 방식으로 변경되지 않도록 보호해야하며, IoC는이 보호 기능 중 일부를 제거하므로 분명히 절충점이 있습니다.
Ron Inbar

1
생성자를 통해 제공된 인수는 여전히 객체가 "변경"될 수있는 예상 된 방식 의 영역에 속합니다 . 명시 적으로 노출되고 주위의 불변이 적용됩니다. 정보 숨기기 는 @RonInbar를 언급하는 개인 정보 보호에 대한 더 나은 용어이며 항상 유익하지는 않습니다 (파스타를 풀기 어렵게 만듭니다 ;-)).
Nicholas Blumhardt

2
OOP의 요점은 파스타 얽힘이 별도의 클래스로 분리되어 있으며 수정하려는 특정 클래스의 동작 인 경우에만 엉망이되어야한다는 것입니다 (OOP가 복잡성을 완화시키는 방법입니다). 클래스 (또는 모듈)는 내부 인터페이스를 캡슐화하고 편리한 공용 인터페이스를 노출합니다 (OOP가 재사용을 용이하게하는 방법 임). 인터페이스를 통해 종속성을 노출하는 클래스는 클라이언트에게 복잡성을 유발하고 결과적으로 재사용 성이 떨어집니다. 또한 본질적으로 더 취약합니다.
Neutrino

1
어떤 방식으로 보더라도 DI는 OOP의 가장 귀중한 이점 중 일부를 심각하게 훼손하는 것으로 보이지만 실제로는 실제 문제를 해결하는 방식으로 실제로 사용되는 상황에 직면했습니다. 기존 문제.
Neutrino

1
문제를 해결하는 또 다른 방법은 다음과 같습니다. .NET 어셈블리가 "캡슐화"를 선택하고 의존하는 다른 어셈블리를 선언하지 않았다고 상상해보십시오. 문서를 읽고로드 한 후에 무언가가 작동하기를 바라고 미친 상황이 될 것입니다. 해당 수준에서 종속성을 선언하면 자동화 된 툴링이 앱의 대규모 구성을 처리 할 수 ​​있습니다. 비유를 보려면 곁눈질해야하지만 비슷한 힘이 구성 요소 수준에서 적용됩니다. 트레이드 오프, 그리고 YMMV 항상 :-)로있다
니콜라스 Blumhardt

29

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

일반적 으로이 후자의 경우는 생성자 인수 또는 setter 메소드를 통해 처리됩니다. 그러나 이것이 반드시 사실은 아닙니다. 예를 들어 Java의 최신 버전의 Spring DI 프레임 워크는 개인 필드에 주석을 달도록하고 (예 :로 @Autowired) 종속성을 공개하지 않고도 리플렉션을 통해 종속성을 설정합니다. 공개 메소드 / 생성자 클래스 이것은 당신이 찾고있는 일종의 해결책 일 수 있습니다.

즉, 생성자 주입이 큰 문제라고 생각하지 않습니다. 나는 항상 생성 후에 객체가 완전히 유효해야한다고 느꼈다. 그래서 그들의 역할을 수행하기 위해 필요한 것 (즉, 유효한 상태에 있음)은 어쨌든 생성자를 통해 공급되어야한다. 공동 작업자가 작동 해야하는 객체가있는 경우 생성자 가이 요구 사항을 공개적으로 알리고 클래스의 새 인스턴스를 만들 때이 요구 사항을 충족시키는 것이 좋습니다.

이상적으로 객체를 다룰 때 인터페이스를 통해 객체와 상호 작용하고, 더 많은 작업을 수행할수록 (DI를 통해 종속성이 연결됨) 실제로 생성자를 직접 처리 할 필요가 없습니다. 이상적인 상황에서 코드는 클래스의 구체적인 인스턴스를 처리하지 않으며 심지어는 클래스를 작성하지도 않습니다. 따라서 IFoo생성자 FooImpl가 자신의 작업을 수행해야한다는 것을 걱정 하지 않고 실제로 FooImpl존재 여부를 알지 못하고 DI를 통해 전달 됩니다. 이 관점에서 캡슐화는 완벽합니다.

이것은 물론 의견이지만, 제 생각에는 DI가 반드시 캡슐화를 위반하지는 않으며 실제로 내부에 필요한 모든 지식을 한 곳에 집중시켜 도움을 줄 수 있습니다. 이것은 그 자체로 좋은 일 일뿐 만 아니라 더 좋은 곳은 자신의 코드베이스 외부에 있으므로 작성하는 코드 중 어느 것도 클래스의 종속성에 대해 알 필요가 없습니다.


2
좋은 지적입니다. 개인 필드에서 @Autowired를 사용하지 않는 것이 좋습니다. 클래스를 테스트하기 어렵게 만듭니다. 그런 다음 모의 품이나 스텁을 어떻게 주입합니까?
lumpynose 2016 년

4
동의하지 않습니다. DI는 캡슐화를 위반하므로 피할 수 있습니다. 예를 들어 ServiceLocator를 사용하면 클라이언트 클래스에 대해 전혀 알 필요가 없습니다. Foo 의존성 구현에 대해서만 알아야합니다. 그러나 대부분의 경우 가장 좋은 방법은 단순히 "새"연산자를 사용하는 것입니다.
Rogério

5
@Rogerio-모든 DI 프레임 워크는 설명하는 ServiceLocator와 똑같이 작동합니다. 클라이언트는 Foo 구현에 대한 특정 정보를 모르고 DI 도구는 클라이언트에 대한 특정 정보를 모릅니다. 정확한 구현 클래스뿐만 아니라 필요한 모든 종속성 의 정확한 클래스 인스턴스 도 알아야하므로 "새"를 사용하면 캡슐화를 위반하는 것이 훨씬 더 나쁩니다 .
Andrzej Doyle

4
"new"를 사용하여 공용 클래스가 아닌 도우미 클래스를 인스턴스화하면 캡슐화가 촉진됩니다. DI 대안은 도우미 클래스를 공개하고 클라이언트 클래스에 공개 생성자 또는 세터를 추가하는 것입니다. 두 가지 변경 모두 원래 도우미 클래스에서 제공 한 캡슐화를 손상시킵니다.
Rogério

1
"좋은 질문이다. 그러나 어떤 시점에서, 객체가 의존성을 충족 시키려면 가장 순수한 형태의 캡슐화를 위반해야한다." @ Rogério는 내부적으로 종속성을 새로 작성하고 객체가 내부적으로 자신의 종속성을 사로 잡는 다른 방법은 캡슐화를 위반하지 않습니다.
Neutrino

17

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

솔직히 말하면 모든 것이 캡슐화를 위반합니다. :) 일종의 부드러운 원칙으로 잘 다루어야합니다.

그렇다면 캡슐화를 위반하는 것은 무엇입니까?

상속 은 않습니다 .

상속은 서브 클래스가 부모 구현의 세부 사항에 서브 클래스를 노출시키기 때문에 종종 '상속이 캡슐화를 깨뜨린 다'고 말합니다. (Gang of Four 1995 : 19)

측면 지향 프로그래밍 . 예를 들어 onMethodCall () 콜백을 등록하면 이상한 부작용 등을 추가하여 정상적인 메소드 평가에 코드를 삽입 할 수 있습니다.

C ++에서 친구 선언은 않습니다 .

루비의 클래스 확장은 않습니다 . 문자열 클래스가 완전히 정의 된 후 어딘가에 문자열 메소드를 재정의하십시오.

음, 물건을 많이는 않습니다 .

캡슐화는 좋고 중요한 원칙입니다. 그러나 유일한 것은 아닙니다.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
"저는 저의 원칙이며, 마음에 들지 않으면 ... 다른 것도 있습니다." (Groucho Marx)
Ron Inbar

2
나는 이것이 일종의 요점이라고 생각합니다. 의존적 인젝션 대 캡슐화입니다. 따라서 상당한 혜택을주는 경우에만 의존성 주입을 사용하십시오. 그것은 DI에게 나쁜 이름을 제공 어디서나 DI의
리처드 설렘

이 답변이 무엇을 말하려고하는지 잘 모르겠습니다 ... DI를 수행 할 때 캡슐화를 위반해도 괜찮습니다. 어쨌든 위반 될 수 있기 때문에 "항상 양호" 입니까 , 아니면 단순히 DI 캡슐화를 위반 한 이유 수 있습니까? 다시 말해, 요즘은 의존성을 주입하기 위해 더 이상 퍼블릭 생성자 또는 속성에 의존 할 필요가 없습니다 . 대신 개인 주석이 달린 필드에 삽입 할 수 있습니다 . 이는 더 간단하고 (코드가 적으며) 캡슐화를 유지합니다. 따라서 두 원칙을 동시에 활용할 수 있습니다.
Rogério

상속은 원칙적으로 캡슐화를 위반하지는 않지만 부모 클래스가 잘못 작성된 경우에도 마찬가지입니다. 또 다른 요점은 상당히 복잡한 프로그래밍 패러다임과 아키텍처 또는 디자인과 관련이없는 여러 언어 기능입니다.
Neutrino

13

예, DI는 캡슐화 ( "정보 숨기기"라고도 함)를 위반합니다.

그러나 실제 문제는 개발자가 KISS (짧고 간결하게 유지) 및 YAGNI (You Ai n't Gonna Need It) 원칙을 위반하는 변명으로 사용하는 경우에 발생합니다.

개인적으로 저는 간단하고 효과적인 솔루션을 선호합니다. 나는 주로 "new"연산자를 사용하여 필요할 때마다 언제 어디서나 상태 저장 종속성을 인스턴스화합니다. 간단하고 캡슐화되어 있고 이해하기 쉽고 테스트하기 쉽습니다. 그래서 왜 안돼?


미리 생각하는 것은 끔찍한 일이 아니지만 특히 간단하게 유지하십시오. 개발자가 너무 미래에 대비할만한 무언가를 설계하고 있고, 알려진 / 불확실한 비즈니스 요구 사항조차도 아님에 따라 낭비되는주기를 보았습니다.
Jacob McKay

5

좋은 depenancy injection container / system은 생성자 주입을 허용합니다. 종속 개체는 캡슐화되며 공개적으로 노출 될 필요가 없습니다. 또한 DP 시스템을 사용하면 코드를 작성하는 개체를 포함하여 개체의 구성 방법에 대한 세부 정보를 "알지"않습니다. 거의 모든 코드가 캡슐화 된 객체에 대한 지식으로부터 보호 될뿐만 아니라 객체 구성에도 참여하지 않기 때문에이 경우 더 많은 캡슐화가 있습니다.

이제 생성 된 객체가 생성자에서 캡슐화 된 객체를 만드는 경우와 비교한다고 가정합니다. DP에 대한 나의 이해는 우리가이 책임을 대상으로부터 멀리하고 다른 사람에게주고 싶다는 것입니다. 이를 위해 DP 컨테이너 인 "다른 사람"은 캡슐화를 "위반"하는 친밀한 지식을 가지고 있습니다. 이점은 지식을 대상에서 벗어나는 것입니다. 누군가 가지고 있어야합니다. 나머지 응용 프로그램은 그렇지 않습니다.

의존성 주입 컨테이너 / 시스템은 캡슐화를 위반하지만 코드는 그렇지 않습니다. 사실, 코드는 그 어느 때보 다 "캡슐화"되어 있습니다.


3
클라이언트 객체가 종속성을 직접 인스턴스화 할 수있는 상황이 있다면 왜 그렇게하지 않습니까? 그것은 가장 간단한 일이며 반드시 테스트 가능성을 낮추지는 않습니다. 단순성과 더 나은 캡슐화 외에도 상태 비 저장 싱글 톤 대신 상태 저장 객체를 쉽게 가질 수 있습니다.
Rogério

1
@ Rogério가 말한 것에 더하여 잠재적으로 훨씬 더 효율적입니다. 세계사에서 창조 된 모든 계급이 소유 개체의 수명 전체에 대해 하나의 의존성을 인스턴스화해야 할 필요는 없었습니다. DI를 사용하는 객체는 자신의 의존성, 즉 수명에 대한 가장 기본적인 제어를 잃습니다.
Neutrino

5

Jeff Sternal이 질문에 대한 의견에서 지적했듯이 답변은 캡슐화 를 정의하는 방법에 전적으로 달려 있습니다 .

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

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

이 두 정의는 서로 모순됩니다. File개체 자체가 인쇄 될 수 있으면 프린터 동작에 크게 의존합니다. 다른 한편으로, 인쇄 할 수있는 것 ( 또는 그러한 인터페이스)에 대해서만 알고 있다면 IFilePrinter, File객체는 인쇄에 대해 아무것도 알 필요가 없으므로 작업을하면 객체에 대한 의존성이 줄어 듭니다.

따라서 첫 번째 정의를 사용하면 종속성 주입이 캡슐화를 중단합니다. 그러나 솔직히 나는 첫 번째 정의를 좋아하는지 모르겠습니다. 확실히 확장되지 않습니다 (그렇다면 MS Word는 하나의 큰 클래스 일 것입니다).

반면 에 캡슐화의 두 번째 정의를 사용하는 경우 종속성 주입은 거의 필수 입니다.


나는 첫 번째 정의에 대해 당신에게 분명히 동의합니다. 또한 프로그래밍의 주요 죄 중 하나이며 아마도 확장되지 않는 이유 중 하나 인 SoC를 위반합니다.
Marcus Stade

4

캡슐화를 위반하지 않습니다. 공동 작업자를 제공하고 있지만 수업에서 사용 방법을 결정합니다. 당신이 따르는 한 Tell은 묻지 말고 괜찮습니다. 나는 생성자 주입이 바람직하다고 생각하지만, 세터는 똑똑하고 좋은 한 괜찮을 수 있습니다. 즉, 클래스가 나타내는 불변량을 유지하는 논리가 포함되어 있습니다.


1
왜냐하면 ... 그렇지 않습니까? 로거가 있고 로거가 필요한 클래스가있는 경우 로거를 해당 클래스로 전달해도 캡슐화를 위반하지 않습니다. 그리고 그것은 모든 의존성 주입입니다.
jrockway

3
캡슐화를 오해한다고 생각합니다. 예를 들어 순진한 수업을들 수 있습니다. 내부적으로 일, 월 및 연도 인스턴스 변수가있을 수 있습니다. 이것들이 논리가없는 간단한 세터로 노출되면 캡슐화가 깨질 것입니다. 달을 2로 설정하고 하루를 31로 설정하는 것과 같은 일을 할 수 있기 때문입니다. 반면에, 세터가 똑똑하고 불변성을 확인하면 일이 잘됩니다. . 또한 후자의 버전에서는 스토리지를 1970 년 1 월 1 일 이후의 날짜로 변경할 수 있으며 인터페이스를 사용한 것은 아무것도 인식하지 않아도됩니다.
Jason Watkins

2
DI는 캡슐화 / 정보 숨기기를 위반합니다. 개인 내부 종속성을 클래스의 공개 인터페이스에 노출 된 것으로 바꾸면 정의에 따라 해당 종속성의 캡슐화가 손상됩니다.
Rogério

2
DI에 의해 캡슐화가 손상되었다고 생각하는 구체적인 예가 있습니다. DB에서 "foo data"를 가져 오는 FooProvider와이를 캐시하고 공급자 위에서 물건을 계산하는 FooManager가 있습니다. 코드 소비자가 실수로 FooProvider에 데이터를 보내려고했지만 FooManager 만 인식하도록 캡슐화하고 싶었습니다. 이것은 기본적으로 내 원래 질문의 트리거입니다.
urig

1
@Rogerio : 생성자는 컴포지션 루트에서만 사용 되므로 생성자가 공용 인터페이스의 일부 가 아니라고 주장합니다 . 따라서, 의존성은 컴포지션 루트에 의해서만 "보인다". 컴포지션 루트의 단일 책임은 이러한 종속성을 서로 연결하는 것입니다. 따라서 생성자 주입을 사용해도 캡슐화가 중단되지 않습니다.
Jay Sullivan

4

이것은 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 세계에서 어떻게 초기화를 보장합니까?


업데이트 : 방금 Unity 2.0이 생성자 매개 변수 (예 : 상태 이니셜 라이저)에 대한 값을 제공하면서 resolve () 동안 종속성의 IoC에 대한 일반적인 메커니즘을 사용하는 것을 지원한다는 것을 알았습니다. 아마도 다른 컨테이너도 이것을 지원합니까? 그것은 하나의 생성자에서 state init와 DI를 혼합하는 기술적 인 어려움을 해결하지만 여전히 캡슐화를 위반합니다!
shawnT

내가 들었어 나는이 질문을했다. 왜냐하면 나는 두 가지 좋은 것들 (DI 및 캡슐화)이 다른 하나를 희생하여 하나 올 것이라고 생각하기 때문이다. BTW, 4 가지 방법 중 2 가지만 IWidget이 필요한 경우, 다른 2 가지가 다른 컴포넌트 IMHO에 속한다는 것을 나타냅니다.
urig

3

순수한 캡슐화는 결코 달성 할 수없는 이상입니다. 모든 종속성이 숨겨져 있으면 DI가 전혀 필요하지 않습니다. 예를 들어 자동차 객체의 속도의 정수 값과 같이 객체 내에 내재화 할 수있는 개인 값이 있다면 외부 의존성이 없으며 해당 종속성을 반전하거나 주입 할 필요가 없습니다. 개인 함수에 의해 순수하게 작동하는 이러한 종류의 내부 상태 값은 항상 캡슐화하려는 것입니다.

그러나 특정 종류의 엔진 객체를 원하는 자동차를 제작하는 경우 외부 의존성이 있습니다. 자동차 객체의 생성자 내에서 해당 엔진 (예 : 새 GMOverHeadCamEngine ())을 인스턴스화하여 캡슐화를 유지하면서 콘크리트 클래스 GMOverHeadCamEngine에 훨씬 더 교묘 한 커플 링을 생성하거나 주입하여 Car 객체를 작동시킬 수 있습니다 예를 들어 구체적인 종속성이없는 인터페이스 IEngine과 무관하게 (그리고 훨씬 강력하게). IOC 컨테이너를 사용하든 간단한 DI를 사용하든 이것이 요점이 아닙니다. 요점은 많은 종류의 엔진을 결합하지 않고도 많은 종류의 엔진을 사용할 수있는 자동차를 보유하고 있기 때문에 코드베이스를보다 유연하고 부작용이 적습니다.

DI는 캡슐화 위반이 아니며, 사실상 모든 OOP 프로젝트 내에서 캡슐화가 반드시 깨질 때 커플 링을 최소화하는 방법입니다. 인터페이스에 외부로 의존성을 주입하면 커플 링 부작용을 최소화하고 클래스가 구현에 대해 무시할 수 있습니다.


3

의존성이 실제로 구현 세부 사항인지 또는 클라이언트가 어떤 식 으로든 알고 싶어하거나 필요로하는 것인지에 달려 있습니다. 관련된 한 가지는 클래스가 어떤 추상화 수준을 목표로 삼고 있는지입니다. 여기 몇 가지 예가 있어요.

후드 아래에서 캐싱을 사용하여 호출 속도를 높이는 메소드가있는 경우 캐시 오브젝트는 싱글 톤 또는 그 외의 것이어야 하며 주입 되어서는 안됩니다 . 캐시가 전혀 사용되지 않는다는 사실은 클래스의 클라이언트가 신경 쓸 필요가없는 구현 세부 사항입니다.

클래스가 데이터 스트림을 출력해야하는 경우 클래스가 결과를 배열, 파일 또는 다른 사람이 데이터를 보내려는 다른 위치로 쉽게 출력 할 수 있도록 출력 스트림을 삽입하는 것이 좋습니다.

회색 영역의 경우 몬테 카를로 시뮬레이션을 수행하는 클래스가 있다고 가정 해 봅시다. 임의의 근원이 필요합니다. 한편으로, 이것이 필요하다는 사실은 클라이언트가 무작위의 출처를 정확히 신경 쓰지 않는다는 점에서 구현 세부 사항입니다. 반면에, 실제 난수 생성기는 클라이언트가 제어하고 싶을 수있는 임의의 정도, 속도 등을 절충하고 클라이언트는 반복 가능한 동작을 얻기 위해 시드를 제어하기를 원할 수 있으므로 주입이 의미가있을 수 있습니다. 이 경우 난수 생성기를 지정하지 않고 클래스를 만드는 방법을 제안하고 스레드 로컬 Singleton을 기본값으로 사용하는 것이 좋습니다. 더 세밀한 제어가 필요한 경우, 무작위 소스를 주입 할 수있는 다른 생성자를 제공하십시오.


2

나는 단순함을 믿습니다. Domain 클래스에 IOC / Dependecy Injection을 적용해도 관계를 설명하는 외부 XML 파일을 가져 와서 코드를 훨씬 더 어렵게 만드는 것 외에는 개선되지 않습니다. EJB 1.0 / 2.0 및 struts 1.1과 같은 많은 기술은 XML에 넣은 내용을 줄이고 주석으로 코드에 넣어보십시오. 따라서 개발하는 모든 클래스에 IOC를 적용하면 코드가 의미가 없습니다.

IOC는 컴파일 타임에 종속 개체를 만들 준비가되지 않은 경우 이점이 있습니다. 이는 대부분의 인프라 추상 수준 아키텍처 구성 요소에서 발생할 수 있으며, 다른 시나리오에서 작동해야하는 공통 기본 프레임 워크를 설정하려고합니다. 그러한 장소에서는 IOC가 더 의미가 있습니다. 여전히 이것은 코드를보다 간단하고 유지 보수 할 수있게하지 않습니다.

다른 모든 기술과 마찬가지로이 기술에도 장점과 단점이 있습니다. 내 걱정은 최고의 컨텍스트 사용에 관계없이 모든 곳에서 최신 기술을 구현한다는 것입니다.


2

캡슐화는 클래스가 객체를 생성 할 책임이 있고 (구현 세부 사항에 대한 지식이 필요함) 클래스를 사용하는 경우에만 해당됩니다 (이 세부 사항에 대한 지식이 필요하지 않음). 이유를 설명하지만 먼저 빠른 자동차 해부학을 설명하겠습니다.

내가 1971 년 콤비를 운전할 때 가속기를 누를 수 있었고 (약간) 더 빨랐습니다. 이유를 알 필요는 없었지만 공장에서 Kombi를 만든 사람들은 그 이유를 정확히 알고있었습니다.

그러나 코딩으로 돌아갑니다. 캡슐화 는 "해당 구현을 사용하여 구현 세부 정보를 숨 깁니다". 클래스의 사용자가 모르게 구현 세부 사항을 변경할 수 있으므로 캡슐화가 좋습니다.

의존성 주입을 사용할 때, 생성자 주입은 서비스 유형 오브젝트 를 구성하는 데 사용됩니다 (상태를 모델링하는 엔티티 / 값 오브젝트와 반대). 서비스 유형 오브젝트의 모든 멤버 변수는 유출되지 않아야하는 구현 세부 사항을 나타냅니다. 소켓 포트 번호, 데이터베이스 자격 증명, 암호화를 수행하기 위해 호출 할 다른 클래스, 캐시 등

생성자는 클래스가 처음 생성 될 때 관련이 있습니다. DI 컨테이너 (또는 공장)가 모든 서비스 개체를 연결하는 동안 구성 단계에서 발생합니다. DI 컨테이너는 구현 세부 정보 만 알고 있습니다. Kombi 공장의 사람들이 점화 플러그에 대해 알고있는 것처럼 구현 세부 정보에 대해 모두 알고 있습니다.

런타임시 만들어진 서비스 개체를 실제 작업을 수행하기 위해 apon이라고합니다. 현재, 객체의 호출자는 구현 세부 사항을 전혀 모른다.

그게 내 콤비를 해변으로 몰고가는 중이 야

이제 캡슐화로 돌아갑니다. 구현 세부 사항이 변경되면 런타임에 해당 구현을 사용하는 클래스를 변경할 필요가 없습니다. 캡슐화가 깨지지 않았습니다.

새 차를 해변까지 운전할 수 있습니다. 캡슐화가 깨지지 않았습니다.

구현 세부 사항이 변경되면 DI 컨테이너 (또는 팩토리)를 변경해야합니다. 처음에는 공장에서 구현 세부 사항을 숨기려고 시도하지 않았습니다.


공장을 어떻게 단위로 테스트하겠습니까? 즉, 클라이언트가 작동하는 자동차를 얻기 위해서는 공장에 대해 알아야합니다. 즉, 시스템의 서로 다른 객체에 대한 공장이 필요합니다.
Rodrigo Ruiz

2

이 문제로 조금 더 어려움을 겪은 지금 나는 의존성 주입이 (현재) 캡슐화를 어느 정도 위반한다고 생각합니다. 그래도 오해하지 마십시오-의존성 주입을 사용하는 것이 대부분의 경우 트레이드 오프 가치가 있다고 생각합니다.

작업중인 구성 요소를 "외부"당사자에게 전달해야하는 경우 DI가 캡슐화를 위반하는 이유가 명확 해집니다 (고객을위한 라이브러리 작성 생각).

내 구성 요소가 생성자 (또는 공용 속성)를 통해 하위 구성 요소를 주입 해야하는 경우 보장 할 수 없습니다

"사용자가 구성 요소의 내부 데이터를 유효하지 않거나 일관성이없는 상태로 설정하지 못하게합니다."

동시에 그것은 말할 수 없다

"구성 요소 사용자 (다른 소프트웨어 조각)는 구성 요소의 기능 만 알면되고 구성 요소의 세부 사항에 의존 할 수 없습니다 . "

두 인용문은 모두 wikipedia 에서 온 것 입니다.

구체적인 예를 들면 다음과 같습니다. WCF 서비스 (기본적으로 원격 파사드)에 대한 통신을 단순화하고 숨기는 클라이언트 쪽 DLL을 제공해야합니다. 3 가지 WCF 프록시 클래스에 의존하기 때문에 DI 접근 방식을 사용하면 생성자를 통해 노출해야합니다. 그것으로 나는 숨기려고하는 통신 계층의 내부를 노출시킵니다.

일반적으로 나는 DI를 위해 모두입니다. 이 특별한 (극단적 인) 예에서, 그것은 나를 위험하게 만듭니다.


2

DI는 비공유 객체에 대한 캡슐화를 위반합니다. 공유 객체는 생성중인 객체 외부의 수명을 가지므로 생성중인 객체로 집계되어야합니다. 작성중인 오브젝트의 개인 오브젝트는 작성된 오브젝트로 작성되어야합니다. 작성된 오브젝트가 소멸되면 작성된 오브젝트를 가져옵니다. 인체를 예로 들어 봅시다. 구성 및 집계 대상 DI를 사용한다면 인체 생성자는 100 개의 객체를 가질 것입니다. 예를 들어, 많은 기관들이 (잠재적으로) 교체 가능합니다. 그러나 그들은 여전히 ​​몸으로 구성되어 있습니다. 혈액 세포는 단백질 이외의 외부 영향없이 매일 신체에서 생성 (파괴)됩니다. 따라서 혈액 세포는 신체에 의해 내부적으로 생성됩니다-새로운 BloodCell ().

DI의 옹호자들은 개체가 절대 새로운 연산자를 사용해서는 안된다고 주장합니다. "순수한"접근 방식은 캡슐화를 위반할뿐만 아니라 개체를 생성하는 사람을위한 Liskov 대체 원칙도 위반합니다.


1

나는이 개념으로도 어려움을 겪었다. 처음에는 스프링과 같은 DI 컨테이너를 사용하여 후프를 통해 점프하는 것처럼 느껴지는 객체를 인스턴스화하는 '요구 사항'. 그러나 실제로는 실제로 후프가 아닙니다. 필요한 객체를 만드는 또 다른 '게시 된'방법 일뿐입니다. 물론, 캡슐화는 '파손'되어 누군가 '클래스 외부'가 필요한 것을 알고 있기 때문에 실제로는 시스템의 나머지 부분이 아니라 DI 컨테이너입니다. DI가 한 개체가 다른 개체를 필요로 '알고'있기 때문에 마술처럼 다르게 일어나는 일은 없습니다.

사실 그것은 공장과 리포지토리에 집중함으로써 더 나아질 것입니다. DI조차 전혀 알지 않아도됩니다! 그것은 뚜껑을 다시 캡슐에 넣습니다. 아휴!


1
DI가 전체 인스턴스화 체인을 담당하는 한 캡슐화가 발생한다는 데 동의합니다. 의존성은 여전히 ​​공개적이고 남용 될 수 있기 때문입니다. 그러나 체인의 "위"에 누군가가 DI를 사용하지 않고 객체를 인스턴스화해야 할 때 (아마도 "타사"일 수 있음) 어지럽습니다. 그들은 당신의 의존성에 노출되어 그들을 학대하려는 유혹을받을 수 있습니다. 또는 그들에 대해 전혀 알고 싶지 않을 수도 있습니다.
urig 2016 년

1

추신. 의존성 주입 을 제공 한다고 해서 반드시 캡슐화를 중단 할 필요는 없습니다 . 예:

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

클라이언트 코드는 여전히 구현 세부 사항을 모릅니다.


귀하의 예에서 어떤 것이 주입됩니까? (세터 함수의 이름 지정을 제외하고)
foo

1

어쩌면 이것은 순진한 생각일지도 모르지만 정수 매개 변수를 사용하는 생성자와 매개 변수로 서비스를 생성하는 생성자의 차이점은 무엇입니까? 이것은 새 객체 외부에서 정수를 정의하고 객체로 공급하면 캡슐화가 중단된다는 것을 의미합니까? 서비스가 새 객체 내에서만 사용되는 경우 캡슐화가 어떻게 중단되는지 알 수 없습니다.

또한 일종의 자동 배선 기능 (예 : C #의 경우 Autofac)을 사용하여 코드를 매우 깨끗하게 만듭니다. Autofac 빌더를위한 확장 메소드를 구축함으로써 종속성 목록이 커짐에 따라 시간이 지남에 따라 유지해야 할 많은 DI 구성 코드를 잘라낼 수있었습니다.


가치 대 서비스에 관한 것이 아닙니다. 클래스의 생성자가 클래스를 설정하는지 또는 해당 서비스가 클래스 설정을 인계하는지 여부는 제어의 반전에 관한 것입니다. 이를 위해 해당 클래스의 구현 세부 사항을 알아야하므로 유지 관리하고 동기화 할 다른 장소가 있습니다.
foo

1

나는 최소한 DI가 캡슐화를 크게 약화시키는 것이 자명하다고 생각합니다. 여기에 추가로 고려해야 할 DI의 다른 단점이 있습니다.

  1. 코드를 재사용하기 어렵게 만듭니다. 클라이언트가 명시 적으로 종속성을 제공하지 않고 사용할 수있는 모듈은 클라이언트가 해당 구성 요소의 종속성이 무엇인지 발견 한 다음 사용 가능하게 만드는 모듈보다 사용하기 쉽습니다. 예를 들어 원래 ASP 응용 프로그램에서 사용하도록 만들어진 구성 요소는 클라이언트 http 요청과 관련된 수명을 개체 인스턴스에 제공하는 DI 컨테이너가 제공하는 종속성을 가질 수 있습니다. 원래 ASP 응용 프로그램과 동일한 기본 제공 DI 컨테이너와 함께 제공되지 않는 다른 클라이언트에서는 재현하기가 쉽지 않을 수 있습니다.

  2. 코드를 더 취약하게 만들 수 있습니다. 인터페이스 사양에서 제공하는 종속성은 예상치 못한 방식으로 구현 될 수있어 정적으로 해결 된 구체적인 종속성으로는 불가능한 전체 런타임 버그 클래스를 발생시킵니다.

  3. 코드의 작동 방식에 대한 선택의 폭이 줄어든다는 점에서 코드의 유연성이 떨어질 수 있습니다. 모든 클래스가 소유 인스턴스의 전체 수명 동안 존재하는 모든 종속성을 가질 필요는 없지만 많은 DI 구현에서는 다른 옵션이 없습니다.

그런 점을 염두에두고 가장 중요한 질문은 " 특정 의존성을 전혀 외부 적으로 지정해야합니까? . "라고 생각합니다. 실제로 테스트를 지원하기 위해 외부에서 종속성을 제공해야 할 필요는 거의 없었습니다.

의존성이 진정으로 외부에 제공되어야하는 경우, 이는 일반적으로 객체 간의 관계가 내부 의존성이 아니라 협업이라는 것을 암시합니다.이 경우 적절한 목표는 다음과 같이 캡슐화됩니다. 한 클래스를 다른 클래스 내부에 캡슐화하는 것이 아니라 클래스의 캡슐화입니다 .

내 경험상 DI 사용에 관한 주요 문제는 DI가 내장 된 응용 프로그램 프레임 워크로 시작하거나 코드베이스에 DI 지원을 추가하는지 여부입니다. 어떤 이유로 사람들은 DI 지원이 있기 때문에 정확해야한다고 가정합니다 모든 것을 인스턴스화 하는 방법 . 그들은 "이 의존성을 외부 적으로 명시 할 필요가 있는가?"라는 질문을 결코 귀찮게하지 않습니다. 그리고 더 나빠, 그들은 또한 다른 사람들이 DI 지원을 사용하도록 강요하기 시작합니다.모든 것을 입니다.

결과적으로 코드베이스에서 인스턴스를 생성하는 데 어려움이있는 DI 컨테이너 구성이 필요하고 상태를 식별하기위한 추가 워크로드가 있기 때문에 디버깅이 두 배 더 어려운 상태로 코드베이스가 전개되기 시작합니다. 무엇을 인스턴스화했는지

그래서 질문에 대한 나의 대답은 이것입니다. DI가 자신을 위해 해결하는 실제 문제를 식별 할 수있는 곳에서는 다른 방법으로는 더 이상 해결할 수 없습니다.


0

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 결정이 그렇게 쉬운 것은 아닙니다. 그러나 다른 답변은 반대의 예를 보여주지 않습니다.


1
단위 테스트는 대개 클래스 작성자가 작성하므로 기술적 인 관점에서 테스트 케이스의 종속성을 철자해도됩니다. 나중에 신용 카드 클래스가 PayPal과 같은 웹 API를 사용하도록 변경되면, 사용자는 DI 된 경우 모든 것을 변경해야합니다. 단위 테스트는 일반적으로 테스트 대상에 대한 친밀한 지식으로 수행되므로 (전체 요점이 아닙니까?) 테스트는 일반적인 예보다 예외적 인 것 같습니다.
kizzx2

1
DI의 요점은 설명 된 변경을 피하는 것입니다. 웹 API에서 PayPal로 전환 한 경우 MockPaymentService를 사용하고 CreditCard가 PaymentService로 구성되므로 대부분의 테스트는 변경하지 않습니다. CreditCard와 실제 PaymentService 사이의 실제 상호 작용을 살펴 보는 소수의 테스트 만 있으므로 향후 변경이 매우 분리됩니다. 더 깊은 의존성 그래프 (예 : CreditCard에 의존하는 클래스)의 이점은 훨씬 더 큽니다.
Craig P. Motlin

@ 크레이그 p. Motlin CreditCard클래스 외부에서 변경하지 않고도 어떻게 WebAPI에서 PayPal로 객체를 변경할 수 있습니까?
Ian Boyd

@Ian 나는 CreditCard가 DatabaseInterfaces의 구현에서 변경되는 것을 막기 위해 생성자에서 DatabaseInterface를 가져 오기 위해 리팩토링되어야한다고 언급했다. 어쩌면 그것은 더 일반적이어야하고 WebAPI가 다른 구현이 될 수있는 StorageInterface를 가져 가야합니다. PayPal은 신용 카드의 대안이기 때문에 잘못된 수준에 있습니다. PayPal과 CreditCard는이 예제 외부에서 애플리케이션의 다른 부분을 보호하기 위해 PaymentInterface를 구현할 수 있습니다.
Craig P. Motlin

@ kizzx2 즉, 신용 카드가 PayPal을 사용해야한다고 말이되지 않습니다. 그들은 실제 생활의 대안입니다.
Craig P. Motlin

0

나는 그것이 범위의 문제라고 생각합니다. 캡슐화를 정의 할 때 (방법을 알려주지 않음) 캡슐화 된 기능을 정의해야합니다.

  1. 그대로 클래스 : 캡슐화하는 것은 클래스의 유일한 책임입니다. 수행 방법을 알고있는 것. 예를 들어 정렬. 주문을 위해 비교기를 주입하면 클라이언트가 캡슐화 된 것의 일부가 아닌 빠른 정렬이라고 가정 해 봅시다.

  2. 구성된 기능 : 즉시 사용 가능한 기능을 제공하려면 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) 분류기에 대해 아무것도 모릅니다 .


0

아마 언급 할만큼 가치의 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를 사용하면 유출 된 모든 지식이 최소한 한 곳에 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.