상속보다 구성을 선호하십니까?


1603

상속보다 구성을 선호하는 이유는 무엇입니까? 각 접근법마다 어떤 절충점이 있습니까? 구성보다 상속을 언제 선택해야합니까?



40
이 질문에 대한 좋은 기사가 여기 있습니다 . 내 개인적인 의견은 디자인에 "더 나은"또는 "더 나쁜"원칙이 없다는 것입니다. 구체적인 작업에는 "적절한"디자인과 "부적절한"디자인이 있습니다. 즉, 상황에 따라 상속 또는 구성을 모두 사용합니다. 목표는 더 작은 코드를 생성하고, 더 쉽게 읽고, 재사용하며, 결국 더 확장하는 것입니다.
m_pGladiator

1
한 문장에서 상속은 공개 메소드가 있고 공개 메소드를 변경하면 공개됩니다. 컴포지션이 있고 구성된 객체가 변경된 경우 게시 된 API를 변경할 필요가 없습니다.
Tomer Ben David

답변:


1188

상속 가능성이 높고 나중에 수정하기가 쉽지만 작성 항상 접근 방식을 사용하지 않으므로 상속보다 구성을 선호하십시오. 컴포지션을 사용하면 Dependency Injection / Setter를 사용하여 동작을 쉽게 변경할 수 있습니다. 대부분의 언어에서는 여러 유형에서 파생 할 수 없으므로 상속이 더 엄격합니다. 따라서 TypeA에서 파생되면 거위는 다소 요리됩니다.

위의 내 산 테스트는 다음과 같습니다.

  • TypeB가 TypeA가 예상되는 곳에서 TypeB를 사용할 수 있도록 TypeA의 완전한 인터페이스 (모든 공용 메소드)를 공개하려고합니까? 상속을 나타냅니다 .

    • 예를 들어 Cessna 복엽 비행기는 비행기의 전체 인터페이스를 보여줍니다. 따라서 비행기에서 파생되는 것이 적합합니다.
  • TypeB는 TypeA에 의해 노출되는 동작의 일부 / 일부만 원합니까? 구성이 필요함을 나타냅니다 .

    • 예를 들어 조류는 비행기의 비행 행동 만 필요할 수 있습니다. 이 경우 인터페이스 / 클래스 / 둘 다로 추출하여 두 클래스의 멤버로 만드는 것이 좋습니다.

업데이트 : 방금 내 대답으로 돌아 왔으며 '이 유형에서 상속해야합니까?'에 대한 테스트로 Barbara Liskov의 Liskov 대체 원칙에 대한 언급이 없으면 불완전한 것으로 보입니다.


81
두 번째 예는 Head First Design Patterns ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) 서적 에서 바로 나온 것입니다.
Jeshurun

4
"B 타입 B가 TypeA가 예상되는 곳에서 TypeB를 사용할 수 있도록 TypeA의 전체 인터페이스를 공개하고 싶습니까?" 그러나 이것이 사실이라면 TypeB는 TypeC의 완전한 인터페이스를 노출시킵니다. 만약 TypeC가 아직 모델링되지 않았다면?
Tristan

4
당신은 내가 가장 기본적인 테스트라고 생각하는 것을 암시한다 : "이 객체는 기본 유형의 객체를 기대하는 코드로 사용할 수 있어야한다". 대답이 예이면 개체 상속 되어야합니다 . 그렇지 않다면 아마 안됩니다. 만약 내가 진실을 알고 있다면, 언어는 "이 클래스"를 가리키는 키워드를 제공 할 것이고, 다른 클래스처럼 행동해야하지만 대체 할 수없는 클래스를 정의하는 수단을 제공 할 것입니다. 클래스 "참조가 자체로 바)).
supercat

22
@Alexey-요점은 'Cessna 복엽 비행기를 놀라지 않고 비행기를 기대하는 모든 고객에게 전달할 수 있습니까?'입니다. 그렇다면 기회는 상속을 원할 것입니다.
Gishu

9
실제로 상속이 내 대답이었던 예를 생각하기 위해 고심하고 있습니다. 통합, 구성 및 인터페이스로 인해 더 우아한 솔루션이 만들어지는 경우가 많습니다. 위의 예 중 많은 부분이 이러한 접근 방식을 사용하여 더 잘 설명 될 수 있습니다.
Stuart Wakefield

414

격리 관계 있다고 생각하십시오 . 자동차에는 "엔진", 사람에는 "이름"등이 있습니다.

상속 관계 라고 생각하십시오 . 자동차는 "차량", 사람은 "동물"등

나는이 접근법에 대해 아무런 신용도받지 않는다. 나는에서 똑바로했다 전체 코드의 두 번째 판 에 의해 스티브 맥코넬 , 6.3 절 .


107
이것은 항상 완벽한 접근 방법은 아니며 단순히 좋은 지침입니다. Liskov 대체 원칙이 훨씬 정확합니다 (실패).
Bill K

40
"내 차에는 차량이 있습니다." 프로그래밍 컨텍스트가 아닌 별도의 문장으로 생각하면 전혀 의미가 없습니다. 이것이이 기술의 요점입니다. 어색하게 들리면 잘못되었을 수 있습니다.
Nick Zalutskiy

36
@Nick Sure 그러나 "My Car has a VehicleBehavior"가 더 의미가 있습니다 ( "Vehicle"클래스의 이름은 "VehicleBehavior"라고 생각합니다). 따라서 "a"대 "is"비교를 바탕으로 결정을 내릴 수 없으며 LSP를 사용해야하거나 실수를 할 것입니다.
Tristan

35
"대신"은 "처럼 행동한다"는 생각입니다. 상속은 의미론 만이 아니라 행동을 상속하는 것에 관한 것입니다.
ybakos

4
@ybakos "처럼 동작"은 상속없이 인터페이스를 통해 달성 할 수 있습니다. 위키피디아 (Wikipedia) : "상속성을 통한 구성의 구현은 일반적으로 시스템이 보여야하는 행동을 나타내는 다양한 인터페이스의 생성으로 시작됩니다 ... 따라서 시스템 행동은 상속없이 실현됩니다."
DavidRR

210

차이점을 이해하면 설명하기가 더 쉽습니다.

절차 코드

예를 들어 클래스를 사용하지 않는 PHP (특히 PHP5 이전)가 있습니다. 모든 논리는 일련의 기능으로 인코딩됩니다. 헬퍼 함수 등을 포함하는 다른 파일을 포함하고 함수에서 데이터를 전달하여 비즈니스 로직을 수행 할 수 있습니다. 애플리케이션이 커질수록 관리하기가 매우 어려울 수 있습니다. PHP5는 더 많은 객체 지향 디자인을 제공함으로써이를 해결하려고합니다.

계승

이것은 클래스 사용을 권장합니다. 상속은 OO 디자인의 세 가지 신조 중 하나입니다 (상속, 다형성, 캡슐화).

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

이것은 직장에서의 상속입니다. 직원은 "개인"이거나 개인으로부터 상속받습니다. 모든 상속 관계는 "is-a"관계입니다. 또한 Employee는 Person의 Title 속성을 숨 깁니다. 이는 Employee를 의미합니다. Title은 Person이 아닌 Employee의 제목을 반환합니다.

구성

상속보다 구성이 선호됩니다. 간단히 말하면 다음과 같습니다.

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

구성은 일반적으로 "있다"또는 "사용"관계입니다. 여기 Employee 클래스에는 Person이 있습니다. Person으로부터 상속받지 않고 대신 Person 오브젝트를 전달받습니다. 이것이 "Person"을 갖는 이유입니다.

상속에 대한 구성

이제 관리자 유형을 만들고 싶다고 가정 해 봅시다.

class Manager : Person, Employee {
   ...
}

그러나이 예제는 제대로 작동하지만 Person과 Employee가 모두 선언 한 경우에는 어떻게 Title됩니까? Manager.Title이 "Manager of Operations"또는 "Mr."를 반환해야합니까? 구성에서이 모호성은 더 잘 처리됩니다.

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Manager 개체는 직원과 사람으로 구성됩니다. 직함 행동은 직원으로부터 가져옵니다. 이 명시적인 구성은 무엇보다도 애매 모호함을 없애고 버그가 줄어 듭니다.


6
상속 : 모호성이 없습니다. 요구 사항에 따라 Manager 클래스를 구현하고 있습니다. 따라서 요구 사항이 지정된 경우 "작업 관리자"를 반환하고 그렇지 않으면 기본 클래스의 구현을 사용합니다. 또한 Person을 추상 클래스로 만들어 다운 스트림 클래스가 Title 속성을 구현하도록 할 수 있습니다.
Raj Rao

68
"상속 상속 구성"이라고 말할 수도 있지만 "항상 상속 구성"이라는 의미는 아닙니다. "I"는 상속을 의미하며 코드 재사용으로 이어집니다. 직원은 개인입니다 (직원에게는 개인이 없습니다).
Raj Rao

36
예는 혼란 스럽습니다. 직원은 사람이므로 상속을 사용해야합니다. 기술적으로 코드에서 선언 할 수 있더라도 도메인 모델에서는 잘못된 관계이기 때문에이 예제에는 컴포지션을 사용하지 않아야합니다.
Michael Freidgeim

15
이 예에 동의하지 않습니다. Employee is-a Person은 상속을 올바르게 사용하는 교과서 사례입니다. 또한 제목 필드의 재정의 "문제"는 의미가 없다고 생각합니다. Employee.Title shadows Person.Title은 프로그래밍이 좋지 않다는 신호입니다. 결국, "미스터"입니다. "운영 관리자"는 실제로 사람의 동일한 측면 (소문자)을 언급합니까? Employee.Title의 이름을 변경하여 실제 상황에 적합한 Employee의 Title 및 JobTitle 속성을 참조 할 수 있습니다. 더욱이, 관리자에 대한 이유가 없습니다 (계속 ...)
Radon Rosborough

9
(... 계속) Person과 Employee 모두에서 상속합니다. 결국 Employee는 이미 Person으로부터 상속받습니다. 사람이 관리자 및 에이전트 일 수있는보다 복잡한 모델에서는 다중 상속을 신중하게 사용할 수 있지만 여러 환경에서 관리자 (직원이 포함 된 추상 역할 클래스)를 갖는 것이 바람직합니다. (관리자) 및 에이전트 (계약 및 기타 정보를 포함)가 상속합니다. 그런 다음 직원은 여러 역할을 가진 사람입니다. 따라서 구성과 상속이 모두 올바르게 사용됩니다.
Radon Rosborough

141

상속을 통해 부인할 수없는 모든 혜택을 누리면 다음과 같은 단점이 있습니다.

상속의 단점 :

  1. 런타임에 수퍼 클래스에서 상속 된 구현을 변경할 수 없습니다 (물론 상속은 컴파일 타임에 정의되므로).
  2. 상속은 서브 클래스를 부모 클래스 구현의 세부 사항에 노출 시키므로 상속이 캡슐화를 깨뜨리는 경우가 종종 있습니다 (구현이 아닌 인터페이스에만 집중해야하므로 서브 클래 싱으로 재사용하는 것이 항상 바람직하지는 않습니다).
  3. 상속에 의해 제공되는 긴밀한 결합은 서브 클래스의 구현이 상위 클래스의 구현과 매우 밀접한 관계를 가지므로 부모 구현의 변경은 서브 클래스가 변경되도록합니다.
  4. 서브 클래 싱에 의한 과도한 재사용은 상속 스택을 매우 깊고 혼란스럽게 만들 수 있습니다.

반면에 객체 구성 은 런타임에 다른 객체에 대한 참조를 얻는 객체를 통해 정의됩니다. 이러한 경우 이러한 개체는 서로의 보호 된 데이터에 도달 할 수 없으며 (캡슐화 중단 없음) 서로의 인터페이스를 강제해야합니다. 이 경우에도 구현 종속성은 상속의 경우보다 훨씬 적습니다.


5
이것은 내 의견으로는 더 나은 답변 중 하나입니다. 제 경험에 비추어 구성 측면에서 문제를 다시 생각하려고하면 작고 간단하며 더 독립적이며 재사용 가능한 수업으로 이어질 수 있습니다. 보다 명확하고 작고 집중적 인 책임 범위를 갖습니다. 종종 이것은 작은 구성 요소가 일반적으로 자체적으로 설 수 있기 때문에 종속성 주입 또는 조롱 (테스트에서)과 같은 것들이 덜 필요하다는 것을 의미합니다. 단지 내 경험. YMMV :-)
mindplay.dk

3
이 게시물의 마지막 단락이 실제로 클릭되었습니다. 감사합니다.
Salx

87

상속보다 구성을 선호하는 또 다른 매우 실용적인 이유는 도메인 모델과 관련이 있으며 관계형 데이터베이스에 매핑해야합니다. 상속을 SQL 모델에 매핑하는 것은 실제로 어렵습니다 (항상 사용되지 않는 열 만들기, 뷰 사용 등)와 같은 모든 종류의 해킹 방법으로 끝납니다. 일부 ORML은이를 처리하려고하지만 항상 빠르게 복잡해집니다. 두 테이블 간의 외래 키 관계를 통해 컴포지션을 쉽게 모델링 할 수 있지만 상속은 훨씬 어렵습니다.


81

간단히 말해서 나는 "상속보다 구성을 선호한다"에 동의하지만, 종종 "코카콜라보다 감자를 선호한다"고 들린다. 상속 장소와 작곡 장소가 있습니다. 차이점을 이해해야합니다. 그러면이 질문은 사라집니다. 그것이 저에게 실제로 의미하는 것은 "상속을 사용하려는 경우 다시 생각하면 기회가 필요하다는 것"입니다.

먹고 싶을 때는 코카콜라보다 감자를, 마시고 싶을 때는 감자보다 코카콜라를 선호해야합니다.

서브 클래스를 생성한다는 것은 수퍼 클래스 메소드를 호출하는 편리한 방법 이상을 의미해야합니다. 서브 클래스 "is-a"수퍼 클래스가 구조적으로나 기능적으로 모두 수퍼 클래스로 사용될 수 있고 상속 할 때는 상속을 사용해야합니다. 그렇지 않은 경우-상속이 아니라 다른 것입니다. 구성은 개체가 다른 개체로 구성되거나 관계가있는 경우입니다.

그래서 누군가가 상속이나 구성이 필요한지 알지 못하는 경우 실제 문제는 그가 마시거나 먹고 싶어하는지 모른다는 것입니다. 문제 영역에 대해 더 많이 생각하고 더 잘 이해하십시오.


5
올바른 작업에 적합한 도구입니다. 망치가 렌치보다 물건을 두드리는 것이 낫다고해서 렌치를 "열등한 망치"로보아야한다는 의미는 아닙니다. 상속은 객체가 수퍼 클래스 객체로 동작하기 위해 서브 클래스에 추가 된 것들이 필요할 때 유용 할 수 있습니다. 예를 들어, InternalCombustionEngine파생 클래스가 있는 기본 클래스 를 고려하십시오 GasolineEngine. 후자는 스파크 플러그와 같은 것을 추가하지만 기본 클래스에는 없지만 InternalCombustionEngine스파크 플러그가 사용됩니다.
supercat

61

상속은 특히 절차 적 국가에서 오는 매우 유혹적이며 종종 기만적으로 우아해 보입니다. 내가해야 할 일은이 클래스의 기능을 다른 클래스에 추가하는 것입니다. 글쎄, 문제 중 하나는

상속은 아마도 최악의 결합 형태 일 것입니다

기본 클래스는 구현 된 세부 정보를 보호 된 멤버 형식으로 하위 클래스에 노출시켜 캡슐화를 중단합니다. 이것은 시스템을 단단하고 취약하게 만듭니다. 그러나 더 비극적 인 결함은 새로운 하위 클래스가 상속 체인의 모든 수하물과 의견을 가져 오는 것입니다.

Inheritance is Evil : DataAnnotationsModelBinder의 Epic Fail 기사 는 C #에서 이에 대한 예제를 보여줍니다. 컴포지션을 사용해야 할 때 상속을 사용하고 리팩토링 할 수있는 방법을 보여줍니다.


상속은 나쁘지 않고 단지 컴포지션의 특별한 경우입니다. 실제로 서브 클래스는 수퍼 클래스와 유사한 기능을 구현하고 있습니다. 제안 된 서브 클래스가 다시 구현되지 않고 단지 수퍼 클래스의 기능 만 사용 하는 경우 상속을 잘못 사용했습니다. 그것은 프로그래머의 실수이며 상속에 대한 반영이 아닙니다.
iPherian

42

Java 또는 C #에서 인스턴스화되면 객체의 유형을 변경할 수 없습니다.

따라서 개체가 다른 개체로 나타나거나 개체 상태 나 조건에 따라 다르게 동작해야하는 경우 구성 : 상태전략 디자인 패턴 참조를 사용 하십시오 .

객체의 유형이 동일해야하는 경우 상속 을 사용 하거나 인터페이스를 구현하십시오.


10
+1 나는 대부분의 상황에서 상속이 작동한다는 것을 점점 더 적게 발견했습니다. 나는 공유 / 상속 된 인터페이스와 객체 구성을 선호합니다 ... 또는 집계라고합니까? 묻지마, 나는 EE 학위를 받았습니다!
kenny

나는 이것이 상속에 대한 컴포지션이 적용되는 가장 일반적인 시나리오라고 생각한다. 둘 다 이론에 적합 할 수 있기 때문이다. 예를 들어, 마케팅 시스템에서는 개념이있을 수 있습니다 Client. 그런 다음 PreferredClient나중에 새로운 개념의 팝업이 나타납니다. PreferredClient상속 해야합니까 Client? 우선 고객은 '고객'입니까? 글쎄, 그렇게 빠르지는 않습니다 ... 당신이 말한 것처럼 객체는 런타임에 클래스를 변경할 수 없습니다. 작업을 어떻게 모델링 client.makePreferred()하시겠습니까? 아마도 답은 개념이없는 컴포지션을 사용하는 Account것입니다.
plalx

오히려 다양한 종류의 필요없이 Client클래스를, 아마도 단지의 개념을 캡슐화 한 거기 AccountStandardAccount또는를 PreferredAccount...
plalx

40

여기서 만족스러운 답변을 찾지 못하여 새로운 답변을 썼습니다.

" 상속보다 구성을 선호하는 "이유를 이해하려면 먼저이 단축 된 관용구에서 생략 된 가정을 되 찾아야합니다.

상속의 두 가지 이점이 있습니다 : 서브 타이핑 및 서브 클래 싱

  1. 서브 타이핑 은 유형 (인터페이스) 서명, 즉 API 세트를 따르는 것을 의미하며 서브 타이핑 다형성을 달성하기 위해 서명의 일부를 무시할 수 있습니다.

  2. 서브 클래 싱은 메소드 구현의 암묵적인 재사용을 의미합니다.

상속을 수행하는 두 가지 목적, 즉 서브 타이핑 지향과 코드 재사용 지향의 두 가지 이점이 있습니다.

코드 재사용이 유일한 목적 이라면 , 서브 클래 싱은 필요한 것 이상을 제공 할 수 있습니다. 즉, 부모 클래스의 일부 공용 메소드는 자식 클래스에 대해 의미가 없습니다. 이 경우, 대신에 상속 위에 조성물을 선호에, 조성은 요구 . 이것은 "is-a"대 "has-a"개념의 유래이기도합니다.

따라서 서브 타이핑을 목적으로하는 경우에만, 즉 새로운 클래스를 나중에 다형성 방식으로 사용하려는 경우에만 상속 또는 구성을 선택하는 문제에 직면하게됩니다. 이것은 논의중인 단축 관용구에서 생략되는 가정입니다.

하위 유형은 유형 서명을 준수하기 위해 작성시 항상 유형의 API를 적은 양으로 노출해야합니다. 이제 트레이드 오프가 시작됩니다.

  1. 상속은 재정의되지 않은 경우 간단한 코드 재사용을 제공하는 반면 컴포지션은 단순한 위임 작업 일지라도 모든 API를 다시 코딩해야합니다.

  2. 상속은 내부 다형성 사이트를 통해 직접적인 공개 재귀 를 제공합니다 this. 즉 공개 또는 비공개 ( 권장 하지는 않지만 ) 다른 멤버 함수에서 재정의 방법 (또는 유형 )을 호출 합니다. 오픈 재귀는 composition을 통해 시뮬레이션 할 수 있지만 추가 노력이 필요하며 항상 실행 가능한 것은 아닙니다 (?). 중복 질문에 대한 이 답변 은 비슷한 것을 말합니다.

  3. 상속은 보호 된 구성원을 노출시킵니다 . 이로 인해 부모 클래스의 캡슐화가 중단되고 하위 클래스에서 사용하는 경우 자식과 부모 간의 다른 종속성이 도입됩니다.

  4. 컴포지션은 반전 제어에 적합하며 데코레이터 패턴프록시 패턴 과 같이 종속성을 동적으로 주입 할 수 있습니다 .

  5. 컴포지션은 컴비 네이터 지향 프로그래밍, 즉 복합 패턴 과 같은 방식으로 작업 하는 이점이 있습니다.

  6. 컴포지션 은 인터페이스에 대한 프로그래밍을 즉시 따릅니다 .

  7. 컴포지션은 다중 상속 이 용이 하다는 이점이 있습니다 .

위의 장단점을 염두에두고 상속보다 구성을 선호합니다 . 그러나 밀접하게 관련된 클래스, 즉 암시 적 코드 재사용이 실제로 이익을 얻거나 개방형 재귀의 마법의 힘이 필요한 경우 상속이 선택됩니다.


34

개인적으로 저는 항상 상속보다 구성을 선호하는 법을 배웠습니다. 컴포지션으로는 해결할 수없는 상속으로 해결할 수있는 프로그래밍 문제는 없습니다. 경우에 따라 Interfaces (Java) 또는 Protocols (Obj-C)를 사용해야 할 수도 있습니다. C ++은 그러한 것을 알지 못하므로 추상 기본 클래스를 사용해야하므로 C ++에서 상속을 완전히 제거 할 수 없습니다.

컴포지션은 더 논리적이고, 더 나은 추상화, 더 나은 캡슐화, 더 나은 코드 재사용 (특히 매우 큰 프로젝트에서)을 제공하며 코드의 어느 곳에서나 고립 된 변경을 수행하여 멀리서도 문제를 일으킬 가능성이 적습니다. 또한 " 단일 책임 원칙 "을 보다 쉽게지지 할 수 있습니다. " 단일 책임 원칙 "은 종종 " 계급이 바뀌어야 할 이유가 두 개 이상 있어야합니다. "로 요약됩니다 . 이는 모든 클래스가 특정 목적을 위해 존재하며 목적과 직접 관련된 방법 만 있습니다. 또한 매우 얕은 상속 트리를 사용하면 프로젝트가 실제로 커지기 시작할 때에도 개요를 훨씬 쉽게 유지할 수 있습니다. 많은 사람들은 상속이 우리의 실제 세계를 대표한다고 생각합니다꽤 잘하지만 그것은 사실이 아닙니다. 실제 세계는 상속보다 훨씬 더 많은 구성을 사용합니다. 당신이 손에 쥐고있을 수있는 거의 모든 실제 물체는 다른 작은 실제 물체로 구성되어 있습니다.

그러나 구성에는 단점이 있습니다. 상속을 완전히 건너 뛰고 컴포지션에만 중점을두면 상속을 사용하는 경우 필요하지 않은 몇 가지 추가 코드 줄을 작성해야하는 경우가 종종 있습니다. 당신은 또한 때때로 자신을 반복하도록 강요받으며 이것은 건조 원칙을 위반합니다(건조 = 자신을 반복하지 마십시오). 또한 작성에는 종종 위임이 필요하며, 메소드는이 호출을 둘러싼 다른 코드없이 다른 오브젝트의 다른 메소드를 호출합니다. 이러한 "이중 메서드 호출"(트리플 또는 쿼드 러플 메서드 호출로 쉽게 확장 될 수 있으며 그보다 훨씬 큼)은 상속보다 성능이 훨씬 떨어 지므로 부모의 메서드를 단순히 상속합니다. 상속 된 메소드를 호출하는 것은 상속되지 않은 메소드를 호출하는 것과 똑같이 빠르거나 약간 느릴 수 있지만 일반적으로 두 번의 연속 메소드 호출보다 여전히 빠릅니다.

대부분의 OO 언어는 다중 상속을 허용하지 않습니다. 다중 상속이 실제로 무언가를 구입할 수있는 몇 가지 경우가 있지만 규칙보다는 예외입니다. "여러 상속이이 문제를 해결하는 정말 멋진 기능"이라고 생각하는 상황에 처할 때마다 대개 몇 가지 추가 코드 줄이 필요할 수 있으므로 상속을 다시 생각해야하는 시점에 있습니다. 구성에 기반한 솔루션은 일반적으로 훨씬 더 우아하고 유연하며 미래의 증거로 판명됩니다.

상속은 정말 멋진 기능이지만 지난 몇 년 동안 과도하게 사용 된 것 같습니다. 사람들은 상속이 실제로 못, 나사 또는 완전히 다른 무엇이든 관계없이 모든 것을 못 박는 망치로 취급했습니다.


"많은 사람들은 상속이 우리의 실제 세계를 잘 대표한다고 생각하지만 그것은 사실이 아닙니다." 너무 많은! 세계의 거의 모든 프로그래밍 자습서와는 달리 실제 객체를 상속 체인으로 모델링하는 것은 장기적으로 나쁜 생각입니다. 믿을 수 없을 정도로 명백하고 타고난 단순한 is-a 관계가있을 때만 상속을 사용해야합니다. 등 TextFile입니다 File.
neonblitzer

25

나의 일반적인 경험 법칙 : 상속을 사용하기 전에 구성이 더 합리적인지 고려하십시오.

이유 : 서브 클래 싱은 일반적으로 더 복잡하고 연결성을 의미합니다. 즉 실수없이 변경, 유지 관리 및 확장하기가 어렵습니다.

Sun의 Tim Boudreau 가 제공 하는 훨씬 더 완전하고 구체적인 답변 :

내가 볼 때 상속 사용에 대한 일반적인 문제는 다음과 같습니다.

  • 무고한 행위는 예기치 않은 결과를 초래할 수 있습니다. 이것의 전형적인 예는 서브 클래스 인스턴스 필드가 초기화되기 전에 수퍼 클래스 생성자에서 재정의 가능한 메소드를 호출하는 것입니다. 완벽한 세상에서는 아무도 그렇게하지 않을 것입니다. 이것은 완벽한 세상이 아닙니다.
  • 서브 클래스가 메소드 호출 순서에 대한 가정을하는 등의 유혹을 제공합니다. 이러한 가정은 수퍼 클래스가 시간이 지남에 따라 진화 할 경우 안정적이지 않은 경향이 있습니다. 내 토스터와 커피 포트 비유 도 참조하십시오 .
  • 클래스가 더 무거워집니다. 수퍼 클래스가 생성자에서 어떤 작업을 수행하는지 또는 얼마나 많은 메모리를 사용할지 반드시 알 필요는 없습니다. 따라서 무고한 가벼운 객체를 만드는 것은 생각보다 훨씬 비쌀 수 있으며 수퍼 클래스가 진화하면 시간이 지남에 따라 변경 될 수 있습니다
  • 그것은 서브 클래스의 폭발을 장려합니다 . 클래스 로딩에는 시간이 걸리고 더 많은 클래스에는 메모리가 필요합니다. NetBeans 규모의 앱을 처리 할 때까지 이것은 문제가되지 않을 수 있지만 메뉴의 첫 표시가 대규모 클래스로드를 트리거했기 때문에 메뉴가 느려지는 등 실제 문제가있었습니다. 우리는보다 선언적인 구문과 다른 기술로 이동하여이 문제를 해결했지만 수정하는 데 시간이 걸렸습니다.
  • 그것은 나중에 변경 일을 어렵게 만든다 - 당신이 코드 공개 한 후, 당신이 결혼하고, 선택 - 당신은 슈퍼 클래스는 서브 클래스를 깰 것입니다 스와핑, 클래스 공개 한 경우. 따라서 실제 기능을 수퍼 클래스로 변경하지 않으면 나중에 필요한 것을 확장하지 않고 나중에 사용할 경우 훨씬 더 자유롭게 변경할 수 있습니다. 예를 들어 JPanel을 서브 클래 싱하는 경우를 생각해보십시오. 일반적으로 잘못되었습니다. 서브 클래스가 어딘가에 공개되어 있다면 그 결정을 다시 방문 할 기회가 없습니다. JComponent getThePanel ()으로 액세스하면 여전히 할 수 있습니다 (힌트 : API로 구성 요소의 모델을 노출하십시오).
  • 개체 계층 구조는 크기가 조정되지 않습니다 (또는 나중에 크기를 조정하는 것이 미리 계획하는 것보다 훨씬 어렵습니다) . 이것은 고전적인 "너무 많은 레이어"문제입니다. 아래에서이 문제에 대해 살펴보고 AskTheOracle 패턴이이를 해결하는 방법 (OOP 순수 주의자들을 불쾌하게 할 수 있음).

...

상속을 허용하면 소금 한 알을 가지고 할 수있는 일에 대한 나의 테이크는 다음과 같습니다.

  • 상수를 제외하고 필드를 노출하지 마십시오.
  • 방법은 추상적이거나 최종적이어야한다
  • 수퍼 클래스 생성자로부터 메소드를 호출하지 않습니다

...

이 모든 것은 큰 프로젝트보다 작은 프로젝트에 덜 적용되고 공개 프로젝트보다 작은 프로젝트에 덜 적용됩니다


25

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

다른 답변을 참조하십시오.

상속을 언제 사용할 수 있습니까?

다음 문장이 참일 때 클래스 Bar가 클래스 를 상속받을 수 있다고 종종 말합니다 Foo.

  1. 바는 foo입니다

불행히도 위의 테스트만으로는 신뢰할 수 없습니다. 대신 다음을 사용하십시오.

  1. 바는 foo, AND
  2. 바는 foos가 할 수있는 모든 것을 할 수 있습니다.

첫 번째 테스트는 모든 getterFoo의미를 갖도록하고 Bar(= 공유 속성) 두 번째 테스트는 모든 setterFoo의미를 갖도록합니다 Bar(= 공유 기능).

예 1 : 개-> 동물

개는 동물이며 개는 동물이 할 수있는 모든 일을 할 수 있습니다 (호흡, 사망 등). 따라서 클래스 Dog 클래스 상속 할 수 있습니다Animal .

예 2 : 원-/-> 타원

원은 타원이지만 원은 타원으로 할 수있는 모든 것을 할 수는 없습니다. 예를 들어, 타원은 뻗을 수 있지만 원은 늘릴 수 없습니다. 따라서 클래스 Circle 클래스 상속 할 수 없습니다Ellipse .

이것을 원형 타원 문제 라고합니다 . 이것은 실제로 문제 가 아니며, 첫 번째 테스트만으로는 상속이 가능하다는 결론을 내리기에 충분하지 않다는 명백한 증거입니다. 특히이 예제는 파생 클래스가 기본 클래스의 기능을 확장 해야 하며 절대로 제한 하지 않아야 함을 강조 합니다. 그렇지 않으면 기본 클래스를 다형성으로 사용할 수 없습니다.

상속을 언제 사용해야합니까?

상속 사용할 있다고 해서 반드시 컴포지션 을 사용해야 하는 것은 아닙니다. 상속은 암시 적 코드 재사용 및 동적 디스패치를 ​​허용하는 강력한 도구이지만 몇 가지 단점이 있으므로 구성이 종종 선호됩니다. 상속과 구성 간의 상충 관계는 분명하지 않으며 lcn의 답변 에 가장 잘 설명되어 있습니다.

일반적으로 다형성 사용이 매우 일반적 일 것으로 예상되는 경우 컴포지션보다 상속을 선택하는 경향이 있습니다.이 경우 동적 디스패치의 힘이 훨씬 더 읽기 쉽고 우아한 API로 이어질 수 있습니다. 예를 들어, WidgetGUI 프레임 워크에 다형성 클래스 또는 NodeXML 라이브러리에 다형성 클래스 를 사용하면 구성에 기반한 솔루션을 사용하는 것보다 훨씬 읽기 쉽고 직관적 인 API를 사용할 수 있습니다.

리스 코프 대체 원리

아시다시피 상속 가능 여부를 결정하는 데 사용되는 또 다른 방법을 Liskov 대체 원칙 이라고합니다 .

기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 몰래 파생 클래스의 객체를 사용할 수 있어야합니다.

기본적으로 이것은 기본 클래스를 다형성으로 사용할 수있는 경우 상속이 가능하다는 것을 의미합니다. 이는 "바는 foo이고 바는 foo가 할 수있는 모든 것을 수행 할 수 있습니다"라고 생각합니다.


원-타원 및 사각형-사각형 시나리오는 좋지 않은 예입니다. 서브 클래스는 수퍼 클래스보다 훨씬 더 복잡하므로 문제가 해결됩니다. 이 문제는 관계를 반전시켜 해결됩니다. 타원은 원에서 파생되고 직사각형은 정사각형에서 파생됩니다. 이 시나리오에서 구성을 사용하는 것은 매우 어리 석습니다.
퍼지 로직

@FuzzyLogic 동의하지만 실제로 내 게시물은이 경우 컴포지션 사용을 옹호하지 않습니다. 원형 타원 문제는 Circle이 타원에서 파생되어야한다는 결론을 내릴 때 "is-a"가 좋은 테스트가 아닌 이유에 대한 훌륭한 예라고했습니다. 실제로 LSP 위반으로 인해 Circle이 Ellipse에서 파생되지 않아야한다는 결론을 내릴 수있는 경우 가능한 옵션은 관계를 반전하거나 컴포지션을 사용하거나 템플릿 클래스를 사용하거나 추가 클래스 또는 도우미 기능이 포함 된보다 복잡한 디자인을 사용하는 것입니다. 등등 ... 그리고 결정은 분명히 사례별로 이루어져야합니다.
Boris Dalstein

1
@FuzzyLogic 그리고 Circle-Ellipse의 특정 사례에 대해 내가 옹호하는 것에 대해 궁금한 점이 있다면 Circle 클래스를 구현하지 말 것을 권합니다. 관계를 반전시키는 문제는 LSP를 위반한다는 것 computeArea(Circle* c) { return pi * square(c->radius()); }입니다. 함수를 상상하십시오 . 타원을 통과하면 분명히 손상됩니다 (반지름 ()은 무엇을 의미합니까?). 타원은 원이 아니므로 원에서 파생되지 않아야합니다.
Boris Dalstein

computeArea(Circle *c) { return pi * width * height / 4.0; }이제는 일반적입니다.
퍼지 로직

2
@FuzzyLogic 나는 동의하지 않습니다 : 이것은 Circle 클래스가 파생 클래스 Ellipse의 존재를 예상 했으므로 제공 width()하고 있음을 의미한다는 것을 알고 있습니다 height(). 라이브러리 사용자가 "EggShape"라는 다른 클래스를 작성하기로 결정하면 어떻게됩니까? "Circle"에서 파생되어야합니까? 당연히 아니지. 계란 모양은 원이 아니며 타원도 원이 아니므로 LSP를 끊기 때문에 Circle에서 파생되는 것은 없습니다. Circle * 클래스에서 작업을 수행하는 메서드는 원이 무엇인지에 대한 강력한 가정을하고 이러한 가정을 어기는 것은 거의 확실히 버그로 이어질 것입니다.
Boris Dalstein

19

상속은 매우 강력하지만 강제로 할 수는 없습니다 ( 원-타원 문제 참조 ). 진정한 "is-a"하위 유형 관계를 완전히 확신 할 수 없으면 컴포지션을 사용하는 것이 가장 좋습니다.


15

상속은 서브 클래스와 수퍼 클래스 사이에 강한 관계를 만듭니다. 서브 클래스는 수퍼 클래스의 구현 세부 사항을 알고 있어야합니다. 어떻게 수퍼 클래스를 만들 수 있는지 생각해야 할 때 슈퍼 클래스를 만드는 것이 훨씬 어렵습니다. 클래스 불변량을주의 깊게 문서화하고 재정의 가능한 다른 메소드가 내부적으로 사용하는 것을 명시해야합니다.

계층 구조가 실제로 관계를 나타내는 경우 상속이 유용 할 때가 있습니다. 이는 공개 폐쇄 원칙과 관련이 있으며, 수정을 위해 클래스를 닫아야하지만 확장 가능합니다. 그렇게하면 다형성을 가질 수 있습니다. 수퍼 유형과 해당 메소드를 처리하는 일반 메소드를 갖지만 동적 디스패치를 ​​통해 서브 클래스의 메소드가 호출됩니다. 이는 유연성이 뛰어나고 소프트웨어에 필수적인 간접 구현을 구현하는 데 도움이됩니다 (구현 세부 정보에 대해서는 덜 알고 있음).

그러나 상속은 쉽게 과도하게 사용되며 클래스 사이의 어려운 종속성으로 인해 추가 복잡성이 발생합니다. 또한 프로그램 실행 중 발생하는 상황을 이해하는 것은 계층과 메소드 호출의 동적 선택으로 인해 매우 어려워집니다.

작성을 기본값으로 사용하는 것이 좋습니다. 더 모듈화되어 있으며 후기 바인딩의 이점을 제공합니다 (구성 요소를 동적으로 변경할 수 있음). 또한 개별적으로 테스트하는 것이 더 쉽습니다. 그리고 클래스의 메소드를 사용해야하는 경우 특정 형식 (Liskov 대체 원칙)이 아니어야합니다.


3
상속이 다형성을 달성하는 유일한 방법은 아니라는 점에 주목할 가치가 있습니다. 데코레이터 패턴은 컴포지션을 통해 다형성의 모양을 제공합니다.
BitMask777

1
@ BitMask777 : 하위 유형 다형성은 다형성의 한 종류 일뿐이며 다른 하나는 매개 변수 다형성 일 것이므로 상속이 필요하지 않습니다. 또한 더 중요한 것은 상속에 대해 이야기 할 때 클래스 상속을 의미합니다. 즉, 여러 클래스에 대한 공통 인터페이스를 사용하여 하위 유형 다형성을 가질 수 있으며 상속 문제는 발생하지 않습니다.
egaga

2
@ engaga : 귀하의 의견 Inheritance is sometimes useful... That way you can have polymorphism은 상속과 다형성의 개념을 밀접하게 연결시키는 것으로 해석 했습니다 (문맥이 주어진 것으로 가정). 내 의견은 당신이 당신의 의견에서 분명히 밝힌 것을 지적하기위한 것입니다 : 상속은 다형성을 구현하는 유일한 방법이 아니며 실제로 구성과 상속을 결정할 때 결정적인 요소는 아닙니다.
BitMask777

15

항공기에 엔진과 날개의 두 부분 만 있다고 가정합니다.
그런 다음 항공기 클래스를 설계하는 두 가지 방법이 있습니다.

Class Aircraft extends Engine{
  var wings;
}

이제 항공기는 고정 날개로 시작할 수 있습니다
하여 회전 날개로 즉시 변경할 수 있습니다. 본질적
으로 날개가 달린 엔진입니다. 그러나
엔진을 즉시 변경 하려면 어떻게해야 합니까?

기본 클래스 Engine
속성 을 변경하기 위해 뮤 테이터를 노출 하거나 Aircraft다음과 같이 다시 디자인 합니다.

Class Aircraft {
  var wings;
  var engine;
}

이제 엔진을 즉시 교체 할 수 있습니다.


귀하의 게시물은 이전에 고려하지 않은 요점을 제시합니다. 총기와 같은 여러 부품으로 기계적인 물체를 유사하게 유지하기 위해 일반적으로 일련 번호가 표시된 부품이 하나 있습니다. 총기의 총기 (총의 경우 일반적으로 프레임). 하나는 다른 모든 부품을 교체해도 여전히 같은 총기를 가지고있을 수 있지만, 프레임에 금이 가거나 교체해야하는 경우, 원래 건의 다른 모든 부품과 함께 새 프레임을 조립 한 결과는 새로운 총이됩니다. 참고 사항
supercat

... 총의 여러 부품에 일련 번호가 표시되어 있어도 총에 여러 ID가있을 수있는 것은 아닙니다. 프레임의 일련 번호 만 건을 식별합니다. 다른 부품의 일련 번호는 해당 부품을 조립하기 위해 제조 된 건을 식별하며, 어떤 시점에서 조립 된 건이 아닐 수도 있습니다.
supercat


7

기본 클래스의 API를 "복사"/ 노출하려면 상속을 사용합니다. 기능 만 "복사"하려면 위임을 사용하십시오.

이에 대한 예 : 목록에서 스택을 생성하려고합니다. 스택에는 팝, 푸시 및 픽만 있습니다. push_back, push_front, removeAt 등의 기능이 스택에 필요하지 않다면 상속을 사용해서는 안됩니다.


7

이 두 가지 방법은 함께 잘 살면서 실제로 서로를 지탱할 수 있습니다.

컴포지션은 모듈 식으로 재생됩니다. 부모 클래스와 유사한 인터페이스를 만들고 새 객체를 만들고 호출을 위임합니다. 이러한 객체가 서로를 알 필요가 없다면 구성이 매우 안전하고 사용하기 쉽습니다. 여기에는 많은 가능성이 있습니다.

그러나 어떤 이유로 부모 클래스가 경험이없는 프로그래머를 위해 "자식 클래스"에서 제공하는 함수에 액세스해야하는 경우 상속을 사용하기에 좋은 장소 인 것 같습니다. 부모 클래스는 서브 클래스로 덮어 쓰는 자체 추상 "foo ()"를 호출 한 다음 추상베이스에 값을 제공 할 수 있습니다.

좋은 생각처럼 보이지만 많은 경우에 필요한 클래스 클래스에서 새 클래스를 상속하는 것보다 foo ()를 구현하는 객체를 클래스에 제공하는 것이 좋습니다 (또는 foo ()를 수동으로 제공된 값으로 설정). 지정할 foo () 함수

왜?

상속은 정보를 옮기는 나쁜 방법이기 때문 입니다.

컴포지션의 실제 우위는 다음과 같습니다. "상위 클래스"또는 "추상 작업자"는 특정 인터페이스를 구현하는 특정 "하위"객체를 집계 할 수 있습니다. 유형 입니다. 예를 들어 MergeSort 또는 QuickSort는 추상 비교 인터페이스를 구현하는 모든 객체 목록을 정렬 할 수 있습니다. 또는 "foo ()"를 구현하는 모든 객체 그룹과 "foo ()"를 가진 객체를 사용할 수있는 다른 객체 그룹은 함께 재생할 수 있습니다.

상속을 사용하는 세 가지 실제 이유를 생각할 수 있습니다.

  1. 인터페이스동일한 클래스가 많으며 작성 시간을 절약하고 싶습니다.
  2. 각 객체에 대해 동일한 기본 클래스를 사용해야합니다
  3. 어떤 경우에도 공개 할 수없는 개인 변수를 수정해야합니다

이것이 사실이라면 상속을 사용해야 할 것입니다.

이유 1을 사용하는 것은 나쁘지 않습니다. 객체에 견고한 인터페이스를 갖는 것이 좋습니다. 이 인터페이스가 단순하고 변경되지 않는 경우 구성을 사용하거나 상속없이 문제없이 수행 할 수 있습니다. 일반적으로 상속은 여기서 매우 효과적입니다.

이유가 2 번이면 조금 까다 롭습니다. 실제로 동일한 기본 클래스 만 사용해야합니까? 일반적으로 동일한 기본 클래스를 사용하는 것만으로는 충분하지 않지만 프레임 워크의 요구 사항이 될 수 있으므로 피할 수없는 디자인 고려 사항이 될 수 있습니다.

그러나 개인 변수, 사례 3을 사용하려면 문제가있을 수 있습니다. 전역 변수를 안전하지 않은 것으로 간주하면 상속을 사용하여 개인 변수에 대한 액세스도 안전하지 않은 것으로 고려해야 합니다. 전역 변수가 모두 나쁜 것은 아닙니다. 데이터베이스는 본질적으로 큰 전역 변수 세트입니다. 그러나 당신이 그것을 처리 할 수 ​​있다면, 그것은 좋습니다.


7

새로운 프로그래머를위한 다른 관점에서이 질문을 해결하려면 :

상속은 종종 객체 지향 프로그래밍을 배울 때 일찍 가르치므로 일반적인 문제에 대한 쉬운 해결책으로 간주됩니다.

공통 기능이 필요한 클래스가 세 개 있습니다. 따라서 기본 클래스를 작성하고 모든 클래스를 상속 받으면 모두 해당 기능을 가지게되므로 한 번만 유지하면됩니다.

그것은 훌륭하게 들리지만 실제로 여러 가지 이유 중 하나 때문에 거의 작동하지 않습니다.

  • 우리는 클래스가 갖고 싶은 다른 함수가 있다는 것을 발견했습니다. 우리가 기능을 클래스에 추가하는 방법이 상속을 통한다면, 결정해야합니다. 상속을받는 모든 클래스가 그 기능을 필요로하지 않더라도 기존의 기본 클래스에 추가해야합니까? 다른 기본 클래스를 작성합니까? 그러나 다른 기본 클래스에서 이미 상속받은 클래스는 어떻습니까?
  • 우리는 기본 클래스에서 상속받은 클래스 중 하나에 대해서만 기본 클래스가 약간 다르게 동작하기를 원한다는 것을 발견했습니다. 이제 기본 클래스로 돌아가서 약간의 가상 메소드를 추가하거나 더 나쁜 경우, "유형 A를 상속받은 경우, 이렇게하십시오. 그러나 B를 상속받은 경우에는 그렇게하십시오. " 여러 가지 이유로 나쁘다. 하나는 기본 클래스를 변경할 때마다 상속 된 모든 클래스를 효과적으로 변경한다는 것입니다. 우리는 클래스 A에서 약간 다른 행동이 필요하기 때문에 클래스 A, B, C 및 D를 실제로 변경하고 있습니다. 우리가 생각하는대로 조심스럽게 생각하면 클래스와 관련이없는 이유로 클래스 중 하나를 중단 할 수 있습니다 클래스.
  • 우리는 왜 이러한 클래스를 모두 서로 상속하기로 결정했는지 알 수 있지만 코드를 유지 관리해야하는 다른 사람에게는 의미가 없을 수도 있습니다. 우리는 그것들을 어려운 선택으로 강요 할 수 있습니다. 내가 필요한 변화를 만들기 위해 정말 못 생기고 지저분한 일을합니까 (이전 글 머리표 참조).

결국, 우리는 코드를 어려운 매듭으로 묶고 "쿨, 나는 상속에 대해 배웠고 이제는 그것을 사용했다"는 말을 제외하고는 아무런 혜택도 얻지 못했습니다. 우리 모두가 해냈 기 때문에 그것은 하찮은 것이 아닙니다. 그러나 아무도 우리에게하지 말라고했기 때문에 우리 모두는 그렇게했습니다.

누군가가 "상속보다 선호하는 구성"을 설명하자마자, 상속을 사용하여 클래스간에 기능을 공유하려고 할 때마다 다시 생각하고 대부분의 시간이 실제로 제대로 작동하지 않는다는 것을 깨달았습니다.

해독제는 단일 책임 원칙 입니다. 그것을 제약 조건으로 생각하십시오. 수업 한 가지만 해야합니다 . 나는 수업에 그 일을 묘사하는 이름을 수업에 줄 수 있어야 한다. (모든 것에 예외가 있지만 학습 할 때 절대 규칙이 더 나은 경우가 있습니다.)라는 기본 클래스를 작성할 수 없습니다 ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. 필요한 기능은 모두 자체 클래스에 있어야하며 해당 기능이 필요한 다른 클래스는 클래스에서 상속 되지 않고 해당 클래스에 따라 달라질 수 있습니다 .

지나치게 단순화 할 위험이있는 경우, 여러 개의 클래스를 구성하여 함께 사용할 수 있습니다. 그리고 일단 습관을 형성하면 상속을 사용하는 것보다 훨씬 유연하고 유지 보수가 가능하며 테스트가 가능하다는 것을 알게됩니다.


클래스가 여러 기본 클래스를 사용할 수 없다는 것은 상속에 대한 성찰이 아니라 특정 언어의 기능 부족에 대한 성찰이 아닙니다.
iPherian

이 답변을 작성한 이후, 나는 능력이 부족한 "Uncle Bob" 에서이 게시물 을 읽었습니다 . 다중 상속을 허용하는 언어를 사용한 적이 없습니다. 그러나 되돌아 보면 질문에 "language agnostic"이라는 태그가 붙어 있으며 C #이라고 가정합니다. 내 시야를 넓혀야합니다.
Scott Hannen

6

고려해야 할 사항 외에도 고려해야 할 사항은 객체가 거쳐야하는 상속의 "깊이"도 고려해야한다는 것입니다. 상속 수준이 5-6 단계를 초과하면 예상치 못한 캐스팅 및 박싱 / 언 박싱 문제가 발생할 수 있으며,이 경우 대신 객체를 작성하는 것이 좋습니다.


6

두 클래스 사이에 is-a 관계 가 있으면 (예 : 개는 개), 상속을받습니다.

반면에 당신이 가지고있는 경우 가-A 또는 일부 두 클래스 사이의 형용사 관계 (학생이 교육 과정을 가지고) 또는 (교사 연구 과정), 당신은 구성을 선택했다.


당신은 상속과 상속이라고 말했습니다. 상속과 구성을 의미하지 않습니까?
trevorKirkby

아뇨 개 인터페이스를 정의하고 모든 개가 그것을 구현하게하면 더 많은 SOLID 코드가 생길 수 있습니다.
markus

5

이것을 이해하는 간단한 방법은 클래스의 객체가 부모 클래스 와 동일한 인터페이스 를 갖기 위해 상속을 사용해야 부모 클래스의 객체로 취급 될 수 있다는 것입니다 (업 캐스팅) . 또한 파생 클래스 객체의 함수 호출은 코드의 모든 곳에서 동일하게 유지되지만 호출 할 특정 메서드는 런타임에 결정됩니다 (즉, 저수준 구현이 다르면 고수준 인터페이스 는 동일하게 유지됨).

새로운 클래스가 동일한 인터페이스를 가질 필요가없는 경우 구성을 사용해야합니다. 즉, 해당 클래스의 사용자가 알 필요가없는 클래스 구현의 특정 측면을 숨기려고합니다. 따라서 컴포지션은 캡슐화 (즉, 구현 은폐)를 지원하는 방법이 더 많으며 상속은 추상화 를 지원하기위한 것입니다 (즉, 내부가 다른 유형의 범위에 대해 동일한 인터페이스를 제공합니다).


인터페이스에 대한 언급은 +1입니다. 이 방법을 자주 사용하여 기존 클래스를 숨기고 컴포지션에 사용되는 객체를 조롱하여 새 클래스를 올바르게 테스트 할 수 있습니다. 이를 위해서는 새로운 객체의 소유자가 후보 부모 클래스를 대신 전달해야합니다.
Kell


4

그가 @Pavel에 동의하면 작곡 장소와 상속 장소가 있다고 말합니다.

귀하의 답변이 위의 질문 중 하나라도 긍정적이라면 상속을 사용해야한다고 생각합니다.

  • 당신의 클래스는 다형성으로부터 이익을 얻는 구조의 일부입니까? 예를 들어 draw ()라는 메서드를 선언하는 Shape 클래스가있는 경우 Circle 및 Square 클래스가 Shape의 하위 클래스가되어야 클라이언트 클래스가 특정 하위 클래스가 아닌 Shape에 종속 될 수 있습니다.
  • 수업에서 다른 수업에 정의 된 높은 수준의 상호 작용을 재사용해야합니까? 템플릿 메소드 디자인 패턴은 상속없이 구현하기가 불가능하다. 모든 확장 가능한 프레임 워크 가이 패턴을 사용한다고 생각합니다.

그러나 의도가 코드 재사용의 의도라면 컴포지션이 더 나은 디자인 선택입니다.


4

상속은 코드 재사용을위한 매우 강력한 메커니즘입니다. 그러나 올바르게 사용되어야합니다. 하위 클래스가 부모 클래스의 하위 유형 인 경우 상속이 올바르게 사용된다고 말하고 싶습니다. 위에서 언급했듯이 Liskov 대체 원칙이 핵심입니다.

하위 클래스는 하위 유형과 다릅니다. 서브 타입이 아닌 서브 클래스를 작성할 수 있습니다 (그리고 컴포지션을 사용해야하는 경우). 하위 유형이 무엇인지 이해하려면 유형이 무엇인지 설명하십시오.

숫자 5가 정수 유형이라고 말하면 5는 가능한 값 세트에 속한다고합니다 (예를 들어, Java 기본 유형의 가능한 값 참조). 또한 덧셈과 뺄셈과 같은 가치에 대해 수행 할 수있는 유효한 방법이 있다고 말하고 있습니다. 마지막으로 우리는 항상 만족하는 속성 세트가 있다고 말하고 있습니다. 예를 들어 값 3과 5를 추가하면 결과적으로 8이됩니다.

다른 예제를 제공하기 위해 추상 데이터 유형, 정수 세트 및 정수 목록에 대해 생각해보십시오. 보유 할 수있는 값은 정수로 제한됩니다. 둘 다 add (newValue) 및 size ()와 같은 일련의 메소드를 지원합니다. 그리고 둘 다 다른 속성을 가지고 있습니다 (클래스 불변) .Sets는 중복을 허용하지 않지만 List는 중복을 허용합니다 (물론 둘 다 만족하는 다른 속성이 있습니다).

하위 유형은 또한 상위 유형 (또는 상위 유형)이라고하는 다른 유형과 관계가있는 유형입니다. 하위 유형은 상위 유형의 기능 (값, 메소드 및 특성)을 충족해야합니다. 관계는 상위 유형이 예상되는 모든 컨텍스트에서 실행 동작에 영향을주지 않고 하위 유형으로 대체 할 수 있음을 의미합니다. 내가 말하는 것을 예시하는 코드를 보도록하겠습니다. 내가 어떤 종류의 의사 언어로 정수 목록을 작성한다고 가정 해보십시오.

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

그런 다음 정수 목록을 정수 목록의 하위 클래스로 작성합니다.

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

정수 세트 클래스는 List of Integers의 서브 클래스이지만 List 클래스의 모든 기능을 만족하지 않기 때문에 서브 타입이 아닙니다. 메소드의 값 및 서명은 만족되지만 특성은 만족되지 않습니다. add (Integer) 메소드의 동작은 상위 유형의 특성을 유지하지 않고 명확하게 변경되었습니다. 수업의 클라이언트의 관점에서 생각하십시오. 정수 목록이 필요한 정수 세트를 수신 할 수 있습니다. 클라이언트는 값이 목록에 이미 존재하더라도 값을 추가하고 해당 값을 목록에 추가 할 수 있습니다. 그러나 그녀는 그 가치가 존재한다면 그 행동을 얻지 못할 것입니다. 그녀를위한 큰 놀라움!

이것은 상속의 부적절한 사용에 대한 전형적인 예입니다. 이 경우 컴포지션을 사용하십시오.

(조각 : 상속을 올바르게 사용하십시오 ).


3

내가 들었던 경험은 상속은 "is-a"관계 일 때 사용되어야하고 "has-a"일 때 구성을 사용해야한다는 것입니다. 그럼에도 불구하고 나는 많은 복잡성을 제거하기 때문에 항상 구성에 의존해야한다고 생각합니다.


2

구성 대 상속은 광범위한 주제입니다. 모든 것이 시스템의 디자인에 달려 있다고 생각하기 때문에 더 나은 것에 대한 진정한 대답은 없습니다.

일반적으로 객체 간의 관계 유형은 더 나은 정보를 제공하여 객체 중 하나를 선택할 수 있습니다.

관계 유형이 "IS-A"관계이면 상속이 더 나은 방법입니다. 그렇지 않으면 관계 유형이 "HAS-A"관계이면 컴포지션이 더 잘 접근합니다.

그것은 엔티티 관계에 전적으로 달려 있습니다.


2

작곡이 선호 되기는하지만 상속의 장점 과 작곡의 단점 을 강조하고 싶습니다 .

상속의 장점 :

  1. 논리적 " IS A" 관계를 설정합니다. 경우 자동차트럭 의 두 종류가 차량 (베이스 클래스), 하위 클래스 A는 기본 클래스.

    자동차는 차량입니다

    트럭은 차량입니다

  2. 상속을 통해 기능을 정의 / 수정 / 확장 할 수 있습니다

    1. 기본 클래스는 구현을 제공하지 않으며 하위 클래스는 완전한 메소드를 대체해야합니다 (추상) => 계약을 구현할 수 있습니다
    2. 기본 클래스는 기본 구현을 제공하고 하위 클래스는 동작을 변경할 수 있습니다 => 계약을 다시 정의 할 수 있습니다
    3. 하위 클래스는 super.methodName ()을 첫 번째 명령문으로 호출하여 기본 클래스 구현에 확장을 추가합니다. => 계약을 연장 할 수 있습니다
    4. 기본 클래스는 알고리즘의 구조를 정의하고 하위 클래스는 알고리즘의 일부를 대체합니다. => 기본 클래스 스켈레톤을 변경하지 않고 Template_method구현할 수 있습니다.

구성의 단점 :

  1. 상속에서 하위 클래스는 IS A 관계로 인해 기본 클래스 메서드를 구현하지 않더라도 기본 클래스 메서드를 직접 호출 할 수 있습니다 . 컴포지션을 사용하는 경우 컨테이너 클래스에 메소드를 추가하여 포함 된 클래스 API를 노출해야합니다.

예를 들어, 만약 자동차가 포함 차량을 하고 가격 얻을 수있는 경우 자동차 에 정의 된, 차량을 , 코드는 다음과 같이 될 것입니다

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

나는 그것이 질문에 대답하지 않는다고 생각한다
almanegra

OP 질문을 다시 볼 수 있습니다. 나는 다음과 같이 언급했다 : 각 접근법마다 어떤 장단점이 있습니까?
Ravindra babu

언급했듯이, "구성의 장점과 단점"에 대해서만 이야기하지만 EACH 접근 방식이나 서로를 사용해야하는 경우에 대한 상충 관계는 아닙니다.
almanegra

상속의 장점은 구성의 단점이고 구성의 단점은 상속의 장점이므로 장단점은 장단점을 제공합니다.
Ravindra babu

1

많은 사람들이 말했듯이 먼저 "is-a"관계가 있는지 확인하는 것으로 시작하겠습니다. 그것이 있다면 나는 보통 다음을 확인합니다.

기본 클래스를 인스턴스화 할 수 있는지 여부 즉, 기본 클래스가 비 추상적 일 수 있는지 여부입니다. 그것이 비 추상적 일 수 있다면 나는 보통 구성을 선호한다

예 1. 회계사 직원입니다. 그러나 Employee 객체를 인스턴스화 할 수 있기 때문에 상속을 사용 하지 않습니다 .

예를 들어 2. Book SellingItem입니다. SellingItem은 인스턴스화 할 수 없으며 추상적 개념입니다. 따라서 나는 상속을 사용합니다. SellingItem은 추상 기본 클래스 (또는 C #의 인터페이스 )입니다.

이 접근법에 대해 어떻게 생각하십니까?

또한 왜 상속을 사용합니까?에서 @anon 답변을 지원 합니다.

상속을 사용하는 주된 이유는 구성의 형태가 아니기 때문에 다형성 동작을 얻을 수 있습니다. 다형성이 필요하지 않으면 상속을 사용하지 않아야합니다.

@MatthieuM. /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448에 말합니다.

상속 문제는 두 가지 직교 목적으로 사용할 수 있다는 것입니다.

인터페이스 (다형성)

구현 (코드 재사용)

참고

  1. 어느 클래스 디자인이 더 낫습니까?
  2. 상속 vs. 집계

1
왜 '기본 클래스가 추상적입니까?' LSP : Poodle 객체가 전달되면 Dogs에서 작동하는 모든 기능이 작동합니까? 그렇다면 Poodle을 Dog로 대체하여 Dog에서 상속받을 수 있습니다.
Gishu

@ 기슈 감사합니다. 확실히 LSP를 살펴볼 것입니다. 그러나 그 전에 "기본 클래스가 추상적 일 수없는 상속이 적절한 예"를 제공 할 수 있습니까? 필자는 기본 클래스가 추상적 인 경우에만 상속을 적용 할 수 있다고 생각합니다. 기본 클래스를 별도로 인스턴스화해야하는 경우 상속하지 마십시오. 즉, 회계사가 직원이지만 상속을 사용하지 마십시오.
LCJ

1
최근 WCF를 읽고있었습니다. .net 프레임 워크의 예는 대기열이 ThreadPool 스레드에서 작동하는 SynchronizationContext (base +를 인스턴스화 할 수 있음)입니다. WinFormsSyncContext (UI 스레드 대기열) 및 DispatcherSyncContext (WPF 디스패처 대기열)
Gishu

@ 기슈 감사합니다. 그러나 은행 도메인, HR 도메인, 소매 도메인 또는 기타 인기있는 도메인을 기반으로 시나리오를 제공 할 수 있으면 더 도움이됩니다.
LCJ

1
죄송합니다. 그 도메인에 익숙하지 않습니다. 이전 도메인이 너무 애매한 경우의 또 다른 예는 Winforms / WPF의 Control 클래스입니다. 기본 / 일반 제어를 인스턴스화 할 수 있습니다. 파생에는 목록 상자, 텍스트 상자 등이 포함됩니다. 이제 생각할 때 데코레이터 디자인 패턴은 IMHO의 좋은 예이며 유용합니다. 데코레이터는 랩 / 장식하려는 비추 상 오브젝트에서 파생됩니다.
Gishu

1

상속과 관련하여 발생할 수 있는 다이아몬드 문제에 대해서는 아무도 언급하지 않았습니다 .

B와 C 클래스가 A를 상속하고 메소드 X를 대체하고 네 번째 클래스 D를 B와 C에서 모두 상속하고 X를 대체하지 않으면 XD의 어떤 구현을 사용해야합니까?

Wikipedia 는이 질문에서 다루고있는 주제에 대한 훌륭한 개요를 제공합니다.


1
D는 A가 아닌 B와 C를 상속합니다. 그렇다면 A 클래스에있는 X의 구현을 사용합니다.
fabricio

1
@ fabricio : 감사합니다. 텍스트를 편집했습니다. 그건 그렇고, 다중 클래스 상속을 허용하지 않는 언어에서는 이러한 시나리오가 발생할 수 없습니다.
Veverke

그렇습니다. 옳습니다 .. (다양한 문제의 예에 따라) 다중 상속을 허용하는 것으로 작업 한 적이 없습니다.
fabricio
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.