상속보다 구성을 선호하는 이유는 무엇입니까? 각 접근법마다 어떤 절충점이 있습니까? 구성보다 상속을 언제 선택해야합니까?
상속보다 구성을 선호하는 이유는 무엇입니까? 각 접근법마다 어떤 절충점이 있습니까? 구성보다 상속을 언제 선택해야합니까?
답변:
상속 가능성이 높고 나중에 수정하기가 쉽지만 작성 항상 접근 방식을 사용하지 않으므로 상속보다 구성을 선호하십시오. 컴포지션을 사용하면 Dependency Injection / Setter를 사용하여 동작을 쉽게 변경할 수 있습니다. 대부분의 언어에서는 여러 유형에서 파생 할 수 없으므로 상속이 더 엄격합니다. 따라서 TypeA에서 파생되면 거위는 다소 요리됩니다.
위의 내 산 테스트는 다음과 같습니다.
TypeB가 TypeA가 예상되는 곳에서 TypeB를 사용할 수 있도록 TypeA의 완전한 인터페이스 (모든 공용 메소드)를 공개하려고합니까? 상속을 나타냅니다 .
TypeB는 TypeA에 의해 노출되는 동작의 일부 / 일부만 원합니까? 구성이 필요함을 나타냅니다 .
업데이트 : 방금 내 대답으로 돌아 왔으며 '이 유형에서 상속해야합니까?'에 대한 테스트로 Barbara Liskov의 Liskov 대체 원칙에 대한 언급이 없으면 불완전한 것으로 보입니다.
격리 는 관계 가 있다고 생각하십시오 . 자동차에는 "엔진", 사람에는 "이름"등이 있습니다.
상속 은 관계 라고 생각하십시오 . 자동차는 "차량", 사람은 "동물"등
나는이 접근법에 대해 아무런 신용도받지 않는다. 나는에서 똑바로했다 전체 코드의 두 번째 판 에 의해 스티브 맥코넬 , 6.3 절 .
차이점을 이해하면 설명하기가 더 쉽습니다.
예를 들어 클래스를 사용하지 않는 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 개체는 직원과 사람으로 구성됩니다. 직함 행동은 직원으로부터 가져옵니다. 이 명시적인 구성은 무엇보다도 애매 모호함을 없애고 버그가 줄어 듭니다.
상속을 통해 부인할 수없는 모든 혜택을 누리면 다음과 같은 단점이 있습니다.
상속의 단점 :
반면에 객체 구성 은 런타임에 다른 객체에 대한 참조를 얻는 객체를 통해 정의됩니다. 이러한 경우 이러한 개체는 서로의 보호 된 데이터에 도달 할 수 없으며 (캡슐화 중단 없음) 서로의 인터페이스를 강제해야합니다. 이 경우에도 구현 종속성은 상속의 경우보다 훨씬 적습니다.
간단히 말해서 나는 "상속보다 구성을 선호한다"에 동의하지만, 종종 "코카콜라보다 감자를 선호한다"고 들린다. 상속 장소와 작곡 장소가 있습니다. 차이점을 이해해야합니다. 그러면이 질문은 사라집니다. 그것이 저에게 실제로 의미하는 것은 "상속을 사용하려는 경우 다시 생각하면 기회가 필요하다는 것"입니다.
먹고 싶을 때는 코카콜라보다 감자를, 마시고 싶을 때는 감자보다 코카콜라를 선호해야합니다.
서브 클래스를 생성한다는 것은 수퍼 클래스 메소드를 호출하는 편리한 방법 이상을 의미해야합니다. 서브 클래스 "is-a"수퍼 클래스가 구조적으로나 기능적으로 모두 수퍼 클래스로 사용될 수 있고 상속 할 때는 상속을 사용해야합니다. 그렇지 않은 경우-상속이 아니라 다른 것입니다. 구성은 개체가 다른 개체로 구성되거나 관계가있는 경우입니다.
그래서 누군가가 상속이나 구성이 필요한지 알지 못하는 경우 실제 문제는 그가 마시거나 먹고 싶어하는지 모른다는 것입니다. 문제 영역에 대해 더 많이 생각하고 더 잘 이해하십시오.
InternalCombustionEngine
파생 클래스가 있는 기본 클래스 를 고려하십시오 GasolineEngine
. 후자는 스파크 플러그와 같은 것을 추가하지만 기본 클래스에는 없지만 InternalCombustionEngine
스파크 플러그가 사용됩니다.
상속은 특히 절차 적 국가에서 오는 매우 유혹적이며 종종 기만적으로 우아해 보입니다. 내가해야 할 일은이 클래스의 기능을 다른 클래스에 추가하는 것입니다. 글쎄, 문제 중 하나는
기본 클래스는 구현 된 세부 정보를 보호 된 멤버 형식으로 하위 클래스에 노출시켜 캡슐화를 중단합니다. 이것은 시스템을 단단하고 취약하게 만듭니다. 그러나 더 비극적 인 결함은 새로운 하위 클래스가 상속 체인의 모든 수하물과 의견을 가져 오는 것입니다.
Inheritance is Evil : DataAnnotationsModelBinder의 Epic Fail 기사 는 C #에서 이에 대한 예제를 보여줍니다. 컴포지션을 사용해야 할 때 상속을 사용하고 리팩토링 할 수있는 방법을 보여줍니다.
Java 또는 C #에서 인스턴스화되면 객체의 유형을 변경할 수 없습니다.
따라서 개체가 다른 개체로 나타나거나 개체 상태 나 조건에 따라 다르게 동작해야하는 경우 구성 : 상태 및 전략 디자인 패턴 참조를 사용 하십시오 .
객체의 유형이 동일해야하는 경우 상속 을 사용 하거나 인터페이스를 구현하십시오.
Client
. 그런 다음 PreferredClient
나중에 새로운 개념의 팝업이 나타납니다. PreferredClient
상속 해야합니까 Client
? 우선 고객은 '고객'입니까? 글쎄, 그렇게 빠르지는 않습니다 ... 당신이 말한 것처럼 객체는 런타임에 클래스를 변경할 수 없습니다. 작업을 어떻게 모델링 client.makePreferred()
하시겠습니까? 아마도 답은 개념이없는 컴포지션을 사용하는 Account
것입니다.
Client
클래스를, 아마도 단지의 개념을 캡슐화 한 거기 Account
수 StandardAccount
또는를 PreferredAccount
...
여기서 만족스러운 답변을 찾지 못하여 새로운 답변을 썼습니다.
" 상속보다 구성을 선호하는 "이유를 이해하려면 먼저이 단축 된 관용구에서 생략 된 가정을 되 찾아야합니다.
상속의 두 가지 이점이 있습니다 : 서브 타이핑 및 서브 클래 싱
서브 타이핑 은 유형 (인터페이스) 서명, 즉 API 세트를 따르는 것을 의미하며 서브 타이핑 다형성을 달성하기 위해 서명의 일부를 무시할 수 있습니다.
서브 클래 싱은 메소드 구현의 암묵적인 재사용을 의미합니다.
상속을 수행하는 두 가지 목적, 즉 서브 타이핑 지향과 코드 재사용 지향의 두 가지 이점이 있습니다.
코드 재사용이 유일한 목적 이라면 , 서브 클래 싱은 필요한 것 이상을 제공 할 수 있습니다. 즉, 부모 클래스의 일부 공용 메소드는 자식 클래스에 대해 의미가 없습니다. 이 경우, 대신에 상속 위에 조성물을 선호에, 조성은 요구 . 이것은 "is-a"대 "has-a"개념의 유래이기도합니다.
따라서 서브 타이핑을 목적으로하는 경우에만, 즉 새로운 클래스를 나중에 다형성 방식으로 사용하려는 경우에만 상속 또는 구성을 선택하는 문제에 직면하게됩니다. 이것은 논의중인 단축 관용구에서 생략되는 가정입니다.
하위 유형은 유형 서명을 준수하기 위해 작성시 항상 유형의 API를 적은 양으로 노출해야합니다. 이제 트레이드 오프가 시작됩니다.
상속은 재정의되지 않은 경우 간단한 코드 재사용을 제공하는 반면 컴포지션은 단순한 위임 작업 일지라도 모든 API를 다시 코딩해야합니다.
상속은 내부 다형성 사이트를 통해 직접적인 공개 재귀 를 제공합니다 this
. 즉 공개 또는 비공개 ( 권장 하지는 않지만 ) 다른 멤버 함수에서 재정의 방법 (또는 유형 )을 호출 합니다. 오픈 재귀는 composition을 통해 시뮬레이션 할 수 있지만 추가 노력이 필요하며 항상 실행 가능한 것은 아닙니다 (?). 중복 질문에 대한 이 답변 은 비슷한 것을 말합니다.
상속은 보호 된 구성원을 노출시킵니다 . 이로 인해 부모 클래스의 캡슐화가 중단되고 하위 클래스에서 사용하는 경우 자식과 부모 간의 다른 종속성이 도입됩니다.
컴포지션은 반전 제어에 적합하며 데코레이터 패턴 및 프록시 패턴 과 같이 종속성을 동적으로 주입 할 수 있습니다 .
컴포지션 은 인터페이스에 대한 프로그래밍을 즉시 따릅니다 .
위의 장단점을 염두에두고 상속보다 구성을 선호합니다 . 그러나 밀접하게 관련된 클래스, 즉 암시 적 코드 재사용이 실제로 이익을 얻거나 개방형 재귀의 마법의 힘이 필요한 경우 상속이 선택됩니다.
개인적으로 저는 항상 상속보다 구성을 선호하는 법을 배웠습니다. 컴포지션으로는 해결할 수없는 상속으로 해결할 수있는 프로그래밍 문제는 없습니다. 경우에 따라 Interfaces (Java) 또는 Protocols (Obj-C)를 사용해야 할 수도 있습니다. C ++은 그러한 것을 알지 못하므로 추상 기본 클래스를 사용해야하므로 C ++에서 상속을 완전히 제거 할 수 없습니다.
컴포지션은 더 논리적이고, 더 나은 추상화, 더 나은 캡슐화, 더 나은 코드 재사용 (특히 매우 큰 프로젝트에서)을 제공하며 코드의 어느 곳에서나 고립 된 변경을 수행하여 멀리서도 문제를 일으킬 가능성이 적습니다. 또한 " 단일 책임 원칙 "을 보다 쉽게지지 할 수 있습니다. " 단일 책임 원칙 "은 종종 " 계급이 바뀌어야 할 이유가 두 개 이상 있어야합니다. "로 요약됩니다 . 이는 모든 클래스가 특정 목적을 위해 존재하며 목적과 직접 관련된 방법 만 있습니다. 또한 매우 얕은 상속 트리를 사용하면 프로젝트가 실제로 커지기 시작할 때에도 개요를 훨씬 쉽게 유지할 수 있습니다. 많은 사람들은 상속이 우리의 실제 세계를 대표한다고 생각합니다꽤 잘하지만 그것은 사실이 아닙니다. 실제 세계는 상속보다 훨씬 더 많은 구성을 사용합니다. 당신이 손에 쥐고있을 수있는 거의 모든 실제 물체는 다른 작은 실제 물체로 구성되어 있습니다.
그러나 구성에는 단점이 있습니다. 상속을 완전히 건너 뛰고 컴포지션에만 중점을두면 상속을 사용하는 경우 필요하지 않은 몇 가지 추가 코드 줄을 작성해야하는 경우가 종종 있습니다. 당신은 또한 때때로 자신을 반복하도록 강요받으며 이것은 건조 원칙을 위반합니다(건조 = 자신을 반복하지 마십시오). 또한 작성에는 종종 위임이 필요하며, 메소드는이 호출을 둘러싼 다른 코드없이 다른 오브젝트의 다른 메소드를 호출합니다. 이러한 "이중 메서드 호출"(트리플 또는 쿼드 러플 메서드 호출로 쉽게 확장 될 수 있으며 그보다 훨씬 큼)은 상속보다 성능이 훨씬 떨어 지므로 부모의 메서드를 단순히 상속합니다. 상속 된 메소드를 호출하는 것은 상속되지 않은 메소드를 호출하는 것과 똑같이 빠르거나 약간 느릴 수 있지만 일반적으로 두 번의 연속 메소드 호출보다 여전히 빠릅니다.
대부분의 OO 언어는 다중 상속을 허용하지 않습니다. 다중 상속이 실제로 무언가를 구입할 수있는 몇 가지 경우가 있지만 규칙보다는 예외입니다. "여러 상속이이 문제를 해결하는 정말 멋진 기능"이라고 생각하는 상황에 처할 때마다 대개 몇 가지 추가 코드 줄이 필요할 수 있으므로 상속을 다시 생각해야하는 시점에 있습니다. 구성에 기반한 솔루션은 일반적으로 훨씬 더 우아하고 유연하며 미래의 증거로 판명됩니다.
상속은 정말 멋진 기능이지만 지난 몇 년 동안 과도하게 사용 된 것 같습니다. 사람들은 상속이 실제로 못, 나사 또는 완전히 다른 무엇이든 관계없이 모든 것을 못 박는 망치로 취급했습니다.
TextFile
입니다 File
.
나의 일반적인 경험 법칙 : 상속을 사용하기 전에 구성이 더 합리적인지 고려하십시오.
이유 : 서브 클래 싱은 일반적으로 더 복잡하고 연결성을 의미합니다. 즉 실수없이 변경, 유지 관리 및 확장하기가 어렵습니다.
Sun의 Tim Boudreau 가 제공 하는 훨씬 더 완전하고 구체적인 답변 :
내가 볼 때 상속 사용에 대한 일반적인 문제는 다음과 같습니다.
- 무고한 행위는 예기치 않은 결과를 초래할 수 있습니다. 이것의 전형적인 예는 서브 클래스 인스턴스 필드가 초기화되기 전에 수퍼 클래스 생성자에서 재정의 가능한 메소드를 호출하는 것입니다. 완벽한 세상에서는 아무도 그렇게하지 않을 것입니다. 이것은 완벽한 세상이 아닙니다.
- 서브 클래스가 메소드 호출 순서에 대한 가정을하는 등의 유혹을 제공합니다. 이러한 가정은 수퍼 클래스가 시간이 지남에 따라 진화 할 경우 안정적이지 않은 경향이 있습니다. 내 토스터와 커피 포트 비유 도 참조하십시오 .
- 클래스가 더 무거워집니다. 수퍼 클래스가 생성자에서 어떤 작업을 수행하는지 또는 얼마나 많은 메모리를 사용할지 반드시 알 필요는 없습니다. 따라서 무고한 가벼운 객체를 만드는 것은 생각보다 훨씬 비쌀 수 있으며 수퍼 클래스가 진화하면 시간이 지남에 따라 변경 될 수 있습니다
- 그것은 서브 클래스의 폭발을 장려합니다 . 클래스 로딩에는 시간이 걸리고 더 많은 클래스에는 메모리가 필요합니다. NetBeans 규모의 앱을 처리 할 때까지 이것은 문제가되지 않을 수 있지만 메뉴의 첫 표시가 대규모 클래스로드를 트리거했기 때문에 메뉴가 느려지는 등 실제 문제가있었습니다. 우리는보다 선언적인 구문과 다른 기술로 이동하여이 문제를 해결했지만 수정하는 데 시간이 걸렸습니다.
- 그것은 나중에 변경 일을 어렵게 만든다 - 당신이 코드 공개 한 후, 당신이 결혼하고, 선택 - 당신은 슈퍼 클래스는 서브 클래스를 깰 것입니다 스와핑, 클래스 공개 한 경우. 따라서 실제 기능을 수퍼 클래스로 변경하지 않으면 나중에 필요한 것을 확장하지 않고 나중에 사용할 경우 훨씬 더 자유롭게 변경할 수 있습니다. 예를 들어 JPanel을 서브 클래 싱하는 경우를 생각해보십시오. 일반적으로 잘못되었습니다. 서브 클래스가 어딘가에 공개되어 있다면 그 결정을 다시 방문 할 기회가 없습니다. JComponent getThePanel ()으로 액세스하면 여전히 할 수 있습니다 (힌트 : API로 구성 요소의 모델을 노출하십시오).
- 개체 계층 구조는 크기가 조정되지 않습니다 (또는 나중에 크기를 조정하는 것이 미리 계획하는 것보다 훨씬 어렵습니다) . 이것은 고전적인 "너무 많은 레이어"문제입니다. 아래에서이 문제에 대해 살펴보고 AskTheOracle 패턴이이를 해결하는 방법 (OOP 순수 주의자들을 불쾌하게 할 수 있음).
...
상속을 허용하면 소금 한 알을 가지고 할 수있는 일에 대한 나의 테이크는 다음과 같습니다.
- 상수를 제외하고 필드를 노출하지 마십시오.
- 방법은 추상적이거나 최종적이어야한다
- 수퍼 클래스 생성자로부터 메소드를 호출하지 않습니다
...
이 모든 것은 큰 프로젝트보다 작은 프로젝트에 덜 적용되고 공개 프로젝트보다 작은 프로젝트에 덜 적용됩니다
다른 답변을 참조하십시오.
다음 문장이 참일 때 클래스 Bar
가 클래스 를 상속받을 수 있다고 종종 말합니다 Foo
.
- 바는 foo입니다
불행히도 위의 테스트만으로는 신뢰할 수 없습니다. 대신 다음을 사용하십시오.
- 바는 foo, AND
- 바는 foos가 할 수있는 모든 것을 할 수 있습니다.
첫 번째 테스트는 모든 getter 가 Foo
의미를 갖도록하고 Bar
(= 공유 속성) 두 번째 테스트는 모든 setter 가 Foo
의미를 갖도록합니다 Bar
(= 공유 기능).
예 1 : 개-> 동물
개는 동물이며 개는 동물이 할 수있는 모든 일을 할 수 있습니다 (호흡, 사망 등). 따라서 클래스 Dog
는 클래스 를 상속 할 수 있습니다Animal
.
예 2 : 원-/-> 타원
원은 타원이지만 원은 타원으로 할 수있는 모든 것을 할 수는 없습니다. 예를 들어, 타원은 뻗을 수 있지만 원은 늘릴 수 없습니다. 따라서 클래스 Circle
는 클래스 를 상속 할 수 없습니다Ellipse
.
이것을 원형 타원 문제 라고합니다 . 이것은 실제로 문제 가 아니며, 첫 번째 테스트만으로는 상속이 가능하다는 결론을 내리기에 충분하지 않다는 명백한 증거입니다. 특히이 예제는 파생 클래스가 기본 클래스의 기능을 확장 해야 하며 절대로 제한 하지 않아야 함을 강조 합니다. 그렇지 않으면 기본 클래스를 다형성으로 사용할 수 없습니다.
상속 을 사용할 수 있다고 해서 반드시 컴포지션 을 사용해야 하는 것은 아닙니다. 상속은 암시 적 코드 재사용 및 동적 디스패치를 허용하는 강력한 도구이지만 몇 가지 단점이 있으므로 구성이 종종 선호됩니다. 상속과 구성 간의 상충 관계는 분명하지 않으며 lcn의 답변 에 가장 잘 설명되어 있습니다.
일반적으로 다형성 사용이 매우 일반적 일 것으로 예상되는 경우 컴포지션보다 상속을 선택하는 경향이 있습니다.이 경우 동적 디스패치의 힘이 훨씬 더 읽기 쉽고 우아한 API로 이어질 수 있습니다. 예를 들어, Widget
GUI 프레임 워크에 다형성 클래스 또는 Node
XML 라이브러리에 다형성 클래스 를 사용하면 구성에 기반한 솔루션을 사용하는 것보다 훨씬 읽기 쉽고 직관적 인 API를 사용할 수 있습니다.
아시다시피 상속 가능 여부를 결정하는 데 사용되는 또 다른 방법을 Liskov 대체 원칙 이라고합니다 .
기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 몰래 파생 클래스의 객체를 사용할 수 있어야합니다.
기본적으로 이것은 기본 클래스를 다형성으로 사용할 수있는 경우 상속이 가능하다는 것을 의미합니다. 이는 "바는 foo이고 바는 foo가 할 수있는 모든 것을 수행 할 수 있습니다"라고 생각합니다.
computeArea(Circle* c) { return pi * square(c->radius()); }
입니다. 함수를 상상하십시오 . 타원을 통과하면 분명히 손상됩니다 (반지름 ()은 무엇을 의미합니까?). 타원은 원이 아니므로 원에서 파생되지 않아야합니다.
computeArea(Circle *c) { return pi * width * height / 4.0; }
이제는 일반적입니다.
width()
하고 있음을 의미한다는 것을 알고 있습니다 height()
. 라이브러리 사용자가 "EggShape"라는 다른 클래스를 작성하기로 결정하면 어떻게됩니까? "Circle"에서 파생되어야합니까? 당연히 아니지. 계란 모양은 원이 아니며 타원도 원이 아니므로 LSP를 끊기 때문에 Circle에서 파생되는 것은 없습니다. Circle * 클래스에서 작업을 수행하는 메서드는 원이 무엇인지에 대한 강력한 가정을하고 이러한 가정을 어기는 것은 거의 확실히 버그로 이어질 것입니다.
상속은 매우 강력하지만 강제로 할 수는 없습니다 ( 원-타원 문제 참조 ). 진정한 "is-a"하위 유형 관계를 완전히 확신 할 수 없으면 컴포지션을 사용하는 것이 가장 좋습니다.
상속은 서브 클래스와 수퍼 클래스 사이에 강한 관계를 만듭니다. 서브 클래스는 수퍼 클래스의 구현 세부 사항을 알고 있어야합니다. 어떻게 수퍼 클래스를 만들 수 있는지 생각해야 할 때 슈퍼 클래스를 만드는 것이 훨씬 어렵습니다. 클래스 불변량을주의 깊게 문서화하고 재정의 가능한 다른 메소드가 내부적으로 사용하는 것을 명시해야합니다.
계층 구조가 실제로 관계를 나타내는 경우 상속이 유용 할 때가 있습니다. 이는 공개 폐쇄 원칙과 관련이 있으며, 수정을 위해 클래스를 닫아야하지만 확장 가능합니다. 그렇게하면 다형성을 가질 수 있습니다. 수퍼 유형과 해당 메소드를 처리하는 일반 메소드를 갖지만 동적 디스패치를 통해 서브 클래스의 메소드가 호출됩니다. 이는 유연성이 뛰어나고 소프트웨어에 필수적인 간접 구현을 구현하는 데 도움이됩니다 (구현 세부 정보에 대해서는 덜 알고 있음).
그러나 상속은 쉽게 과도하게 사용되며 클래스 사이의 어려운 종속성으로 인해 추가 복잡성이 발생합니다. 또한 프로그램 실행 중 발생하는 상황을 이해하는 것은 계층과 메소드 호출의 동적 선택으로 인해 매우 어려워집니다.
작성을 기본값으로 사용하는 것이 좋습니다. 더 모듈화되어 있으며 후기 바인딩의 이점을 제공합니다 (구성 요소를 동적으로 변경할 수 있음). 또한 개별적으로 테스트하는 것이 더 쉽습니다. 그리고 클래스의 메소드를 사용해야하는 경우 특정 형식 (Liskov 대체 원칙)이 아니어야합니다.
Inheritance is sometimes useful... That way you can have polymorphism
은 상속과 다형성의 개념을 밀접하게 연결시키는 것으로 해석 했습니다 (문맥이 주어진 것으로 가정). 내 의견은 당신이 당신의 의견에서 분명히 밝힌 것을 지적하기위한 것입니다 : 상속은 다형성을 구현하는 유일한 방법이 아니며 실제로 구성과 상속을 결정할 때 결정적인 요소는 아닙니다.
항공기에 엔진과 날개의 두 부분 만 있다고 가정합니다.
그런 다음 항공기 클래스를 설계하는 두 가지 방법이 있습니다.
Class Aircraft extends Engine{
var wings;
}
이제 항공기는 고정 날개로 시작할 수 있습니다
하여 회전 날개로 즉시 변경할 수 있습니다. 본질적
으로 날개가 달린 엔진입니다. 그러나
엔진을 즉시 변경 하려면 어떻게해야 합니까?
기본 클래스 Engine
가
속성 을 변경하기 위해 뮤 테이터를 노출 하거나 Aircraft
다음과 같이 다시 디자인 합니다.
Class Aircraft {
var wings;
var engine;
}
이제 엔진을 즉시 교체 할 수 있습니다.
Bob 삼촌의 SOLID 클래스 설계 원칙 에 있는 Liskov 대체 원리 를 살펴 봐야 합니다. :)
이 두 가지 방법은 함께 잘 살면서 실제로 서로를 지탱할 수 있습니다.
컴포지션은 모듈 식으로 재생됩니다. 부모 클래스와 유사한 인터페이스를 만들고 새 객체를 만들고 호출을 위임합니다. 이러한 객체가 서로를 알 필요가 없다면 구성이 매우 안전하고 사용하기 쉽습니다. 여기에는 많은 가능성이 있습니다.
그러나 어떤 이유로 부모 클래스가 경험이없는 프로그래머를 위해 "자식 클래스"에서 제공하는 함수에 액세스해야하는 경우 상속을 사용하기에 좋은 장소 인 것 같습니다. 부모 클래스는 서브 클래스로 덮어 쓰는 자체 추상 "foo ()"를 호출 한 다음 추상베이스에 값을 제공 할 수 있습니다.
좋은 생각처럼 보이지만 많은 경우에 필요한 클래스 클래스에서 새 클래스를 상속하는 것보다 foo ()를 구현하는 객체를 클래스에 제공하는 것이 좋습니다 (또는 foo ()를 수동으로 제공된 값으로 설정). 지정할 foo () 함수
왜?
상속은 정보를 옮기는 나쁜 방법이기 때문 입니다.
컴포지션의 실제 우위는 다음과 같습니다. "상위 클래스"또는 "추상 작업자"는 특정 인터페이스를 구현하는 특정 "하위"객체를 집계 할 수 있습니다. 유형 입니다. 예를 들어 MergeSort 또는 QuickSort는 추상 비교 인터페이스를 구현하는 모든 객체 목록을 정렬 할 수 있습니다. 또는 "foo ()"를 구현하는 모든 객체 그룹과 "foo ()"를 가진 객체를 사용할 수있는 다른 객체 그룹은 함께 재생할 수 있습니다.
상속을 사용하는 세 가지 실제 이유를 생각할 수 있습니다.
이것이 사실이라면 상속을 사용해야 할 것입니다.
이유 1을 사용하는 것은 나쁘지 않습니다. 객체에 견고한 인터페이스를 갖는 것이 좋습니다. 이 인터페이스가 단순하고 변경되지 않는 경우 구성을 사용하거나 상속없이 문제없이 수행 할 수 있습니다. 일반적으로 상속은 여기서 매우 효과적입니다.
이유가 2 번이면 조금 까다 롭습니다. 실제로 동일한 기본 클래스 만 사용해야합니까? 일반적으로 동일한 기본 클래스를 사용하는 것만으로는 충분하지 않지만 프레임 워크의 요구 사항이 될 수 있으므로 피할 수없는 디자인 고려 사항이 될 수 있습니다.
그러나 개인 변수, 사례 3을 사용하려면 문제가있을 수 있습니다. 전역 변수를 안전하지 않은 것으로 간주하면 상속을 사용하여 개인 변수에 대한 액세스도 안전하지 않은 것으로 고려해야 합니다. 전역 변수가 모두 나쁜 것은 아닙니다. 데이터베이스는 본질적으로 큰 전역 변수 세트입니다. 그러나 당신이 그것을 처리 할 수 있다면, 그것은 좋습니다.
새로운 프로그래머를위한 다른 관점에서이 질문을 해결하려면 :
상속은 종종 객체 지향 프로그래밍을 배울 때 일찍 가르치므로 일반적인 문제에 대한 쉬운 해결책으로 간주됩니다.
공통 기능이 필요한 클래스가 세 개 있습니다. 따라서 기본 클래스를 작성하고 모든 클래스를 상속 받으면 모두 해당 기능을 가지게되므로 한 번만 유지하면됩니다.
그것은 훌륭하게 들리지만 실제로 여러 가지 이유 중 하나 때문에 거의 작동하지 않습니다.
결국, 우리는 코드를 어려운 매듭으로 묶고 "쿨, 나는 상속에 대해 배웠고 이제는 그것을 사용했다"는 말을 제외하고는 아무런 혜택도 얻지 못했습니다. 우리 모두가 해냈 기 때문에 그것은 하찮은 것이 아닙니다. 그러나 아무도 우리에게하지 말라고했기 때문에 우리 모두는 그렇게했습니다.
누군가가 "상속보다 선호하는 구성"을 설명하자마자, 상속을 사용하여 클래스간에 기능을 공유하려고 할 때마다 다시 생각하고 대부분의 시간이 실제로 제대로 작동하지 않는다는 것을 깨달았습니다.
해독제는 단일 책임 원칙 입니다. 그것을 제약 조건으로 생각하십시오. 수업 은 한 가지만 해야합니다 . 나는 수업에 그 일을 묘사하는 이름을 수업에 줄 수 있어야 한다. (모든 것에 예외가 있지만 학습 할 때 절대 규칙이 더 나은 경우가 있습니다.)라는 기본 클래스를 작성할 수 없습니다 ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
. 필요한 기능은 모두 자체 클래스에 있어야하며 해당 기능이 필요한 다른 클래스는 클래스에서 상속 되지 않고 해당 클래스에 따라 달라질 수 있습니다 .
지나치게 단순화 할 위험이있는 경우, 여러 개의 클래스를 구성하여 함께 사용할 수 있습니다. 그리고 일단 습관을 형성하면 상속을 사용하는 것보다 훨씬 유연하고 유지 보수가 가능하며 테스트가 가능하다는 것을 알게됩니다.
두 클래스 사이에 is-a 관계 가 있으면 (예 : 개는 개), 상속을받습니다.
반면에 당신이 가지고있는 경우 가-A 또는 일부 두 클래스 사이의 형용사 관계 (학생이 교육 과정을 가지고) 또는 (교사 연구 과정), 당신은 구성을 선택했다.
이것을 이해하는 간단한 방법은 클래스의 객체가 부모 클래스 와 동일한 인터페이스 를 갖기 위해 상속을 사용해야 부모 클래스의 객체로 취급 될 수 있다는 것입니다 (업 캐스팅) . 또한 파생 클래스 객체의 함수 호출은 코드의 모든 곳에서 동일하게 유지되지만 호출 할 특정 메서드는 런타임에 결정됩니다 (즉, 저수준 구현이 다르면 고수준 인터페이스 는 동일하게 유지됨).
새로운 클래스가 동일한 인터페이스를 가질 필요가없는 경우 구성을 사용해야합니다. 즉, 해당 클래스의 사용자가 알 필요가없는 클래스 구현의 특정 측면을 숨기려고합니다. 따라서 컴포지션은 캡슐화 (즉, 구현 은폐)를 지원하는 방법이 더 많으며 상속은 추상화 를 지원하기위한 것입니다 (즉, 내부가 다른 유형의 범위에 대해 동일한 인터페이스를 제공합니다).
서브 타이핑은 변수가 열거 될 수있는 경우 적절하고 강력하며 , 그렇지 않으면 확장 성을 위해 함수 구성을 사용하십시오.
그가 @Pavel에 동의하면 작곡 장소와 상속 장소가 있다고 말합니다.
귀하의 답변이 위의 질문 중 하나라도 긍정적이라면 상속을 사용해야한다고 생각합니다.
그러나 의도가 코드 재사용의 의도라면 컴포지션이 더 나은 디자인 선택입니다.
상속은 코드 재사용을위한 매우 강력한 메커니즘입니다. 그러나 올바르게 사용되어야합니다. 하위 클래스가 부모 클래스의 하위 유형 인 경우 상속이 올바르게 사용된다고 말하고 싶습니다. 위에서 언급했듯이 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) 메소드의 동작은 상위 유형의 특성을 유지하지 않고 명확하게 변경되었습니다. 수업의 클라이언트의 관점에서 생각하십시오. 정수 목록이 필요한 정수 세트를 수신 할 수 있습니다. 클라이언트는 값이 목록에 이미 존재하더라도 값을 추가하고 해당 값을 목록에 추가 할 수 있습니다. 그러나 그녀는 그 가치가 존재한다면 그 행동을 얻지 못할 것입니다. 그녀를위한 큰 놀라움!
이것은 상속의 부적절한 사용에 대한 전형적인 예입니다. 이 경우 컴포지션을 사용하십시오.
(조각 : 상속을 올바르게 사용하십시오 ).
내가 들었던 경험은 상속은 "is-a"관계 일 때 사용되어야하고 "has-a"일 때 구성을 사용해야한다는 것입니다. 그럼에도 불구하고 나는 많은 복잡성을 제거하기 때문에 항상 구성에 의존해야한다고 생각합니다.
작곡이 선호 되기는하지만 상속의 장점 과 작곡의 단점 을 강조하고 싶습니다 .
상속의 장점 :
논리적 " IS A" 관계를 설정합니다. 경우 자동차 및 트럭 의 두 종류가 차량 (베이스 클래스), 하위 클래스 A는 기본 클래스.
즉
자동차는 차량입니다
트럭은 차량입니다
상속을 통해 기능을 정의 / 수정 / 확장 할 수 있습니다
구성의 단점 :
예를 들어, 만약 자동차가 포함 차량을 하고 가격 얻을 수있는 경우 자동차 에 정의 된, 차량을 , 코드는 다음과 같이 될 것입니다
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
많은 사람들이 말했듯이 먼저 "is-a"관계가 있는지 확인하는 것으로 시작하겠습니다. 그것이 있다면 나는 보통 다음을 확인합니다.
기본 클래스를 인스턴스화 할 수 있는지 여부 즉, 기본 클래스가 비 추상적 일 수 있는지 여부입니다. 그것이 비 추상적 일 수 있다면 나는 보통 구성을 선호한다
예 1. 회계사 는 직원입니다. 그러나 Employee 객체를 인스턴스화 할 수 있기 때문에 상속을 사용 하지 않습니다 .
예를 들어 2. Book 은 SellingItem입니다. SellingItem은 인스턴스화 할 수 없으며 추상적 개념입니다. 따라서 나는 상속을 사용합니다. SellingItem은 추상 기본 클래스 (또는 C #의 인터페이스 )입니다.
이 접근법에 대해 어떻게 생각하십니까?
또한 왜 상속을 사용합니까?에서 @anon 답변을 지원 합니다.
상속을 사용하는 주된 이유는 구성의 형태가 아니기 때문에 다형성 동작을 얻을 수 있습니다. 다형성이 필요하지 않으면 상속을 사용하지 않아야합니다.
@MatthieuM. /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448에 말합니다.
상속 문제는 두 가지 직교 목적으로 사용할 수 있다는 것입니다.
인터페이스 (다형성)
구현 (코드 재사용)
참고
상속과 관련하여 발생할 수 있는 다이아몬드 문제에 대해서는 아무도 언급하지 않았습니다 .
B와 C 클래스가 A를 상속하고 메소드 X를 대체하고 네 번째 클래스 D를 B와 C에서 모두 상속하고 X를 대체하지 않으면 XD의 어떤 구현을 사용해야합니까?
Wikipedia 는이 질문에서 다루고있는 주제에 대한 훌륭한 개요를 제공합니다.