상속보다 구성을 선호해야하는 이유는 무엇입니까?


109

나는 항상 구성이 상속보다 선호된다는 것을 읽었습니다. 예를 들어, 상속 과는 달리 컴포지션을 사용하는 것을 옹호하는 것과 같은 종류블로그 게시물은 다형성이 어떻게 이루어지는 지 알 수 없습니다.

그러나 사람들이 구성을 선호한다고 말할 때 실제로 구성과 인터페이스 구현의 조합을 선호한다는 느낌이 들었습니다. 상속없이 어떻게 다형성을 얻을 수 있습니까?

상속을 사용하는 구체적인 예는 다음과 같습니다. 이것이 작곡을 사용하도록 어떻게 바뀌었을 것이며, 무엇을 얻을 수 있습니까?

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}

57
아니, 사람들이 구성을 선호한다고 말할 때, 그들은 결코 상속을 사용 하지 않고 구성을 선호 한다는 의미 입니다. 귀하의 모든 질문은 잘못된 전제에 기초합니다. 적절한 경우 상속을 사용하십시오.
Bill the Lizard


2
저는 Bill에 동의합니다. 상속을 사용하는 것이 GUI 개발에서 일반적인 관행임을 알았습니다.
Prashant Cholachagudda

2
정사각형은 두 개의 삼각형의 구성이기 때문에 작곡을 사용하고 싶습니다. 사실 타원 이외의 모든 모양은 삼각형의 구성이라고 생각합니다. 다형성은 계약 의무에 관한 것이며 상속에서 100 % 제거되었습니다. 누군가 피라미드를 생성 할 수 있기를 원했기 때문에 삼각형에 이상한 점이 추가되면 삼각형에서 상속하면 육각형에서 3D 피라미드를 생성하지 않더라도 그 모든 것을 얻을 수 있습니다. .
Jimmy Hoffa

2
@BilltheLizard 나는 그것이 상속을 결코 사용 하지 않는다는 것을 의미 한다고 말하는 많은 사람들 이 생각하지만, 그들은 틀렸다.
immibis

답변:


51

다형성이 반드시 상속을 의미하지는 않습니다. 상속은 다형성 동작을 구현하기위한 쉬운 수단으로 사용되는 경우가 많습니다. 왜냐하면 유사한 동작 객체를 완전히 공통적 인 루트 구조와 동작을 갖는 것으로 분류하는 것이 편리하기 때문입니다. 수년 동안 본 모든 자동차 및 개 코드 예제를 생각해보십시오.

그러나 동일하지 않은 개체는 어떻습니까? 자동차와 행성을 모델링하는 것은 매우 다르지만 둘 다 Move () 동작을 구현할 수 있습니다.

실제로, 당신은 당신이 말했을 때 기본적으로 당신 자신의 질문에 대답했습니다 "But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation.". 일반적인 행동은 인터페이스와 행동 합성을 통해 제공 될 수 있습니다.

어느 쪽이 더 좋은지에 대한 대답은 다소 주관적이며 실제로 시스템 작동 방식, 상황 및 구조적으로 의미가있는 것, 테스트 및 유지 관리가 얼마나 쉬운 지에 따라 결정됩니다.


실제로, "인터페이스를 통한 다형성"은 얼마나 자주 나타나며 (언어 표현의 착취가 아닌) 정상적인 것으로 간주됩니다. 상속을 통한 다형성은 신중한 디자인에 의한 것이지, 사양 이후에 발견 된 언어 (C ++)의 결과는 아닙니다.
samis

1
누가 행성과 자동차에서 move ()를 호출하고 동일한 것으로 간주합니까?! 여기서 질문은 그들이 움직일 수있는 어떤 상황에 있는가? 둘 다 단순한 2D 게임에서 2D 객체 인 경우 이동을 상속 할 수 있습니다. 대규모 데이터 시뮬레이션의 객체 인 경우 동일한 기반에서 상속하도록하는 것이 의미가 없을 수 있습니다. 인터페이스
NikkyD

2
그것은 @SamusArin 표시 까지 모든 곳에서 , 인터페이스를 지원하는 언어로 완벽하게 정상으로 간주됩니다. "언어 표현력의 착취"란 무엇을 의미합니까? 이것이 바로 인터페이스 입니다 .
Andres F.

@AndresF. Observer 패턴을 보여주는 "Object Oriented Analysis and Design with Applications"를 읽는 동안 예제에서 "인터페이스를 통한 다형성"을 보았습니다. 그런 다음 내 대답을 깨달았습니다.
samis

@AndresF. 나는 마지막 (첫 번째) 프로젝트에서 다형성을 어떻게 사용했기 때문에 내 비전이 이것에 대해 약간 눈이 멀었다 고 생각합니다. 나는 모두 같은 기반에서 파생 된 5 가지 레코드 유형을 가지고 있습니다. 어쨌든 깨달음에 감사드립니다.
samis

79

선호하는 구성은 다형성에 관한 것이 아닙니다. 그것이 그것의 일부이지만, 사람들이 실제로 의미하는 바는 "구성과 인터페이스 구현의 조합을 선호합니다"라는 것이 맞습니다. 그러나 (많은 상황에서) 구성을 선호하는 이유는 심오합니다.

다형성 은 여러 가지 방식으로 동작하는 것입니다. 따라서 제네릭 / 템플릿은 단일 코드 조각이 유형에 따라 동작을 변화시킬 수있는 "다형성"기능입니다. 실제로,이 유형의 다형성은 실제로 가장 잘 작동하며 일반적으로 변수가 매개 변수에 의해 정의되기 때문에 파라 메트릭 다형성 이라고합니다.

많은 언어는 "오버로딩"또는 임시 다형성이라는 형태를 제공합니다. 여기서 동일한 이름을 가진 여러 프로 시저가 임시 방식으로 정의되고 언어에 따라 선택됩니다 (가장 구체적으로 표시됨). 개발 된 규칙을 제외하고는 두 절차의 동작을 연결하는 것이 없기 때문에 이것은 가장 잘 동작하지 않는 다형성입니다.

세 번째 종류의 다형성은 아형 다형성 입니다. 여기에서 주어진 유형에 정의 된 절차는 해당 유형의 전체 "하위 유형"패밀리에서 작동 할 수 있습니다. 인터페이스를 구현하거나 클래스를 확장 할 때 일반적으로 하위 유형을 만들려는 의도를 선언합니다. 진정한 하위 유형은 Liskov의 대체 원칙 에 의해 관리됩니다즉, 수퍼 타입의 모든 객체에 대해 무언가를 증명할 수 있으면 서브 타입의 모든 인스턴스에 대해 증명할 수 있습니다. 그러나 C ++ 및 Java와 같은 언어에서 사람들은 일반적으로 하위 클래스에 대해 사실 일 수도 있고 그렇지 않을 수도있는 클래스에 대해 시행되지 않고 문서화되지 않은 가정을 가지고 있기 때문에 위험합니다. 즉, 코드는 실제보다 더 증명 가능한 것처럼 작성되므로 부주의하게 하위 유형을 지정하면 전체 문제가 발생합니다.

상속 은 실제로 다형성과 무관합니다. 자체에 대한 참조가있는 "T"가있는 경우 "T"에서 "S"로의 참조를 "S"에 대한 참조로 대체하여 "T"에서 새 "S"를 작성할 때 상속이 발생합니다. 상속은 많은 상황에서 발생할 수 있기 때문에 의도적으로 모호합니다.하지만 가장 일반적인 것은 this가상 함수에 의해 호출되는 this포인터를 하위 유형에 대한 포인터로 대체하는 효과가있는 객체를 서브 클래 싱하는 것입니다 .

상속은 상속이 혼란을 야기 할 수있는 모든 강력한 것들과 마찬가지로 위험 합니다. 예를 들어, 어떤 클래스에서 상속 할 때 메서드를 재정의한다고 가정 해 봅시다. 클래스의 다른 메서드가 상속 한 메서드가 특정 방식으로 동작한다고 가정 할 때까지는 모두 훌륭합니다. 결국 원래 클래스의 작성자가 설계 한 방식입니다 . 재정의 되도록 설계된 경우가 아니면 다른 방법으로 호출 된 모든 메서드를 비공개 또는 비가 상 (최종) 선언하여이를 부분적으로 보호 할 수 있습니다 . 그럼에도 불구하고 항상 충분하지는 않습니다. 때로는 다음과 같은 것을 볼 수도 있습니다 (의사 Java에서는 C ++ 및 C # 사용자가 읽을 수 있음)

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

여러분은 이것이 사랑스럽고 자신 만의 "일"을하는 방식을 가지고 있지만 상속을 사용하여 "moreThings"를 할 수있는 능력을 얻습니다.

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

그리고 모든 것이 좋고 좋습니다. WayOfDoingUsefulThings하나의 방법을 대체해도 다른 방법의 의미는 변경되지 않도록 설계되었습니다. 기다리십시오. 그것은 그대로있는 것처럼 보이지만 doThings중요한 변경 가능한 상태로 변경되었습니다. 따라서 재정의 가능한 함수를 호출하지는 않았지만

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

이제 당신이 그것을 전달할 때 예상했던 것과 다른 것을 수행합니다 MyUsefulThings. 더 나쁜 것은, 당신 WayOfDoingUsefulThings은 그러한 약속을 했는지 알지 못할 수도 있습니다 . 아마 dealWithStuff같은 라이브러리에서 제공 WayOfDoingUsefulThings하고 getStuff()(생각조차 라이브러리에서 내보낼 수 없습니다 친구 클래스 C ++로). 더 나쁜 것은, 당신이 그것을 실현하지 않고 언어의 정적 검사를 물리 치고 : dealWithStuff했다 WayOfDoingUsefulThings단지는 것이다 있는지 확인하기 위해 getStuff()특정한 방식으로 행동 기능.

구성 사용

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

정적 타입 안전을 되 찾습니다. 일반적으로 구성은 하위 유형 지정을 구현할 때 상속보다 사용하기 쉽고 안전합니다. 또한 최종 메소드를 무시할 수 있습니다. 즉 , 대부분의 경우 인터페이스를 제외하고 모든 것을 최종 / 비 가상적 으로 선언 할 있습니다.

더 나은 언어에서는 delegation키워드를 사용 하여 상용구를 자동으로 삽입합니다 . 대부분은 그렇지 않으므로 단점은 더 큰 클래스입니다. 그럼에도 불구하고 IDE가 위임 인스턴스를 작성하도록 할 수 있습니다.

이제 인생은 다형성에 관한 것이 아닙니다. 항상 하위 유형을 지정할 필요는 없습니다. 다형성의 목표는 일반적으로 코드 재사용 이지만 그 목표를 달성하는 유일한 방법은 아닙니다. 종종 기능 관리 방법으로 하위 유형 다형성없이 구성을 사용하는 것이 합리적입니다.

또한 행동 상속에는 그 용도가 있습니다. 컴퓨터 과학에서 가장 강력한 아이디어 중 하나입니다. 단지 대부분의 경우 인터페이스 상속 및 구성 만 사용하여 좋은 OOP 응용 프로그램을 작성할 수 있습니다. 두 가지 원칙

  1. 상속 또는 디자인 금지
  2. 구성 선호

위의 이유에 대한 좋은 가이드이며 실질적인 비용이 발생하지 않습니다.


4
좋은 대답입니다. 상속을 통해 코드 재사용을 달성하는 것은 명백한 잘못된 길이라고 요약합니다. 상속은 매우 강력한 제약이며 ( "파워"를 추가하는 것은 잘못된 유추입니다!) 상속 된 클래스간에 강한 의존성을 만듭니다. 너무 많은 의존성 = 나쁜 코드 : 그래서 상속은 일반적으로 통일 된 인터페이스에 빛나는 (일명 "와 같은 동작") (= 상속 클래스의 복잡성을 숨기는) 아무것도 두 번 생각이나 구성을 사용하기 위해 ...

2
이것은 좋은 대답입니다. +1. 적어도 내 눈에는 여분의 합병증이 상당한 비용으로 보일 수 있지만 이것은 개인적으로 작곡을 선호하는 데 큰 도움이됩니다. 특히 인터페이스, 컴포지션 및 DI를 통해 단위 테스트 친화적 인 코드를 많이 만들려고 할 때 (다른 것을 추가하고 있음을 알고 있음) 누군가 다른 파일을 여러 번 살펴보면 매우 쉽게 조회 할 수 있습니다. 몇 가지 세부 사항. 상속 디자인 원칙에 대한 구성 만 다룰 때에도 왜 이것이 더 자주 언급되지 않습니까?
Panzercrisis

27

사람들이 이것이 말하는 이유는 상속을 통한 다형성-강의에서 벗어난 OOP 프로그래머를 시작하면 많은 다형성 방법으로 큰 클래스를 작성하는 경향이 있으며 길 어딘가에서 유지 보수가 불가능한 혼란에 빠지기 때문입니다.

전형적인 예는 게임 개발의 세계에서 비롯됩니다. 플레이어의 우주선, 괴물, 총알 등 모든 게임 엔티티에 대한 기본 클래스가 있다고 가정하십시오. 각 엔티티 유형에는 자체 서브 클래스가 있습니다. 상속 방식은 예를 들어, 몇 다형성 방법을 사용 update_controls(), update_physics(), draw()등, 각각의 서브 클래스를 구현합니다. 그러나 이것은 관련없는 기능을 결합한다는 것을 의미합니다. 객체가 움직이는 것처럼 보이는 것과 관련이 없으며 AI에 대해 알 필요가 없습니다. 컴포지션 방식은 대신 EntityBrain(하위 클래스가 AI 또는 플레이어 입력을 EntityPhysics구현 함 ), (하위 클래스가 이동 물리를 구현 함) 및 EntityPainter(하위 클래스가 그림을 처리 함), 비다 형성 클래스와 같은 몇 가지 기본 클래스 (또는 인터페이스)를 정의합니다.Entity각 인스턴스를 하나씩 보유합니다. 이런 식으로 모든 모양을 모든 물리 모델 및 AI와 결합 할 수 있으며,이를 별도로 유지하므로 코드도 훨씬 깨끗해집니다. 또한 "레벨 1에서는 풍선 몬스터처럼 보이지만 레벨 15에서는 미친 광대처럼 행동하는 몬스터를 원합니다"와 같은 문제가 사라집니다. 적절한 구성 요소를 가져와 서로 붙입니다.

구성 방식은 여전히 ​​각 구성 요소 내에서 상속을 사용합니다. 이상적으로는 인터페이스와 해당 구현을 사용하는 것이 이상적입니다.

여기서는 "문제의 분리"가 핵심 표현입니다. 물리를 표현하고, 인공 지능을 구현하며, 개체를 그리는 것은 세 가지 문제이며, 개체로 결합하는 것은 네 번째입니다. 구성 접근 방식을 사용하면 각 관심사가 하나의 클래스로 모델링됩니다.


1
이것은 모두 좋으며 원래 질문과 관련된 기사에서 옹호되었습니다. 다형성 (C ++)을 유지하면서 이러한 모든 엔티티를 포함하는 간단한 예를 제공 할 수 있다면 (시간이 오면 언제든지) 좋아합니다. 또는 저에게 자원을 알려주십시오. 감사.
MustafaM

나는 당신의 대답이 매우 오래되었다는 것을 알고 있지만, 당신이 내 게임에서 언급 한 것과 똑같은 일을했고 지금은 상속 접근법에 대한 작곡을하려고합니다.
Sneh

"나는 닮은 괴물을 원해 ..." 인터페이스는 구현을 제공하지 않으므로 모양과 동작 코드를 한 가지 방법으로 복사해야합니다.
NikkyD

1
@NikkyD 당신은 걸릴 MonsterVisuals balloonVisualsMonsterBehaviour crazyClownBehaviour와의 인스턴스를 Monster(balloonVisuals, crazyClownBehaviour)함께, Monster(balloonVisuals, balloonBehaviour)그리고 Monster(crazyClownVisuals, crazyClownBehaviour)레벨 1 및 레벨 15에 인스턴스화 된 경우
Caleth에게

13

당신이 제시 한 예는 상속이 자연스러운 선택입니다. 나는 컴포지션이 상속보다 항상 더 나은 선택이라고 주장하는 사람은 없다고 생각합니다. 그것은 단지 지침 일 뿐이며, 고도로 전문화 된 많은 객체를 만드는 것보다 비교적 간단한 여러 객체를 조립하는 것이 더 낫다는 것을 의미합니다.

위임은 상속 대신 구성을 사용하는 방법의 한 예입니다. 위임을 사용하면 하위 클래스없이 클래스의 동작을 수정할 수 있습니다. 네트워크 연결을 제공하는 클래스 인 NetStream을 고려하십시오. 일반적인 네트워크 프로토콜을 구현하기 위해 NetStream을 서브 클래 싱하는 것이 당연 할 수 있으므로 FTPStream 및 HTTPStream을 생각해 낼 수 있습니다. 그러나 UpdateMyWebServiceHTTPStream과 같은 단일 목적을 위해 매우 특정한 HTTPStream 서브 클래스를 작성하는 대신 해당 오브젝트에서 수신 한 데이터로 수행 할 작업을 알고있는 대리자와 함께 기존의 HTTPStream 인스턴스를 사용하는 것이 더 좋습니다. 더 좋은 이유는 유지해야하지만 재사용 할 수없는 클래스의 확산을 피할 수 있기 때문입니다.


11

이주기는 소프트웨어 개발 담론에서 많이 볼 수 있습니다.

  1. 일부 기능 또는 패턴 ( "패턴 X"라고 함)은 특정 목적에 유용한 것으로 밝혀졌습니다. 블로그 게시물은 패턴 X에 대한 미덕을 강조하여 작성되었습니다.

  2. 과대 광고는 일부 사람들이 가능할 때마다 패턴 X를 사용해야한다고 생각 하게합니다 .

  3. 다른 사람들은 패턴 X가 적절하지 않은 상황에서 패턴 X가 사용되는 것을보고 짜증을 내며, 항상 패턴 X를 사용 해서는 안되며 일부 상황에서는 해롭다는 블로그 게시물을 작성 합니다.

  4. 반발은 일부 사람들이 패턴 X가 항상 해롭고 절대 사용 해서는 안된다고 믿게합니다 .

이 과대 / 백래시주기 GOTO는 패턴에서 SQL, NoSQL 및 상속 과 같은 거의 모든 기능에서 발생 합니다. 해독제는 항상 상황을 고려하는 것 입니다.

Circle후손을 갖는 Shape것은 상속을 지원하는 OO 언어에서 상속이 사용되는 방식 과 정확히 같습니다 .

엄지 손가락의 규칙은 "상속보다 컴포지션을 선호합니다"는 문맥없이 실제로 오해의 소지가 있습니다. 상속이 더 적절한 경우 상속을 선호해야하지만 구성이 더 적절한 경우 구성을 선호해야합니다. 이 판결은 과대 광고의 2 단계에있는 사람들을 대상으로하며, 상속은 모든 곳에서 사용되어야한다고 생각합니다. 그러나 그 순환이 진행되어 오늘날 일부 사람들은 상속 자체가 다소 나쁘다고 생각하게 만드는 것 같습니다.

망치 대 드라이버처럼 생각하십시오. 망치보다 드라이버를 선호해야합니까? 문제는 이해가되지 않습니다. 작업에 적합한 도구를 사용해야하며, 수행해야하는 작업에 따라 다릅니다.

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