C ++ : 클래스가 종속성을 소유하거나 관찰해야합니까?


17

class Foobar사용 하는 클래스가 있다고 가정 해보십시오 Widget. 좋은 시절에는 Widgetwolud를의 필드로 선언 Foobar하거나 다형성 동작이 필요한 경우 스마트 포인터로 선언 하면 생성자에서 초기화됩니다.

class Foobar {
    Widget widget;
    public:
    Foobar() : widget(blah blah blah) {}
    // or
    std::unique_ptr<Widget> widget;
    public:
    Foobar() : widget(std::make_unique<Widget>(blah blah blah)) {}
    (…)
};

그리고 우리는 모두 준비되고 끝났습니다. 불행하게도, 오늘날, 자바 어린이들은 우리가 그것을 본 후에, 그리고 합당하게 결합 Foobar하여 Widget함께 웃을 것입니다. 해결책은 간단 해 보입니다. 종속성 주입을 적용하여 종속성을 Foobar클래스 외부로 가져옵니다 . 그러나 C ++은 의존성의 소유권에 대해 생각하도록합니다. 세 가지 솔루션이 떠 오릅니다.

고유 포인터

class Foobar {
    std::unique_ptr<Widget> widget;
    public:
    Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
    (…)
}

FoobarWidget그 소유권 만 주장 합니다. 다음과 같은 장점이 있습니다.

  1. 성능에 미치는 영향은 미미합니다.
  2. Foobar수명이 길어질수록 안전 Widget하므로 Widget갑자기 사라지지 않습니다.
  3. Widget누출되지 않으며 더 이상 필요하지 않을 때 올바르게 파괴되는 것이 안전 합니다.

그러나 이것은 비용이 듭니다 :

  1. Widget예를 들어 스택 할당을 Widgets사용할 수없고, Widget공유 할 수 없는 것과 같이 인스턴스 사용 방법에 제한 이 있습니다.

공유 포인터

class Foobar {
    std::shared_ptr<Widget> widget;
    public:
    Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
    (…)
}

이것은 아마도 Java 및 기타 가비지 수집 언어와 가장 비슷합니다. 장점 :

  1. 종속성을 공유 할 수 있으므로보다 보편적입니다.
  2. unique_ptr솔루션의 안전 (2 및 3 지점)을 유지 합니다.

단점 :

  1. 공유가 포함되지 않으면 리소스를 낭비합니다.
  2. 여전히 힙 할당이 필요하며 스택 할당 객체를 허용하지 않습니다.

평범한 관찰 포인터

class Foobar {
    Widget *widget;
    public:
    Foobar(Widget *w) : widget(w) {}
    (…)
}

수업 중에 생생한 포인터를 놓고 소유의 부담을 다른 사람에게 이전하십시오. 장점 :

  1. 최대한 간단합니다.
  2. 유니버설, 그냥 받아 들인다 Widget.

단점 :

  1. 더 이상 안전하지 않습니다.
  2. Foobar및의 소유권을 담당하는 다른 엔터티를 소개 Widget합니다.

미친 템플릿 메타 프로그래밍

내가 생각할 수있는 유일한 장점은 소프트웨어가 구축되는 동안 내가 찾지 못한 모든 책을 읽을 수 있다는 것입니다.)

가장 보편적 Foobars이므로 어쨌든 무언가를 관리해야 하므로 관리 Widgets는 간단한 변화입니다. 그러나 원시 포인터를 사용하면 나를 귀찮게 할 수 있습니다. 반면에 스마트 포인터 솔루션은 종속성 소비자가 종속성 생성 방법을 제한하므로 나에게 잘못된 느낌을줍니다.

뭔가 빠졌습니까? 아니면 C ++의 의존성 주입이 사소하지 않습니까? 클래스가 의존성을 소유해야합니까 아니면 그냥 관찰해야합니까?


1
처음부터 C ++ 11에서 컴파일 할 수 있고 클래스에서 리소스를 처리하는 방법으로 일반 포인터를 사용하는 것이 더 이상 권장되지 않습니다. Foobar 클래스가 리소스의 유일한 소유자이고 리소스가 해제되어야하는 경우 Foobar가 범위를 벗어날 때 std::unique_ptr갈 수있는 방법입니다. std::move()자원 소유권을 상위 범위에서 클래스로 이전하는 데 사용할 수 있습니다 .
Andy

1
Foobar이것이 유일한 소유자 인지 어떻게 알 수 있습니까? 오래된 경우에는 간단합니다. 그러나 DI의 문제점은 클래스가 종속성 구성에서 분리 될 때 클래스가 종속성의 소유권에서 분리되는 것입니다 (소유가 건설과 관련되어 있기 때문에). Java와 같은 가비지 수집 환경에서는 문제가되지 않습니다. C ++에서 이것은입니다.
el.pescado

1
@ el.pescado Java에도 동일한 문제가 있으며 메모리 만이 유일한 리소스는 아닙니다. 예를 들어 파일과 연결도 있습니다. Java 에서이 문제를 해결하는 일반적인 방법은 포함 된 모든 객체의 수명주기를 관리하는 컨테이너를 갖는 것입니다. 따라서 DI 컨테이너에 포함 된 개체에서 걱정할 필요가 없습니다. DI 컨테이너가 어떤 수명주기 단계로 전환 할시기를 알 수있는 방법이 있다면 c ++ 애플리케이션에서도 작동 할 수 있습니다.
SpaceTrucker

3
물론 복잡성없이 구식으로 작업 할 수 있고 작동하고 유지 관리하기 쉬운 코드를 가질 수 있습니다. (자바 소년들은 웃을 지 모르지만 항상 리팩토링에 대해 진행 중이므로 디커플링으로 많은 도움이되지는 않는다)
gbjbaanb

3
또한 다른 사람들이 당신을 비웃는 것은 그들과 같은 이유가 아닙니다. 그들의 주장에 귀를 기울이고 ( "우리에게 들었 기 때문에"이외) DI가 프로젝트에 실질적인 이익을 가져다 준다면 DI를 구현하십시오. 이 경우 패턴을 프로젝트에 고유 한 방식으로 적용해야하는 매우 일반적인 규칙으로 생각하십시오. 세 가지 접근 방식 (및 아직 생각하지 않은 다른 모든 잠재적 접근 방식)은 유효합니다. 그것들은 전반적인 이익을 가져옵니다 (즉, 단점보다 더 많은 단점이 있습니다).

답변:


2

나는 이것을 주석으로 쓰려고했지만 너무 길었습니다.

Foobar이것이 유일한 소유자 인지 어떻게 알 수 있습니까? 오래된 경우에는 간단합니다. 그러나 DI의 문제점은 클래스가 종속성 구성에서 분리 될 때 클래스가 종속성의 소유권에서 분리되는 것입니다 (소유가 건설과 관련되어 있기 때문에). Java와 같은 가비지 수집 환경에서는 문제가되지 않습니다. C ++에서 이것은입니다.

사용할지 여부 std::unique_ptr<Widget>또는 std::shared_ptr<Widget>, 그 결정하는 당신에게 달려 및 기능에서 비롯됩니다.

Utilities::Factory와 같은 블록 생성을 담당하는가 있다고 가정 해 봅시다 Foobar. 디 원리에 따라, 당신은해야합니다 Widget그것을 사용하여 주입, 인스턴스를 Foobar'중 하나 내부 의미의 생성자를 Utilities::Factory예를 들어,의 방법' createWidget(const std::vector<std::string>& params), 당신은에 위젯을 만들고 주입 Foobar객체입니다.

이제 객체 Utilities::Factory를 생성 한 Widget메소드가 있습니다. 그 방법은 삭제에 대한 책임이 있습니까? 당연히 아니지. 단지 당신을 인스턴스로 만드는 것입니다.


여러 개의 창이있는 응용 프로그램을 개발한다고 가정 해 봅시다. 각 창은 Foobar클래스를 사용하여 표시 되므로 실제로 Foobar컨트롤러처럼 작동합니다.

컨트롤러는 아마도 당신 Widget의 일부를 사용 하고 당신은 스스로에게 물어야합니다 :

내 응용 프로그램 에서이 특정 창으로 이동하면이 위젯이 필요합니다. 이 위젯은 다른 애플리케이션 창과 공유됩니까? 그렇다면 공유 된 상태이기 때문에 항상 동일하게 보이기 때문에 다시 작성해서는 안됩니다.

std::shared_ptr<Widget> 갈 길입니다.

또한 Widget이 하나의 창에만 특별히 연결된 응용 프로그램 창이 있습니다 . 즉, 다른 곳에서는 표시되지 않습니다. 따라서 창을 닫으면 Widget더 이상 응용 프로그램 의 아무 곳이나 인스턴스 가 필요하지 않습니다 .

그것이 std::unique_ptr<Widget>왕좌를 주장하는 곳 입니다.


최신 정보:

평생 문제에 대해 @DominicMcDonnell에 동의하지 않습니다 . 호출 std::movestd::unique_ptr완전히가 소유권을 전송 당신은을 만들 수 있도록 경우에도 object A방법과 다른에 전달 object B종속성대로는 object B지금 자원에 대한 책임을 질 것입니다 object A때 제대로 삭제합니다 object B범위를 벗어나.


소유권과 스마트 포인터에 대한 일반적인 아이디어는 분명합니다. DI에 대한 아이디어를 가지고 놀아주는 것만은 아닙니다. 의존성 주입은 클래스를 종속성에서 분리하는 것에 관한 것이지만이 경우 클래스는 여전히 종속성이 생성되는 방법에 영향을 미칩니다.
el.pescado

예를 들어, 내가 가지고있는 문제 unique_ptr는 단위 테스트 (DI는 테스트 친화적으로 광고됩니다) : 테스트하고 싶습니다 Foobar. 그래서 생성 Widget하고 전달하고 Foobar운동 Foobar한 다음 검사 Widget하고 싶지만 Foobar어떻게 든 노출 되지 않으면 그것이 주장한 이래로 Foobar.
el.pescado

@ el.pescado 두 가지를 함께 섞고 있습니다. 접근성과 소유권을 혼합하고 있습니다. 그냥 때문에 Foobar의미하지, 아무도 그것을 사용하지합니다 않습니다 자원을 보유하고있다. Foobar와 같은 메소드를 구현하면 Widget* getWidget() const { return this->_widget.get(); }작업 할 수있는 원시 포인터를 반환합니다. 그런 다음 Widget클래스 를 테스트하려는 경우이 메소드를 단위 테스트의 입력으로 사용할 수 있습니다 .
Andy

좋은 지적입니다.
el.pescado

1
shared_ptr을 unique_ptr로 변환 한 경우 unique_ness를 어떻게 보장 하시겠습니까 ???
Aconcagua

2

참조 형식으로 관찰 포인터를 사용합니다. 그것은 당신이 그것을 사용할 때 훨씬 더 좋은 구문을 제공하며 소유권을 의미하지 않는 의미 론적 이점을 가지고 있습니다.

이 방법의 가장 큰 문제는 수명입니다. 종속성이 종속 클래스 이전에 구성되고 종속 클래스 이후에 파괴되는지 확인해야합니다. 간단한 문제는 아닙니다. 공유 포인터 (종속성 저장소 및 그것에 의존하는 모든 클래스에서 위의 옵션 2)를 사용하면이 문제를 제거 할 수 있지만 사소하지 않은 순환 종속성의 문제가 발생합니다. 문제가 발생하기 전에 감지하십시오. 그렇기 때문에 수명과 시공 순서를 자동 및 수동으로 관리하지 않는 것이 좋습니다. 또한 간단한 템플릿 접근 방식을 사용하여 객체 목록을 생성하고 반대 순서로 파괴하는 시스템을 보았습니다.

최신 정보

데이비드 패커의 대답은 그 질문에 대해 조금 더 생각하게 만들었습니다. 내 대답의 공유 종속성에 대한 원래의 대답은 사실입니다. 이는 의존성 주입의 장점 중 하나이며 종속성의 한 인스턴스를 사용하여 여러 인스턴스를 가질 수 있습니다. 그러나 클래스에 특정 종속성의 자체 인스턴스 std::unique_ptr가 있어야하는 경우 정답입니다.


1

우선-이것은 Java가 아닌 C ++ 이며 여기에는 많은 것이 다릅니다. Java 사용자는 자동 가비지 콜렉션이 있기 때문에 소유권에 대한 문제가 없습니다.

둘째 :이 질문에 대한 일반적인 대답은 없습니다-요구 사항에 달려 있습니다!

FooBar와 위젯을 연결하는 데 어떤 문제가 있습니까? FooBar는 위젯을 사용하기를 원하며 모든 FooBar 인스턴스가 항상 고유하고 동일한 위젯을 갖는 경우 결합 된 상태로 두십시오 ...

C ++에서는, 단순히 variadic 템플릿 생성자를 갖는 것과 같이 Java에는 존재하지 않는 "이상한"작업을 수행 할 수도 있습니다 (자바에는 생성자에도 사용할 수있는 ... 실제 배열 변수와는 전혀 관련이없는 객체 배열을 숨기려면 구문 설탕 만!)-카테고리 '일부 미친 템플릿 메타 프로그래밍':

namespace WidgetFactory
{
    Widget* create(int, int)
    {
        return 0;
    }
    Widget* create(int, bool, long)
    {
        return 0;
    }
}
class FooBar
{
public:
    template < typename ...Arguments >
    FooBar(Arguments... arguments)
        : mWidget(WidgetFactory::create(arguments...))
    {
    }
    ~FooBar()
    {
        delete mWidget;
    }
private:
    Widget* mWidget;
};

FooBar foobar1(10, 12);
FooBar foobar2(51, true, 54L);

좋아?

물론, 거기에 있습니다 예를 들어, 당신이 어떤 FOOBAR 인스턴스가 원하는 경우, 존재 또는 위젯, ..., 또는 단순히를 재사용 할 필요가 훨씬 전에 한 번에 위젯을 만들어야하는 경우 - 당신이 원하는 이유 또는 두 클래스를 분리 할 필요가 현재 문제 때문에 더 적절하기 때문입니다 (예를 들어 위젯이 GUI 요소이고 FooBar가 아닌 경우).

그런 다음 두 번째 요점으로 돌아갑니다. 일반적인 대답은 없습니다. 실제 문제 의 경우 어떤 것이 더 적합한 솔루션 인지 결정해야합니다 . 나는 DominicMcDonnell의 참조 접근법을 좋아하지만 FooBar가 소유권을 얻지 않으면 적용 할 수 있습니다 (실제로는 할 수는 있지만 매우 더러운 코드를 의미합니다 ...). 그 외에도 David Packer의 답변 (댓글로 쓰여졌지만 좋은 답변)에 합류했습니다.


1
C ++에서는 class예제와 같이 공장 인 경우에도 정적 메소드 이외의 es를 사용하지 마십시오 . 이는 OO 원칙을 위반합니다. 네임 스페이스를 대신 사용하고 네임 스페이스를 사용하여 함수를 그룹화하십시오.
Andy

@DavidPacker 흠, 네 말이 맞아-나는 교실에 개인 내부가 있다고 가정했지만 다른 곳에서는 볼 수 없거나 언급되지 않았다 ...
Aconcagua

1

C ++에서 사용할 수있는 옵션이 두 개 이상 누락되었습니다.

하나는 의존성이 템플릿 매개 변수 인 '정적'의존성 주입을 사용하는 것입니다. 이것은 컴파일 타임 의존성 주입을 계속 허용하면서 값으로 의존성을 유지하는 옵션을 제공합니다. STL 컨테이너는 예를 들어 할당 자 및 비교 및 ​​해시 함수에이 방법을 사용합니다.

또 다른 방법은 깊은 복사를 사용하여 값으로 다형성 객체를 가져 오는 것입니다. 이를 수행하는 전통적인 방법은 가상 클론 방법을 사용하는 것입니다. 또 다른 인기있는 옵션은 유형 삭제를 사용하여 다형성으로 작동하는 값 유형을 만드는 것입니다.

가장 적합한 옵션은 실제로 사용 사례에 따라 다르며 일반적인 대답을하기가 어렵습니다. 정적 다형성이 필요한 경우 템플릿이 가장 C ++ 방법이라고 말할 수 있습니다.


0

게시 한 첫 번째 코드 (값별로 멤버를 저장하는 "좋은 날짜")와 종속성 삽입을 결합하는 네 번째 가능한 답변을 무시했습니다.

class Foobar {
    Widget widget;
public:
    Foobar(Widget w) // pass w by value
     : widget{ std::move(w) } {}
};

클라이언트 코드는 다음과 같이 쓸 수 있습니다 :

Widget w;
Foobar f1{ w }; // default: copy w into f1
Foobar f2{ std::move(w) }; // move w into f2

나열한 기준에 따라 (순전히) 객체 간 연결을 수행해서는 안됩니다 (즉, "안전한 수명 관리에 더 적합한"순전히 기반이 아님).

개념적인 기준을 사용할 수도 있습니다 ( "자동차에는 4 개의 바퀴가 있습니다"와 "자동차는 4 개의 바퀴가 있습니다").

다른 API에 의해 부과 된 기준을 가질 수 있습니다 (API에서 얻는 것이 사용자 정의 랩퍼 또는 std :: unique_ptr 인 경우 클라이언트 코드의 옵션도 제한됩니다).


1
이것은 부가 가치를 심각하게 제한하는 다형성 행동을 배제합니다.
el.pescado

진실. 나는 그것이 가장 많이 사용하는 형태이기 때문에 그것을 언급하려고 생각했습니다 (다시 행동에 대한 코딩에서 상속만을 사용합니다-코드 재사용이 아니므로 다형성 행동이 필요한 경우는 많지 않습니다) ..
utnapistim
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.