상속보다 구성을 선호하는 이유는 무엇입니까? 각 접근 방식에는 어떤 절충점이 있습니까? 구성보다 상속을 선택해야 하는 경우는 언제입니까?
질문자 :readonly
나중에 수정하기 쉽기 때문에 상속보다 구성을 선호하지만 항상 작성 접근 방식을 사용하지 마십시오. 구성을 사용하면 종속성 주입/세터를 사용하여 즉시 동작을 쉽게 변경할 수 있습니다. 상속은 대부분의 언어가 둘 이상의 유형에서 파생되는 것을 허용하지 않기 때문에 더 엄격합니다. 따라서 거위는 TypeA에서 파생되면 다소 요리됩니다.
위의 내 산성 테스트는 다음과 같습니다.
TypeB는 TypeA가 예상되는 곳에서 TypeB를 사용할 수 있도록 TypeA의 전체 인터페이스(모든 공용 메서드 이상)를 노출하기를 원합니까? 상속을 나타냅니다.
- 예를 들어 Cessna 복엽기는 비행기의 전체 인터페이스를 노출합니다. 따라서 Airplane에서 파생되는 것이 적합합니다.
TypeB는 TypeA에 의해 노출된 동작의 일부/일부만 원합니까? 구성이 필요함을 나타냅니다.
- 예를 들어 새는 비행기의 비행 동작만 필요할 수 있습니다. 이 경우 인터페이스 / 클래스 / 둘 다로 추출하여 두 클래스의 멤버로 만드는 것이 합리적입니다.
업데이트: 방금 내 대답으로 돌아왔고 '이 유형에서 상속해야 하나요?'에 대한 테스트로 Barbara Liskov의 Liskov 대체 원리에 대한 특정 언급 없이는 불완전한 것 같습니다.
Gishu
억제를 관계 가 있는 것으로 생각하십시오. 자동차에는 엔진이 "있다", 사람은 이름이 "있다" 등등.
는 관계로 상속 생각하십시오. 자동차는 "차량"이고, 사람은 "포유류"입니다.
나는 이 접근 방식을 인정하지 않습니다. Steve McConnell 의 Code Complete 2판 , 섹션 6.3 에서 직접 가져왔습니다.
Nick Zalutskiy
차이점을 이해하면 더 쉽게 설명할 수 있습니다.
절차 코드
이것의 예는 클래스를 사용하지 않는 PHP입니다(특히 PHP5 이전). 모든 논리는 함수 집합으로 인코딩됩니다. 도우미 함수 등이 포함된 다른 파일을 포함하고 함수에서 데이터를 전달하여 비즈니스 논리를 수행할 수 있습니다. 이는 애플리케이션이 성장함에 따라 관리하기가 매우 어려울 수 있습니다. PHP5는 보다 객체 지향적인 디자인을 제공하여 이 문제를 해결하려고 합니다.
계승
이것은 수업의 사용을 장려합니다. 상속은 객체지향 설계의 세 가지 신조(상속, 다형성, 캡슐화) 중 하나입니다.
class Person { String Title; String Name; Int Age } class Employee : Person { Int Salary; String Title; }
이것은 직장에서의 상속입니다. 직원은 "사람"이거나 Person에서 상속합니다. 모든 상속 관계는 "is-as" 관계입니다. Employee는 또한 Person의 Title 속성을 숨기므로 Employee.Title은 Person이 아닌 Employee에 대한 Title을 반환합니다.
구성
상속보다 구성이 선호됩니다. 아주 간단하게 말하면 다음과 같습니다.
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 개체는 Employee와 Person으로 구성됩니다. 직위 동작은 직원에게서 가져옵니다. 이 명시적 구성은 무엇보다도 모호성을 제거하고 더 적은 수의 버그가 발생합니다.
aleemb
상속이 제공하는 부인할 수 없는 모든 이점과 함께 여기에 몇 가지 단점이 있습니다.
상속의 단점:
- 런타임에 슈퍼 클래스에서 상속된 구현을 변경할 수 없습니다(상속이 컴파일 시간에 정의되기 때문에).
- 상속은 하위 클래스를 상위 클래스 구현의 세부 사항에 노출하므로 상속이 캡슐화를 중단한다고 자주 말하는 이유입니다.
- 상속에 의해 제공되는 긴밀한 결합은 하위 클래스의 구현을 상위 클래스의 구현과 매우 밀접하게 연결하여 상위 구현의 변경이 하위 클래스를 강제로 변경하도록 합니다.
- 하위 분류에 의한 과도한 재사용은 상속 스택을 매우 깊고 매우 혼란스럽게 만들 수 있습니다.
반면에 개체 구성 은 다른 개체에 대한 참조를 획득하는 개체를 통해 런타임에 정의됩니다. 이러한 경우 이러한 개체는 서로의 보호된 데이터에 도달할 수 없으며(캡슐화 중단 없음) 서로의 인터페이스를 존중해야 합니다. 그리고 이 경우에도 구현 종속성은 상속의 경우보다 훨씬 적습니다.
Galilyou
상속보다 구성을 선호하는 또 다른 매우 실용적인 이유는 도메인 모델과 관계형 데이터베이스에 매핑하는 것과 관련이 있습니다. 상속을 SQL 모델에 매핑하는 것은 정말 어렵습니다(항상 사용되지 않는 열 생성, 뷰 사용 등과 같은 모든 종류의 해킹 해결 방법으로 끝납니다). 일부 ORML은 이를 처리하려고 하지만 항상 빠르게 복잡해집니다. 구성은 두 테이블 간의 외래 키 관계를 통해 쉽게 모델링할 수 있지만 상속은 훨씬 어렵습니다.
Tim Howland
간단히 말해서 나는 "상속보다 작곡을 선호한다"에 동의하지만, 저에게는 "코카콜라보다 감자를 선호합니다"처럼 들릴 때가 많습니다. 상속할 곳과 구성할 곳이 있습니다. 차이점을 이해해야이 질문이 사라집니다. 그것이 나에게 실제로 의미하는 것은 "상속을 사용하려는 경우 - 다시 생각해 보세요. 구성이 필요할 가능성이 있습니다."입니다.
먹고 싶을 때는 코카콜라보다 감자를, 마시고 싶을 때는 감자보다 코카콜라를 즐겨야 한다.
서브클래스를 생성한다는 것은 슈퍼클래스 메소드를 호출하는 편리한 방법 이상을 의미해야 합니다. 구조적으로나 기능적으로나 서브클래스가 "is-a" 슈퍼클래스일 때 상속을 사용해야 하고, 슈퍼클래스로 사용할 수 있고 그것을 사용하려고 할 때 상속을 사용해야 합니다. 그렇지 않은 경우 - 상속이 아니라 다른 것입니다. 구성은 개체가 다른 개체로 구성되거나 개체와 어떤 관계가 있는 경우입니다.
그래서 나에게는 상속이나 구성이 필요한지 모르는 사람이 있으면 진짜 문제는 그가 마실 것인지 먹을 것인지 모른다는 것입니다. 문제 영역에 대해 더 많이 생각하고 더 잘 이해하십시오.
Pavel Feldman
상속은 특히 절차적 영역에서 오는 매우 매력적이며 종종 현혹될 정도로 우아해 보입니다. 내가 해야 할 일은 이 기능을 다른 클래스에 추가하는 것뿐입니다. 그렇죠? 음, 문제 중 하나는
상속은 아마도 당신이 가질 수 있는 최악의 커플링 형태일 것입니다.
기본 클래스는 보호된 멤버의 형태로 하위 클래스에 구현 세부 정보를 노출하여 캡슐화를 중단합니다. 이것은 시스템을 단단하고 취약하게 만듭니다. 그러나 더 비극적인 결점은 새로운 하위 클래스가 상속 체인의 모든 짐과 의견을 수반한다는 것입니다.
Inheritance is Evil: The Epic Fail of the DataAnnotationsModelBinder 기사는 C#에서 이에 대한 예제를 안내합니다. 컴포지션이 사용되어야 하는 경우와 이를 리팩토링할 수 있는 방법을 보여줍니다.
Mike Valenty
여기에서 만족스러운 답변을 찾지 못해 새로 작성했습니다.
"상속보다 구성을 선호하는 " 이유를 이해하려면 먼저 이 단축된 관용구에서 생략된 가정을 다시 가져와야 합니다.
상속에는 두 가지 이점이 있습니다. 하위 유형 지정 및 하위 분류
하위 유형 지정은 유형(인터페이스) 서명, 즉 API 세트를 준수하는 것을 의미하며 하위 유형 다형성을 달성하기 위해 서명의 일부를 재정의할 수 있습니다.
서브클래 싱은 메소드 구현의 암시적 재사용을 의미합니다.
두 가지 이점과 함께 상속을 수행하는 두 가지 다른 목적, 즉 하위 유형 지정 및 코드 재사용 지향이 제공됩니다.
코드 재사용이 유일한 목적이라면, 서브클래싱은 그가 필요로 하는 것보다 더 많은 것을 제공할 수 있습니다. 이 경우 상속보다 구성을 선호하는 대신 구성이 요구 됩니다. 이것은 또한 "is-a" 대 "has-a" 개념이 나오는 곳이기도 합니다.
따라서 하위 유형 지정이 목적인 경우, 즉 나중에 다형성 방식으로 새 클래스를 사용하는 경우에만 상속 또는 구성을 선택하는 문제에 직면하게 됩니다. 이것은 논의 중인 단축 관용구에서 생략된 가정입니다.
하위 유형은 유형 서명을 준수하는 것입니다. 이는 구성이 항상 유형의 API를 더 적게 노출해야 함을 의미합니다. 이제 트레이드 오프가 시작됩니다.
상속은 재정의되지 않는 경우 간단한 코드 재사용을 제공하는 반면 작성은 단순한 위임 작업일지라도 모든 API를 다시 코딩해야 합니다.
상속은 내부 다형성 사이트
this
를 통해 직접 공개 재귀 를 제공합니다. 즉, public 또는 private( 권장 되지는 않지만) 다른 멤버 함수에서 재정의 메서드(또는 type )를 호출합니다. 개방형 재귀는 구성을 통해 시뮬레이션 할 수 있지만 추가 노력이 필요하고 항상 실행 가능한(?) 것은 아닙니다. 중복된 질문에 대한 이 답변 은 비슷한 내용을 말합니다.상속은 보호된 멤버를 노출합니다. 이것은 상위 클래스의 캡슐화를 깨고 하위 클래스에서 사용하는 경우 하위 클래스와 상위 클래스 사이에 또 다른 종속성이 도입됩니다.
구성은 제어 역전(inversion of control)의 장점이 있으며, 데코레이터 패턴 및 프록시 패턴 에서 볼 수 있듯이 종속성을 동적으로 주입할 수 있습니다.
합성은 결합자 지향 프로그래밍의 이점이 있습니다 . 즉, 합성 패턴 과 같은 방식으로 작동합니다.
구성 은 인터페이스에 대한 프로그래밍 직후에 수행됩니다.
합성은 쉬운 다중 상속 의 이점이 있습니다.
위의 절충안을 염두에 두고 우리는 상속보다 합성을 선호합니다. 그러나 밀접하게 관련된 클래스의 경우, 즉 암시적 코드 재사용이 실제로 이점을 제공하거나 공개 재귀의 마법의 힘이 필요한 경우 상속이 선택되어야 합니다.
lcn
Java 또는 C#에서 개체는 일단 인스턴스화되면 해당 유형을 변경할 수 없습니다.
따라서 개체가 다른 개체로 나타나거나 개체 상태 또는 조건에 따라 다르게 동작해야 하는 경우 구성 을 사용하십시오. 상태 및 전략 디자인 패턴을 참조하십시오.
객체가 동일한 유형이어야 하는 경우 상속 을 사용하거나 인터페이스를 구현하십시오.
dance2die
컴포지션은 언제 사용할 수 있나요?
당신은 항상 합성을 사용할 수 있습니다. 경우에 따라 상속도 가능하고 더 강력하고 직관적인 API로 이어질 수 있지만 구성은 항상 옵션입니다.
상속은 언제 사용할 수 있습니까?
"bar가 foo"이면 Bar
Foo
클래스를 상속할 수 있다고 흔히 말합니다. 불행히도 이 테스트만으로는 신뢰할 수 없으므로 다음을 대신 사용하십시오.
- 바는 foo는이며,
- bar는 foo가 할 수 있는 모든 것을 할 수 있습니다.
첫 번째 테스트는 Foo
모든 getter 가 Bar
(= 공유 속성)에서 의미가 있는지 확인하는 반면, 두 번째 테스트는 Foo
모든 setter 가 Bar
(= 공유 기능)에서 의미가 있는지 확인합니다.
예: 개/동물
개는 동물이며 개는 동물이 할 수 있는 모든 것을 할 수 있습니다(예: 호흡, 이동 등). 따라서 Dog
Animal
클래스를 상속 할 수 있습니다 .
반대 예: 원/타원
원은 타원이지만 원이 타원이 할 수 있는 모든 일을 할 수는 없습니다. 예를 들어 원은 늘릴 수 없지만 타원은 늘릴 수 있습니다. 따라서 Circle
Ellipse
클래스를 상속 할 수 없습니다 .
이것을 Circle-Ellipse problem 이라고 하며 실제로 문제가 되지는 않지만 "bar is foo" 자체가 신뢰할 수 있는 테스트가 아니라는 표시가 더 많습니다. 특히 이 예제는 파생 클래스가 기본 클래스의 기능을 확장 해야 하며 제한 하지 않아야 한다는 점을 강조합니다. 그렇지 않으면 기본 클래스를 다형성으로 사용할 수 없습니다. "bars can do all things that foos can do" 테스트를 추가하면 다형성 사용이 가능하고 Liskov 대체 원리 와 동일합니다.
기본 클래스에 대한 포인터나 참조를 사용하는 함수는 자신도 모르게 파생 클래스의 개체를 사용할 수 있어야 합니다.
상속은 언제 사용해야 합니까?
상속을 사용할 수 있다고 해서 반드시 해야 하는 것은 아닙니다. 구성을 사용하는 것은 항상 선택 사항입니다. 상속은 암시적 코드 재사용 및 동적 디스패치를 허용하는 강력한 도구이지만 몇 가지 단점이 있으므로 구성이 선호되는 경우가 많습니다. 상속과 구성 사이의 절충점은 분명하지 않으며 제 생각에는 lcn의 답변 에 가장 잘 설명되어 있습니다.
경험상 나는 다형성 사용이 매우 일반적일 것으로 예상되는 경우 구성보다 상속을 선택하는 경향이 있습니다. 이 경우 동적 디스패치의 힘이 훨씬 더 읽기 쉽고 우아한 API로 이어질 수 있습니다. 예를 들어, GUI 프레임워크에 Widget
Node
를 사용하면 순전히 구성에 기반한 솔루션보다 훨씬 더 읽기 쉽고 직관적인 API를 사용할 수 있습니다.
Boris Dalstein
개인적으로 나는 항상 상속보다 합성을 선호하는 법을 배웠습니다. 합성으로 해결할 수 없는 상속으로 해결할 수 있는 프로그래밍 문제는 없습니다. 경우에 따라 Interfaces(Java) 또는 Protocols(Obj-C)를 사용해야 할 수도 있습니다. C++는 그런 것을 모르기 때문에 추상 기본 클래스를 사용해야 합니다. 즉, C++에서 상속을 완전히 제거할 수는 없습니다.
컴포지션은 종종 더 논리적이고 더 나은 추상화, 더 나은 캡슐화, 더 나은 코드 재사용(특히 매우 큰 프로젝트에서)을 제공하고 코드의 어느 곳에서나 격리된 변경을 수행했기 때문에 멀리 떨어진 곳에서 아무 것도 깨뜨릴 가능성이 적습니다. 또한 종종 "클래스가 변경되는 이유는 하나 이상 있어서는 안 된다."로 요약되는 "단일 책임 원칙 "을 쉽게 유지할 수 있으며, 이는 모든 클래스가 특정 목적을 위해 존재하며 목적과 직접적으로 관련된 메서드만 있습니다. 또한 매우 얕은 상속 트리를 사용하면 프로젝트가 실제로 커지기 시작하는 경우에도 개요를 훨씬 쉽게 유지할 수 있습니다. 많은 사람들은 상속이 우리의 현실 세계를 꽤 잘 나타낸다고 생각하지만, 그것은 사실이 아닙니다. 현실 세계는 상속보다 훨씬 더 많은 구성을 사용합니다. 손에 쥘 수 있는 거의 모든 실제 세계 개체는 다른 더 작은 현실 세계 개체로 구성되어 있습니다.
그러나 구성의 단점이 있습니다. 상속을 완전히 건너뛰고 구성에만 집중하면 상속을 사용했다면 필요하지 않은 몇 줄의 추가 코드를 작성해야 하는 경우가 많다는 것을 알게 될 것입니다. 또한 때때로 자신을 반복해야 하며 이는 DRY 원칙 (DRY = Don't Repeat Yourself)에 위배됩니다. 또한 합성에는 종종 위임이 필요하며 메서드는 이 호출을 둘러싼 다른 코드 없이 다른 개체의 다른 메서드를 호출하고 있습니다. 이러한 "이중 메서드 호출"(3중 또는 4중 메서드 호출로 쉽게 확장될 수 있으며 그 이상으로 확장될 수 있음)은 단순히 부모의 메서드를 상속하는 상속보다 성능이 훨씬 나쁩니다. 상속된 메서드를 호출하는 것은 상속되지 않은 메서드를 호출하는 것과 동일하게 빠르거나 약간 느릴 수 있지만 일반적으로 두 번의 연속 메서드 호출보다 여전히 빠릅니다.
대부분의 객체지향 언어가 다중 상속을 허용하지 않는다는 사실을 눈치채셨을 것입니다. 다중 상속이 실제로 당신에게 무언가를 살 수 있는 몇 가지 경우가 있지만, 이는 규칙보다 오히려 예외입니다. "다중 상속이 이 문제를 해결하는 데 정말 멋진 기능이 될 것"이라고 생각하는 상황에 처할 때마다 일반적으로 상속을 완전히 다시 생각해야 하는 상황에 처하게 됩니다. 두 줄의 추가 코드가 필요할 수도 있기 때문입니다 , 구성에 기반한 솔루션은 일반적으로 훨씬 더 우아하고 유연하며 미래에 대비할 수 있습니다.
상속은 정말 멋진 기능이지만 지난 몇 년 동안 과도하게 사용된 것 같습니다. 사람들은 상속을 그것이 실제로 못이든, 나사이든, 아니면 완전히 다른 것이든 상관없이 모든 것을 못 박을 수 있는 하나의 망치로 취급했습니다.
Mecki
나의 일반적인 경험 법칙: 상속을 사용하기 전에 구성이 더 합리적인지 고려하십시오.
이유: 하위 분류는 일반적으로 더 많은 복잡성과 연결성을 의미합니다. 즉, 실수 없이 변경, 유지 관리 및 확장하기가 더 어렵습니다.
Sun의 Tim Boudreau 의 훨씬 더 완전하고 구체적인 답변:
내가 보는 상속 사용의 일반적인 문제는 다음과 같습니다.
- 순진한 행위는 예상치 못한 결과를 초래할 수 있습니다. - 이것의 전형적인 예는 서브클래스 인스턴스 필드가 초기화되기 전에 슈퍼클래스 생성자에서 재정의 가능한 메소드에 대한 호출입니다. 완벽한 세상에서는 아무도 그렇게 하지 않을 것입니다. 이것은 완벽한 세상이 아닙니다.
- 이는 서브클래서가 메서드 호출 순서 등에 대해 가정하는 잘못된 유혹을 제공합니다. 이러한 가정은 슈퍼클래스가 시간이 지남에 따라 진화할 수 있는 경우 안정적이지 않은 경향이 있습니다. 내 토스터와 커피 포트 비유 도 참조하십시오.
- 클래스가 무거워집니다. 수퍼클래스가 생성자에서 수행하는 작업이나 사용할 메모리 양을 반드시 알 수는 없습니다. 따라서 순진한 경량 객체를 구성하는 것은 생각보다 훨씬 비쌀 수 있으며 슈퍼클래스가 진화하면 시간이 지남에 따라 변경될 수 있습니다.
- 그것은 하위 클래스의 폭발을 조장합니다 . 클래스 로딩은 시간을 소모하고, 더 많은 클래스는 메모리를 소모합니다. 이것은 NetBeans 규모의 앱을 다룰 때까지는 문제가 되지 않을 수 있지만, 예를 들어 메뉴의 첫 번째 표시가 대규모 클래스 로딩을 유발하여 메뉴가 느려지는 것과 같은 실제 문제가 있었습니다. 보다 선언적인 구문 및 기타 기술로 이동하여 이 문제를 수정했지만 수정하는 데에도 시간이 소요되었습니다.
- 나중에 변경하기가 더 어려워 집니다. 클래스를 공개한 경우 수퍼클래스를 교체하면 하위 클래스가 중단됩니다. 코드를 공개하면 결혼하게 되는 선택입니다. 따라서 수퍼클래스의 실제 기능을 변경하지 않는다면 필요한 것을 확장하는 것보다 나중에 사용할 때 훨씬 더 자유롭게 변경할 수 있습니다. 예를 들어, JPanel을 서브클래싱하는 것은 일반적으로 잘못된 것입니다. 하위 클래스가 어딘가에 공개되어 있으면 해당 결정을 다시 검토할 기회가 없습니다. JComponent getThePanel() 로 액세스하는 경우 여전히 수행할 수 있습니다(힌트: API로 내부 구성 요소의 모델을 노출).
- 개체 계층 구조는 확장되지 않습니다(또는 나중에 확장하는 것이 미리 계획하는 것보다 훨씬 어렵습니다) . 이것은 고전적인 "너무 많은 레이어" 문제입니다. 아래에서 이에 대해 설명하고 AskTheOracle 패턴이 문제를 해결하는 방법(OOP 순수주의자의 기분을 상하게 할 수 있음)에 대해 설명합니다.
...
소금 한 알과 함께 받을 수 있는 상속을 허용한다면 해야 할 일에 대한 나의 견해는 다음과 같습니다.
- 상수를 제외하고 필드를 노출하지 마십시오.
- 메소드는 추상적이거나 최종적이어야 합니다.
- 수퍼클래스 생성자에서 메소드를 호출하지 않음
...
이 모든 것은 큰 프로젝트보다 작은 프로젝트에 덜 적용되고 공공 프로젝트보다 개인 클래스에는 덜 적용됩니다.
Peter Tseng
상속은 매우 강력하지만 강제할 수는 없습니다( 원-타원 문제 참조 ). 진정한 "is-a" 하위 유형 관계를 완전히 확신할 수 없다면 구성을 사용하는 것이 가장 좋습니다.
yukondude
상속은 하위 클래스와 상위 클래스 사이에 강력한 관계를 만듭니다. 하위 클래스는 상위 클래스의 구현 세부 정보를 알고 있어야 합니다. 슈퍼 클래스를 만드는 것은 확장 방법에 대해 생각해야 할 때 훨씬 더 어렵습니다. 클래스 불변성을 주의 깊게 문서화하고 재정의 가능한 다른 메서드가 내부적으로 사용하는 방법을 명시해야 합니다.
계층 구조가 실제로 is-relationship을 나타내는 경우 상속이 유용할 때가 있습니다. 이것은 클래스가 수정을 위해 닫혀야 하지만 확장은 열려 있어야 한다는 개방-폐쇄 원칙과 관련이 있습니다. 그렇게 하면 다형성을 가질 수 있습니다. 수퍼 유형 및 해당 메소드를 처리하는 일반 메소드를 가지지만 동적 디스패치를 통해 하위 클래스의 메소드가 호출됩니다. 이것은 유연하고 소프트웨어에서 필수적인 간접 참조를 생성하는 데 도움이 됩니다(구현 세부 정보에 대해 덜 알기 위해).
그러나 상속은 쉽게 남용되고 클래스 간의 엄격한 종속성으로 인해 추가 복잡성을 생성합니다. 또한 프로그램 실행 중에 일어나는 일을 이해하는 것은 계층과 메소드 호출의 동적 선택으로 인해 상당히 어려워집니다.
작성을 기본값으로 사용하는 것이 좋습니다. 모듈식이며 후기 바인딩의 이점을 제공합니다(구성 요소를 동적으로 변경할 수 있음). 또한 개별적으로 테스트하는 것이 더 쉽습니다. 그리고 클래스의 메서드를 사용해야 하는 경우 특정 형식(리스코프 대체 원리)을 강요하지 않습니다.
egaga
항공기에 엔진과 날개의 두 부분만 있다고 가정합니다.
그런 다음 항공기 클래스를 설계하는 두 가지 방법이 있습니다.
Class Aircraft extends Engine{ var wings; }
이제 항공기는 고정 날개로 시작할 수 있습니다.
즉시 회전 날개로 변경합니다. 그것은 본질적으로
날개가 달린 엔진. 하지만 내가 바꾸고 싶다면?
엔진도 즉석에서?
기본 클래스 Engine
은 변경을 위해 mutator를 노출합니다.
속성, 또는 나는 Aircraft
를 다음과 같이 재설계합니다:
Class Aircraft { var wings; var engine; }
이제 엔진도 즉석에서 교체할 수 있습니다.
simplfuzz
Uncle Bob의 클래스 디자인 의 SOLID 원칙에서 Liskov 대체 원칙 을 살펴봐야 합니다. :)
nabeelfarid
기본 클래스의 API를 "복사"/노출하려면 상속을 사용합니다. 기능을 "복사"하려는 경우에만 위임을 사용합니다.
이에 대한 한 가지 예: 목록에서 스택을 만들고 싶습니다. 스택에는 팝, 푸시 및 엿보기만 있습니다. 스택에서 push_back, push_front, removeAt 등의 기능을 원하지 않는다면 상속을 사용해서는 안 됩니다.
Anzurio
새로운 프로그래머를 위해 다른 관점에서 이 질문을 해결하려면:
상속은 종종 객체 지향 프로그래밍을 배울 때 일찍 가르쳐지기 때문에 일반적인 문제에 대한 쉬운 솔루션으로 간주됩니다.
나는 모두 몇 가지 공통 기능이 필요한 세 가지 클래스가 있습니다. 따라서 기본 클래스를 작성하고 모두 기본 클래스에서 상속하도록 하면 모두 해당 기능을 갖게 되며 한 곳에서 유지하기만 하면 됩니다.
훌륭하게 들리지만 실제로는 다음 몇 가지 이유 중 하나로 인해 거의 작동하지 않습니다.
- 우리는 우리 클래스에 갖고 싶은 다른 기능이 있다는 것을 발견했습니다. 클래스에 기능을 추가하는 방법이 상속을 통한 것이라면, 기존 기본 클래스에 해당 기능을 상속하는 모든 클래스가 해당 기능을 필요로 하지 않더라도 이를 기존 기본 클래스에 추가할 것인지 결정해야 합니다. 다른 기본 클래스를 생성합니까? 그러나 이미 다른 기본 클래스에서 상속받은 클래스는 어떻습니까?
- 우리는 기본 클래스에서 상속받은 클래스 중 하나에 대해서만 기본 클래스가 약간 다르게 동작하기를 원한다는 것을 발견했습니다. 이제 우리는 기본 클래스로 돌아가서 약간의 가상 메서드를 추가하거나 더 나쁘게는 다음과 같은 코드를 추가할 수 있습니다. ." 여러 가지 이유로 좋지 않습니다. 하나는 기본 클래스를 변경할 때마다 상속된 모든 클래스를 효과적으로 변경한다는 것입니다. 그래서 우리는 클래스 A에서 약간 다른 동작이 필요하기 때문에 클래스 A, B, C 및 D를 변경합니다. 우리가 생각하는 만큼 조심스럽게, 우리는 클래스 A, B, C 및 D와 아무 관련이 없는 이유로 해당 클래스 중 하나를 중단할 수 있습니다. 클래스.
- 우리는 이 모든 클래스가 서로 상속되도록 결정한 이유를 알 수 있지만 코드를 유지 관리해야 하는 다른 사람에게는 이해가 되지 않을 수도 있습니다(아마도). 우리는 그들을 어려운 선택으로 몰아넣을 수 있습니다. 내가 필요한 변경을 하기 위해 정말 추하고 지저분한 일을 합니까(이전 글머리 기호 참조), 아니면 그냥 이것의 무리를 다시 작성해야 합니까?
결국 우리는 코드를 어려운 매듭으로 묶고 "멋져요, 상속에 대해 배웠고 이제 사용했습니다."라고 말하는 것을 제외하고는 아무런 이점도 얻지 못합니다. 그것은 우리 모두가 해냈기 때문에 멸시하려는 것이 아닙니다. 하지만 아무도 하지 말라고 하지 않았기 때문에 우리는 모두 그렇게 했습니다.
누군가가 나에게 "상속보다 구성을 선호한다"고 설명하자마자 상속을 사용하여 클래스 간에 기능을 공유하려고 할 때마다 다시 생각했고 대부분의 경우 제대로 작동하지 않는다는 것을 깨달았습니다.
해독제는 단일 책임 원칙 입니다. 제약으로 생각하십시오. 내 수업은 한 가지를 해야 합니다. 클래스가 하는 일을 설명하는 이름을 클래스에 지정할 수 있어야 합니다. (모든 것에 예외가 있지만 우리가 학습할 때 절대 규칙이 더 나은 경우가 있습니다.) 따라서 ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
라는 기본 클래스를 작성할 수 없습니다. 내가 필요로 하는 고유한 기능이 무엇이든 자체 클래스에 있어야 하며 해당 기능이 필요한 다른 클래스는 해당 클래스에서 상속 되지 않고 해당 클래스에 종속될 수 있습니다.
과도하게 단순화할 위험이 있는 것은 함께 작업하기 위해 여러 클래스를 구성하는 구성입니다. 그리고 일단 그 습관을 형성하면 상속을 사용하는 것보다 훨씬 더 유연하고 유지 관리 가능하며 테스트 가능하다는 것을 알게 됩니다.
Scott Hannen
이 두 가지 방법은 함께 잘 살 수 있고 실제로 서로를 지원합니다.
컴포지션은 모듈식으로 재생됩니다. 부모 클래스와 유사한 인터페이스를 만들고 새 개체를 만들고 호출을 위임합니다. 이러한 개체가 서로를 알 필요가 없는 경우 구성을 사용하는 것이 매우 안전하고 사용하기 쉽습니다. 여기에는 많은 가능성이 있습니다.
그러나 어떤 이유로 부모 클래스가 경험이 없는 프로그래머를 위해 "자식 클래스"에서 제공하는 기능에 액세스해야 하는 경우 상속을 사용하기에 좋은 장소처럼 보일 수 있습니다. 상위 클래스는 하위 클래스에 의해 덮어쓰여진 자체 추상 "foo()"를 호출한 다음 추상 기반에 값을 제공할 수 있습니다.
좋은 생각처럼 보이지만 많은 경우에 필요한 기본 클래스에서 새 클래스를 상속하는 것보다 foo()를 구현하는 객체를 클래스에 제공하는 것이 (또는 foo() 제공한 값을 수동으로 설정하는 것이 더 좋습니다. foo() 함수를 지정합니다.
왜요?
상속은 정보 이동에 좋지 않은 방법이기 때문 입니다.
구성은 여기에서 진정한 장점을 가지고 있습니다. 관계는 역전될 수 있습니다. "부모 클래스" 또는 "추상 작업자"는 특정 인터페이스를 구현하는 특정 "자식" 개체를 집계할 수 있습니다. 유형 입니다. 예를 들어 MergeSort 또는 QuickSort는 추상 비교 인터페이스를 구현하는 모든 개체 목록을 정렬할 수 있습니다. 다른 식으로 말하면 "foo()"를 구현하는 모든 개체 그룹과 "foo()"가 있는 개체를 사용할 수 있는 다른 개체 그룹이 함께 사용할 수 있습니다.
상속을 사용하는 세 가지 실제 이유를 생각할 수 있습니다.
- 동일한 인터페이스를 가진 많은 클래스가 있고 작성 시간을 절약하고 싶습니다.
- 각 개체에 대해 동일한 기본 클래스를 사용해야 합니다.
- 어떤 경우에도 공개할 수 없는 비공개 변수를 수정해야 합니다.
이것이 사실이라면 상속을 사용해야 할 것입니다.
이유 1을 사용하는 데 나쁜 것은 없습니다. 객체에 견고한 인터페이스를 갖는 것은 매우 좋은 일입니다. 이것은 구성을 사용하거나 상속을 사용하여 수행할 수 있습니다. 이 인터페이스가 간단하고 변경되지 않으면 문제가 없습니다. 일반적으로 상속은 여기에서 매우 효과적입니다.
이유가 2번이라면 조금 까다로워집니다. 동일한 기본 클래스만 사용해야 합니까? 일반적으로 동일한 기본 클래스를 사용하는 것만으로는 충분하지 않지만 프레임워크의 요구 사항일 수 있으며 피할 수 없는 디자인 고려 사항입니다.
그러나 private 변수를 사용하고 싶다면 case 3에 문제가 생길 수 있습니다. 전역 변수가 안전하지 않다고 생각한다면 상속을 사용하여 개인 변수에도 unsafe 에 액세스하는 것을 고려해야 합니다. 전역 변수가 나쁜 것만은 아닙니다. 데이터베이스는 본질적으로 전역 변수의 큰 집합입니다. 하지만 당신이 그것을 다룰 수 있다면, 그것은 꽤 괜찮습니다.
Tero Tolonen
고려 사항이 있음을 제외하고는 객체가 거쳐야 하는 상속의 "깊이"도 고려해야 합니다. 5~6레벨 이상의 상속 깊이는 예기치 않은 캐스팅 및 boxing/unboxing 문제를 일으킬 수 있으며, 이러한 경우 대신 개체를 구성하는 것이 현명할 수 있습니다.
Jon Limjap
당신이 때이 두 클래스 (예를 들어, 개는 개입니다)의 관계-A는, 당신은 상속을 위해 이동합니다.
반면에 당신이 가지고있는 경우가-a 또는 일부 두 클래스 사이의 형용사 관계 (학생이 교육 과정을 가지고) 또는 (교사 연구 과정), 당신은 구성을 선택했다.
Amir Aslam
이것을 이해하는 간단한 방법은 클래스의 객체가 부모 클래스와 동일한 인터페이스 를 가질 필요가 있을 때 상속을 사용하여 부모 클래스의 객체로 취급할 수 있도록 해야 한다는 것입니다(업캐스팅) . 또한 파생 클래스 개체에 대한 함수 호출은 코드의 모든 위치에서 동일하게 유지되지만 호출할 특정 메서드는 런타임에 결정됩니다(즉, 하위 수준 구현이 다르고 상위 수준 인터페이스 가 동일하게 유지됨).
새 클래스가 동일한 인터페이스를 가질 필요가 없을 때, 즉 해당 클래스의 사용자가 알 필요가 없는 클래스 구현의 특정 측면을 숨기고 싶을 때 합성을 사용해야 합니다. 따라서 구성은 캡슐화 를 지원하는 방식(즉, 구현을 숨김)에 더 가깝지만 상속은 추상화 를 지원하기 위한 것입니다(즉, 무언가의 단순화된 표현 제공, 이 경우 내부가 다른 다양한 유형에 대해 동일한 인터페이스 제공).
Y.S
하위 유형 지정은 불변량이 열거될 수 있는 경우 적절하고 더 강력하며, 그렇지 않으면 확장성을 위해 함수 구성을 사용합니다.
Shelby Moore III
나는 @Pavel이 구성할 곳이 있고 상속받을 곳이 있다고 말할 때 동의합니다.
귀하의 답변이 이러한 질문 중 하나라도 긍정적인 경우 상속을 사용해야 한다고 생각합니다.
- 귀하의 클래스는 다형성의 혜택을 받는 구조의 일부입니까? 예를 들어, draw()라는 메서드를 선언하는 Shape 클래스가 있는 경우에는 Circle 및 Square 클래스가 Shape의 하위 클래스가 되어야 하므로 해당 클라이언트 클래스가 특정 하위 클래스가 아니라 Shape에 종속되도록 해야 합니다.
- 당신의 수업은 다른 수업에 정의된 높은 수준의 상호 작용을 재사용해야 합니까? 템플릿 메서드 디자인 패턴은 상속 없이 구현하는 것이 불가능합니다. 모든 확장 가능한 프레임워크가 이 패턴을 사용한다고 생각합니다.
그러나 의도가 순전히 코드 재사용이라면 구성이 더 나은 디자인 선택일 가능성이 큽니다.
Parag
상속은 코드 재사용을 위한 매우 강력한 메커니즘입니다. 하지만 제대로 사용해야 합니다. 하위 클래스가 상위 클래스의 하위 유형이기도 하면 상속이 올바르게 사용된다고 말하고 싶습니다. 위에서 언급했듯이 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); } } }
정수 집합 클래스는 정수 목록의 하위 클래스이지만 목록 클래스의 모든 기능을 충족하지 않기 때문에 하위 유형이 아닙니다. 메서드의 값과 서명은 충족되지만 속성은 충족되지 않습니다. add(Integer) 메서드의 동작은 부모 유형의 속성을 유지하지 않고 명확하게 변경되었습니다. 수업의 클라이언트의 관점에서 생각하십시오. 그들은 정수 목록이 예상되는 정수 집합을 받을 수 있습니다. 클라이언트는 값을 추가하고 해당 값이 목록에 이미 있는 경우에도 목록에 해당 값을 추가하기를 원할 수 있습니다. 그러나 가치가 존재한다면 그녀는 그 행동을 취하지 않을 것입니다. 그녀를 위한 큰 서프라이즈!
이것은 상속의 부적절한 사용의 전형적인 예입니다. 이 경우 컴포지션을 사용하십시오.
(의 단편: 상속을 적절하게 사용하십시오 ).
Enrique Molinari
내가 들었던 경험에 따르면 상속은 "is-a" 관계일 때 사용되어야 하고 구성은 "has-a"일 때 사용되어야 합니다. 그럼에도 불구하고 많은 복잡성을 제거하기 때문에 항상 구성에 의존해야 한다고 생각합니다.
Evrhet Milam
Composition 이 선호되지만 Inheritance 의 장점과 Composition 의 단점을 강조하고 싶습니다.
상속의 장점:
논리적 " IS A" 관계를 설정합니다. Car 와 Truck 이 두 가지 유형의 Vehicle (기본 클래스)인 경우 자식 클래스 는 기본 클래스입니다.
즉
자동차는 차량이다
트럭은 차량이다
상속을 통해 기능을 정의/수정/확장할 수 있습니다.
- 기본 클래스는 구현을 제공하지 않으며 하위 클래스는 완전한 메서드(추상)를 재정의해야 합니다. => 계약을 구현할 수 있습니다.
- 기본 클래스는 기본 구현을 제공하고 하위 클래스는 동작을 변경할 수 있습니다. => 계약을 재정의할 수 있습니다.
- 하위 클래스는 super.methodName()을 첫 번째 명령문으로 호출하여 기본 클래스 구현에 확장을 추가합니다. => 계약을 확장할 수 있습니다.
- 기본 클래스는 알고리즘의 구조를 정의하고 하위 클래스는 알고리즘의 일부를 재정의합니다 => 기본 클래스 골격을 변경하지 않고 Template_method 를 구현할 수 있습니다.
구성의 단점:
- 상속에서는 IS A 관계로 인해 기본 클래스 메서드를 구현하지 않더라도 하위 클래스에서 직접 기본 클래스 메서드를 호출할 수 있습니다. 합성을 사용하는 경우 컨테이너 클래스에 메서드를 추가하여 포함된 클래스 API를 노출해야 합니다.
예를 들어, 만약 자동차는 차량을 포함하고 차량에 정의 된 자동차의 가격을해야하는 경우, 코드는 다음과 같이 될 것입니다
class Vehicle{ protected double getPrice(){ // return price } } class Car{ Vehicle vehicle; protected double getPrice(){ return vehicle.getPrice(); } }
Ravindra babu
구성 대 상속은 광범위한 주제입니다. 모든 것이 시스템의 설계에 달려 있다고 생각하기 때문에 무엇이 더 나은지에 대한 진정한 답은 없습니다.
일반적으로 객체 간의 관계 유형은 그 중 하나를 선택하는 데 더 나은 정보를 제공합니다.
관계 유형이 "IS-A" 관계인 경우 상속이 더 나은 접근 방식입니다. 그렇지 않으면 관계 유형이 "HAS-A" 관계이면 구성이 더 잘 접근합니다.
엔티티 관계에 전적으로 의존합니다.
Shami Qureshi
상속과 함께 발생할 수 있는 다이아몬드 문제 에 대해 언급한 사람은 아무도 없습니다.
한 눈에, 클래스 B와 C가 A를 상속하고 둘 다 메서드 X를 재정의하고 네 번째 클래스 D가 B와 C를 모두 상속하고 X를 재정의하지 않는 경우 XD의 어떤 구현을 사용해야 합니까?
Wikipedia 는 이 질문에서 논의되는 주제에 대한 훌륭한 개요를 제공합니다.
Veverke
많은 사람들이 말했듯이 나는 먼저 "is-is-a" 관계가 있는지 여부를 확인하는 것으로 시작할 것입니다. 존재하는 경우 일반적으로 다음을 확인합니다.
기본 클래스를 인스턴스화할 수 있는지 여부입니다. 즉, 기본 클래스가 추상이 아닐 수 있는지 여부입니다. 그것이 비추상적일 수 있다면 나는 보통 구성을 선호한다
예 1. 회계사 는 직원입니다. 그러나 Employee 개체를 인스턴스화할 수 있으므로 상속을 사용 하지 않습니다.
예 2. 책 은 SellingItem입니다. SellingItem은 인스턴스화할 수 없습니다. 추상적 개념입니다. 그래서 나는 상속여드름을 사용할 것이다. SellingItem은 추상 기본 클래스(또는 C#의 인터페이스)입니다.
이 접근 방식에 대해 어떻게 생각하십니까?
또한 상속을 전혀 사용하지 않는 이유 에서 @anon 답변을 지원합니다.
상속을 사용하는 주된 이유는 합성 형태가 아니라 다형성 동작을 얻을 수 있기 때문입니다. 다형성이 필요하지 않다면 상속을 사용하지 않아야 합니다.
@MattieuM. https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse/12448#comment303759_12448 에서 말합니다.
상속 문제는 두 가지 직교 목적으로 사용할 수 있다는 것입니다.
인터페이스(다형성용)
구현(코드 재사용용)
참조
LCJ
출처 : http:www.stackoverflow.com/questions/49002/prefer-composition-over-inheritance
'etc. > StackOverFlow' 카테고리의 다른 글
C#에서 두 개의 물음표가 함께 의미하는 것은 무엇입니까? (0) | 2022.01.02 |
---|---|
목록 항목의 발생 횟수를 어떻게 계산합니까? (0) | 2022.01.02 |
TypeScript는 무엇이며 JavaScript 대신 사용하는 이유는 무엇입니까? [닫은] (0) | 2022.01.02 |
Bash에서 문자열 배열을 반복합니까? (0) | 2021.12.31 |
Git에서 한 파일의 작업 복사본 수정을 취소하시겠습니까? (0) | 2021.12.31 |