클래스 계층 구조에서 객체 동작 또는 속성의 * 제거 *를 허용하는 언어 또는 디자인 패턴이 있습니까?


28

전통적인 클래스 계층 구조의 잘 알려진 단점은 실제 세계를 모델링 할 때 나쁘다는 것입니다. 예를 들어, 클래스로 동물의 종을 표현하려고합니다. 실제로 그렇게 할 때 몇 가지 문제가 있지만 해결책을 찾지 못한 것은 하위 클래스가 펭귄이 날 수없는 것처럼 수퍼 클래스에 정의 된 동작이나 속성을 "손실"할 때입니다. 아마도 더 좋은 예 일지 모르지만 그것이 내 마음에 오는 첫 번째 예입니다).

한편으로는 모든 속성과 동작에 대해 존재 여부를 지정하는 플래그를 정의하고 해당 동작이나 속성에 액세스하기 전에 매번 확인하지 않으려 고합니다. 조류 수업에서 새가 간단하고 명확하게 날 수 있다고 말하고 싶습니다. 그러나 나중에 어디에서나 끔찍한 핵을 사용하지 않고 "예외"를 정의 할 수 있다면 좋을 것입니다. 이것은 종종 시스템이 한동안 생산적 일 때 발생합니다. 갑자기 원래 디자인에 맞지 않는 "예외"를 발견하고이를 수용하기 위해 코드의 많은 부분을 변경하고 싶지 않습니다.

"슈퍼 클래스"에 큰 변화를주지 않고이 문제를 깨끗하게 처리 할 수있는 언어 나 디자인 패턴이 있습니까? 솔루션이 특정 사례 만 처리하더라도 여러 솔루션이 함께 완전한 전략을 구성 할 수 있습니다.

더 많은 생각을 한 후, 나는 Liskov 대체 원칙에 대해 잊었다는 것을 알게되었습니다. 그렇기 때문에 할 수 없습니다. 모든 주요 "기능 그룹"에 대해 "특성 / 인터페이스"를 정의한다고 가정하면 새 특성, 특수한 다람쥐 및 물고기와 같은 플라잉 특성이 구현되는 것처럼 계층 구조의 여러 가지에서 특성을 자유롭게 구현할 수 있습니다.

그래서 제 질문은 "어떻게 특성을 구현 해제 할 수 있습니까?" 수퍼 클래스가 Java Serializable 인 경우 상태를 직렬화 할 수있는 방법이 없어도 (예 : "소켓"이 포함 된 경우에도) 수퍼 클래스 여야합니다.

이를 수행하는 한 가지 방법은 항상 시작부터 모든 특성을 쌍으로 정의하는 것입니다. Flying 및 NotFlying (체크되지 않은 경우 UnsupportedOperationException이 발생 함). Not-trait는 새로운 인터페이스를 정의하지 않으며 간단히 확인할 수 있습니다. 특히 처음부터 사용하는 경우 "저렴한"솔루션처럼 들립니다.


3
'어딘가에 끔찍한 핵을 사용하지 않고'행동을 무능하게하는 것은 끔찍한 핵 function save_yourself_from_crashing_airplane(Bird b) { f.fly() }이다. (Peter Török가 말했듯이 LSP를 위반 함)
keppla

전략 패턴과 상속을 조합하면 특정 수퍼 유형에 대해 상속 된 동작을 "구성"할 수 있습니까? 말할 때 : " it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"행동을 해킹하는 팩토리 방법을 고려하십니까?
StuperUser

1
물론에서 그냥 던질 수 NotSupportedException있습니다 Penguin.fly().
Felix Dombek

언어가 진행 되는 한, 자식 클래스에서 메소드를 구현 해제 할 수 있습니다 . 예를 들어, 루비 : class Penguin < Bird; undef fly; end;. 당신이 상관없이 해야하는 것은 또 다른 문제이다.
Nathan Long

이것은 liskov 원리와 OOP의 요점을 어길 것입니다.
deadalnix

답변:


17

다른 사람들이 언급했듯이 LSP에 반대해야합니다.

그러나 서브 클래스는 수퍼 클래스의 임의의 확장 일 뿐이라고 주장 할 수 있습니다. 그것은 그 자체의 새로운 대상이며 수퍼 클래스와의 유일한 관계는 그것이 기초를 사용한다는 것입니다.

펭귄 새 라고 말하는 것이 논리적으로 의미 있습니다. 말하는 펭귄은 Bird의 행동 중 일부를 상속합니다.

일반적으로 동적 언어를 사용하면이를 쉽게 표현할 수 있습니다. JavaScript를 사용한 예는 다음과 같습니다.

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

이 특별한 경우 에는 값 을 가진 속성을 객체 에 작성하여 상속 Penguin하는 Bird.fly메서드를 섀도 잉 합니다.flyundefined

이제는 더 이상 Penguin정상으로 취급 될 수 없다고 말할 수 있습니다 Bird. 그러나 언급했듯이 실제 세계에서는 단순히 할 수 없습니다. 우리는 Bird비행 실체로 모델링 하기 때문입니다.

대안은 새가 날 수 있다는 광범위한 가정을하지 않는 것입니다. Bird모든 새들이 실패없이 상속받을 수 있는 추상화를하는 것이 합리적 일 것 입니다. 이것은 모든 서브 클래스가 보유 할 수있는 가정 만한다는 것을 의미합니다.

일반적으로 Mixin의 아이디어는 여기에 잘 적용됩니다. 매우 얇은 기본 클래스를 사용하고 다른 모든 동작을 혼합하십시오.

예:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

당신의 호기심 경우, 나는이 의 구현을Object.make

부가:

그래서 제 질문은 "어떻게 특성을 구현 해제 할 수 있습니까?" 수퍼 클래스가 Java Serializable 인 경우 상태를 직렬화 할 수있는 방법이 없어도 (예 : "소켓"이 포함 된 경우에도) 수퍼 클래스 여야합니다.

당신은 특성을 "구현하지"않습니다. 당신은 단순히 상속 상속 체계를 수정합니다. 당신은 슈퍼 클래스 계약을 이행 할 수 있거나 당신이 그런 종류의 척을해서는 안됩니다.

이것은 물체 구성이 빛나는 곳입니다.

게다가, Serializable은 모든 것이 직렬화되어야한다는 것을 의미하는 것이 아니라 "관심있는 상태"가 직렬화되어야한다는 것을 의미합니다.

"NotX"특성을 사용해서는 안됩니다. 그것은 단지 끔찍한 코드 팽창입니다. 함수가 날아 다니는 물체를 기대하면 매머드를 줄 때 충돌하고 타야합니다.


10
"실제 세계에서는 단순히 할 수 없습니다." 예, 그럴 수 있습니다. 펭귄은 새입니다. 비행 능력은 새의 재산이 아니며, 대부분의 조류 종의 우연의 재산입니다. 새를 정의하는 속성은 "깃털, 날개, 이족, 흡열, 알을 낳는, 척추 동물"(위키 백과)입니다.
pdr

2
@ pdr 다시 조류의 정의에 따라 다릅니다. "새"라는 용어를 사용하면서 나는 비행 법을 포함하여 조류를 나타내는 데 사용하는 클래스 추상화를 의미했습니다. 또한 클래스 추상화를 덜 구체적으로 만들 수 있다고 언급했습니다. 또한 펭귄은 깃털이 없습니다.
Raynos

2
@ Raynos : 펭귄은 실제로 깃털입니다. 그들의 깃털은 물론 짧고 밀도가 높습니다.
Jon Purdy

@JonPurdy 박람회, 나는 항상 그들이 모피가 있다고 상상합니다.
Raynos

일반적으로 +1, 특히 "매머드"의 경우 +1 롤!
Sebastien Diot

28

AFAIK의 모든 상속 기반 언어는 Liskov 대체 원칙을 기반으로합니다 . 서브 클래스에서 기본 클래스 속성을 제거 / 비활성화하면 LSP가 분명히 위반되므로 그러한 가능성이 어느 곳에서나 구현되지 않는다고 생각합니다. 실제 세계는 지저분하고 수학적인 추상화로 정확하게 모델링 할 수 없습니다.

일부 언어는 이러한 문제를보다 융통성있게 처리 하기 위해 특성 또는 믹스 인을 제공 합니다.


1
LSP는 클래스가 아닌 유형 에 대한 것 입니다.
Jörg W Mittag

2
@ PeterTörök :이 질문은 달리 존재하지 않을 것입니다 :-) Ruby의 두 가지 예를 생각할 수 있습니다. IS-NOT-A 이지만 Class서브 클래스입니다 . 그러나 많은 코드를 재사용하기 때문에 여전히 서브 클래스가되는 것이 좋습니다. OTOH, IS-A 이지만 두 코드는 코드를 공유하지 않기 때문에 상속 관계가 없습니다 ( 물론을 상속하는 것은 명백 합니다). 클래스는 코드 공유를위한 것이고, 유형은 프로토콜을 설명하기위한 것입니다. 와 같은 프로토콜 따라서 동일한 유형을 가지고 있지만, 자신의 클래스는 관련이 없습니다. ModuleClassModuleStringIOIOObjectIOStringIO
Jörg W Mittag

1
@ JörgWMittag, 알았어. 이제 네가 의미하는 바를 더 잘 이해한다. 그러나 나에게 당신의 첫 번째 예는 당신이 제안하는 근본적인 문제의 표현보다 상속의 오용처럼 보입니다. 퍼블릭 상속 IMO는 구현을 재사용하는 데 사용해서는 안되며 하위 유형 관계 (is-a) 만 표현하기 위해 사용해야합니다. 그리고 그것이 오용 될 수 있다는 사실은 그것을 실격시키지 않습니다. 나는 오용 될 수없는 어떤 도메인에서도 유용한 도구를 상상할 수 없습니다.
Péter Török

2
이 답변을지지하는 사람들에게 : 이것은 특히 수정 된 설명 후에 질문에 대한 답변이 아닙니다. 나는이 답변에 공감대가 있다고 생각하지 않는다. 왜냐하면 그가 말한 것이 매우 진실하고 아는 것이 중요하기 때문이다. 그러나 그는 실제로 그 질문에 대답하지 않았다.
jhocking

1
인터페이스 만 타입이고, 클래스가없고, 서브 클래스가 슈퍼 클래스의 인터페이스를 "구현 해제"할 수있는 자바를 상상 해보자.
Jörg W Mittag

15

Fly()전략 패턴에 대한 첫 번째 디자인 패턴 의 첫 번째 예에 있으며, 이것이 "상속 구성보다 선호되는 구성"에 대한 좋은 상황 입니다. .

당신의 슈퍼 타입함으로써 구성과 상속을 혼합 할 수있는 FlyingBird, FlightlessBird예를 들어, 관련 하위 유형이 있음을, 팩토리에 의해 주입 된 정확한 동작을 Penguin : FlightlessBird자동으로 얻고, 아무것도 정말 특정는 당연히 공장에 의해 처리됩니다.


1
내 대답에 데코레이터 패턴을 언급했지만 전략 패턴도 꽤 잘 작동합니다.
jhocking

1
"상속에 대한 선호 구성"+1 그러나 정적으로 유형이 지정된 언어로 컴포지션을 구현하려면 특수 디자인 패턴이 필요하므로 Ruby와 같은 동적 언어에 대한 편견이 강화됩니다.
Roy Tinker

11

당신이 가정하고있는 실제 문제가 아니라 BirdFly방법은? 왜 안되 겠어요 :

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

이제 명백한 문제는 다중 상속 ( Duck)이므로 실제로 필요한 것은 인터페이스입니다.

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}

3
문제는 진화가 Liskov 대체 원칙을 따르지 않으며 기능 제거와 상속을 수행한다는 것입니다.
Donal Fellows

7

먼저 그렇습니다. 객체 동적 수정을 쉽게 할 수있는 언어라면 그렇게 할 수 있습니다. 예를 들어, Ruby에서는 메소드를 쉽게 제거 할 수 있습니다.

그러나 Péter Török이 말했듯이 LSP를 위반할 것 입니다.


이 부분에서는 LSP를 잊어 버리고 다음과 같이 가정합니다.

  • Bird는 fly () 메소드가있는 클래스입니다
  • 펭귄은 새로부터 물려 받아야한다
  • 펭귄은 날 수 없다 ()
  • 나는 그것이 좋은 디자인인지 또는 실제 질문과 일치하는지는 신경 쓰지 않습니다.이 질문에 제공된 예입니다.

당신은 말했다 :

한편으로, 모든 속성 및 동작에 대해 존재 여부를 지정하는 플래그를 정의하고 해당 동작 또는 속성에 액세스하기 전에 매번 확인하지 않으려는 경우

그것은 당신이 원하는 것이 파이썬의 " 허가보다는 용서를 구하는 것 "입니다

펭귄에게 예외를 던지거나 예외를 발생시키는 NonFlyingBird 클래스에서 상속하십시오 (의사 코드).

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

그건 그렇고, 당신이 선택하는 것 : 예외 발생 또는 메소드 제거, 결국 다음 코드 (언어가 메소드 제거를 지원한다고 가정) :

var bird:Bird = new Penguin();
bird.fly();

런타임 예외가 발생합니다.


"펭귄이 예외를 던지도록하거나 예외를 던지는 NonFlyingBird 클래스에서 상속 받으십시오."여전히 LSP의 위반입니다. 펭귄의 비행이 실패하더라도 펭귄은 날 수 있다고 제안하고있다. 펭귄에는 비행 방법이 없어야합니다.
pdr

@ pdr : 그것은 펭귄 있다고 제안하는 것이 아니라 날아 가야 한다는 것입니다 (계약입니다). 예외는 할 수 없다는 것을 알려줍니다 . 그런데, 나는 단지 문제의 일부에 대한 답을주는거야, 그것이 좋은의 OOP의 방법입니다 주장하지는 않겠지
데이비드

요점은 펭귄이 새이기 때문에 날지 않아야한다는 것입니다. "x가 날 수 있다면 그렇게하세요. 그렇지 않으면 그렇게하세요"라는 코드를 작성하려면 나는 당신의 버전에서 try / catch를 사용해야하는데, 여기서 객체가 날 수 있는지 물어볼 수 있어야합니다 (캐스팅 또는 검사 방법이 존재합니다). 그것은 단지 말로 표현되어 있지만 귀하의 대답은 예외를 던지는 것이 LSP를 준수한다는 것을 암시합니다.
pdr

@pdr "귀하의 버전에서는 try / catch를 사용해야합니다."-> 그것은 허가보다는 용서를 요구하는 요점입니다. 나는 문구를 고칠 것이다.
David

"그것은 허가보다는 용서를 구하는 요점입니다." 예, 프레임 워크가 누락 된 메소드에 대해 동일한 유형의 예외를 throw 할 수 있다는 점을 제외하면 Python의 "try : except AttributeError :"는 C #의 "if (X는 Y) {} else {}"와 정확히 동일하며 즉시 인식 가능합니다. 따라서. 그러나 Bird에서 기본 fly () 기능을 재정의하기 위해 고의로 CannotFlyException을 발생시킨 경우 인식 할 수 없게됩니다.
pdr

7

누군가 위에서 언급 한 것처럼 펭귄은 새이며, 펭귄은 날지 않으며, 모든 새가 날 수있는 것은 아닙니다.

따라서 Bird.fly ()가 존재하지 않거나 작동하지 않아야합니다. 나는 전자를 선호합니다.

FlyingBird가 Bird를 확장하면 물론 .fly () 메소드가 있습니다.


Fly는 새 구현할 수있는 인터페이스 여야한다는 데 동의합니다 . 재정의 될 수있는 기본 동작과 함께 메서드로 구현 될 수 있지만보다 깔끔한 접근 방식은 인터페이스를 사용하고 있습니다.
Jon Raynor

6

fly () 예제의 실제 문제점은 조작의 입력 및 출력이 올바르게 정의되지 않았다는 것입니다. 새가 날기 위해서는 무엇이 필요합니까? 그리고 비행이 성공하면 어떻게됩니까? fly () 함수의 매개 변수 유형 및 리턴 유형에는 해당 정보가 있어야합니다. 그렇지 않으면 디자인이 임의의 부작용에 의존하고 어떤 일이 발생할 수 있습니다. 아무것도 부분은 인터페이스가 제대로 정의되지 않고 구현의 모든 종류의이 허용됩니다, 전체 문제를 일으키는 것입니다.

따라서이 대신 :

class Bird {
public:
   virtual void fly()=0;
};

다음과 같은 것이 있어야합니다.

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

이제는 기능의 한계를 명시 적으로 정의합니다. 비행 행동에는 위치를 지정할 때지면으로부터의 거리를 결정하기 위해 단일 부유물 만 있습니다. 이제 전체 문제가 자동으로 해결됩니다. 날 수없는 새는 그 함수에서 0.0 만 반환하며, 절대 땅을 떠나지 않습니다. 올바른 동작이며, 하나의 float가 결정되면 인터페이스를 완전히 구현 한 것입니다.

실제 동작은 유형으로 인코딩하기 어려울 수 있지만 인터페이스를 올바르게 지정하는 유일한 방법입니다.

편집 : 한 측면을 명확히하고 싶습니다. 이 float-> float 버전의 fly () 함수는 경로를 정의하기 때문에 중요합니다. 이 버전은 한 마리의 새가 날고있는 동안 마술처럼 스스로 복제 할 수 없음을 의미합니다. 이것이 매개 변수가 단일 부동 소수점 인 이유입니다. 새가 취하는 경로의 위치입니다. 더 복잡한 경로를 원하면 Point2d posinpath (float x); fly () 함수와 동일한 x를 사용합니다.


1
나는 당신의 대답을 아주 좋아합니다. 더 많은 표를받을 가치가 있다고 생각합니다.
Sebastien Diot

2
훌륭한 답변입니다. 문제는이 질문이 fly ()가 실제로하는 일에 대해 손을 흔들 었다는 것입니다. 파리의 실제 구현은,이 것 적어도, 대상 - 펭귄의 경우, 구현 오버라이드 (override) 할 수있는 비행 (목적지 좌표) {반환 currentPosition)}
크리스 Cudmore

4

기술적으로 거의 모든 동적 / 덕 유형 언어 (JavaScript, Ruby, Lua 등) 로이 작업을 수행 할 수 있지만 거의 항상 나쁜 생각입니다. 클래스에서 메소드를 제거하는 것은 전역 변수를 사용하는 것과 유사한 유지 보수의 악몽입니다 (즉, 하나의 모듈에서 전역 상태가 다른 곳에서 수정되지 않았다는 것을 알 수 없습니다).

설명한 문제에 대한 좋은 패턴은 데코레이터 또는 전략이며 구성 요소 아키텍처를 설계합니다. 기본적으로 서브 클래스에서 불필요한 동작을 제거하는 대신 필요한 동작을 추가하여 오브젝트를 빌드합니다. 따라서 대부분의 새를 만들려면 플라잉 구성 요소를 추가하지만 펭귄에 해당 구성 요소를 추가하지 마십시오.


3

Peter는 Liskov 대체 원칙에 대해 언급했지만 설명이 필요하다고 생각합니다.

q (x)를 유형 T의 객체 x에 대해 증명할 수있는 속성으로하자. 그러면 S가 T의 하위 유형 인 경우 유형 S의 객체 y에 대해 q (y)를 입증 할 수 있어야합니다.

따라서 조류 (T 유형의 객체 x)가 날아갈 수 있으면 (q (x)) 펭귄 (S 유형의 객체 y)은 정의에 따라 날아갈 수 있습니다 (q (y)). 그러나 그것은 사실이 아닙니다. 날 수는 있지만 조류 유형이 아닌 다른 생물도 있습니다.

이것을 다루는 방법은 언어에 따라 다릅니다. 언어가 다중 상속을 지원하는 경우 날 수있는 생물에 대해 추상 클래스를 사용해야합니다. 언어가 인터페이스를 선호한다면 이것이 해결책입니다 (그리고 비행의 구현은 상속되는 것이 아니라 캡슐화되어야합니다). 또는 언어가 Duck Typing을 지원하는 경우 (말장난 의도 없음) 해당 클래스에 대해 fly 메소드를 구현하고 해당 클래스가있는 경우이를 호출 할 수 있습니다.

그러나 수퍼 클래스의 모든 속성은 모든 서브 클래스에 적용되어야합니다.

[편집에 대한 응답으로]

CanFly의 "특성"을 Bird에 적용하는 것은 좋지 않습니다. 여전히 모든 조류가 날 수있는 코드를 호출 할 것을 제안하고 있습니다.

귀하가 정의한 용어의 특성은 Liskov가 "재산"이라고 말했을 때의 의미와 정확히 같습니다.


2

왜 그렇게하지 말아야하는지 설명하는 Liskov 대체 원칙을 다른 사람들과 마찬가지로 언급하면서 시작하겠습니다. 그러나해야 할 문제는 디자인 중 하나입니다. 어떤 경우에는 펭귄이 실제로 날지 못하는 것이 중요하지 않을 수 있습니다. Bird :: fly ()의 문서에 명확하지 않는 한, 날지 못하는 새들에게 던질 수있는 펭귄이 날아갈 때 InsufficientWingsException을 던질 수 있습니다. 인터페이스가 팽창하지만 실제로 날 수 있는지 테스트하는 테스트가 있습니다.

대안은 수업을 재구성하는 것입니다. "FlyingCreature"클래스 (또는 허용 언어를 다루는 경우 더 나은 인터페이스)를 만들어 봅시다. "새"는 FlyingCreature에서 상속받지 않지만 "FlyingBird"를 만들 수 있습니다. Lark, Vulture 및 Eagle은 모두 FlyingBird에서 상속받습니다. 펭귄은 그렇지 않습니다. 그것은 단지 Bird에서 상속받습니다.

순진한 구조보다 조금 더 복잡하지만 정확하다는 장점이 있습니다. 예상되는 모든 클래스가 있으며 (Bird) 생물이 날 수 있는지 여부가 중요하지 않은 경우 사용자는 일반적으로 '발명 된'클래스 (FlyingCreature)를 무시할 수 있습니다.


0

이러한 상황을 처리하는 일반적인 방법은 UnsupportedOperationException(Java) resp 와 같은 것을 던지는 것 입니다. NotImplementedException(기음#).


Bird에서이 가능성을 문서화하는 한.
DJClayworth

0

많은 의견을 가진 많은 좋은 답변이지만 모두 동의하지는 않으며 하나만 선택할 수 있으므로 여기에 동의하는 모든 견해를 요약하겠습니다.

0) "정적 입력"을 가정하지 마십시오 (Java를 거의 독점적으로 수행하기 때문에 요청했을 때 수행했습니다). 기본적으로 문제는 사용하는 언어의 유형에 따라 매우 다릅니다.

1) 타입 계층 구조는 디자인과 머리 부분의 코드 재사용 계층 구조와 대부분 중복되는 경우에도 분리해야합니다. 일반적으로 재사용에는 클래스를 사용하고 유형에는 인터페이스를 사용하십시오.

2) 일반적으로 Bird IS-A Fly의 이유는 대부분의 조류가 날 수 있기 때문에 코드 재사용 관점에서 실용적이지만 Bird IS-A Fly는 적어도 하나의 예외가 있기 때문에 실제로 잘못되었다고 말하는 것입니다. (펭귄).

3) 정적 언어와 동적 언어 모두에서 예외를 던질 수 있습니다. 그러나 이것은 기능을 선언하는 클래스 / 인터페이스의 "계약"에서 명시 적으로 선언 된 경우에만 사용해야하며, 그렇지 않으면 "계약 위반"입니다. 또한 이제 모든 곳에서 예외를 포착 할 수 있도록 준비해야하므로 콜 사이트에서 더 많은 코드를 작성하면 못생긴 코드입니다.

4) 일부 동적 언어에서는 실제로 수퍼 클래스의 기능을 "제거 / 숨기기"할 수 있습니다. 기능이 있는지 확인하는 것이 해당 언어로 "IS-A"를 확인하는 방법이면 적절하고 합리적인 솔루션입니다. 반면에 "IS-A"작업이 여전히 개체가 "현재 없어진"기능을 구현해야한다고 말하는 호출 코드 인 경우 호출 코드는 해당 기능이 존재한다고 가정하고 호출하여 충돌을 일으 킵니다. 예외를 던지는 정도입니다.

5) 더 좋은 대안은 플라이 특성과 조류 특성을 실제로 분리하는 것입니다. 따라서 날아 다니는 새는 새와 비행 / 비행을 명시 적으로 확장 / 구현해야합니다. 아무것도 "제거"할 필요가 없기 때문에 아마도 가장 깨끗한 디자인 일 것입니다. 한 가지 단점은 이제 거의 모든 새가 Bird와 Fly를 모두 구현해야하므로 더 많은 코드를 작성해야한다는 것입니다. 이 문제를 해결하는 방법은 Bird와 Fly를 모두 구현하고 일반적인 경우를 나타내는 FlyingBird 중개 클래스를 사용하는 것이지만이 해결 방법은 다중 상속없이 사용이 제한 될 수 있습니다.

6) 다중 상속이 필요없는 또 다른 대안은 상속 대신 구성을 사용하는 것입니다. 동물의 각 측면은 독립적 인 클래스로 모델링되며, 구체적인 Bird는 Bird의 구성이며, 아마도 Fly 또는 Swim 일 수 있습니다. 전체 코드를 재사용 할 수는 있지만 하나 이상의 추가 단계를 수행해야합니다. 구체적인 Bird에 대한 참조가있을 때 Flying 기능. 또한 자연 언어 "object IS-A Fly"및 "object AS-A (cast) Fly"는 더 이상 작동하지 않으므로 구문을 직접 만들어야합니다 (일부 동적 언어에는이 방법이있을 수 있음). 코드가 더 번거로워 질 수 있습니다.

7) 날 수없는 무언가를위한 명확한 방법을 제공하도록 비행 특성을 정의하십시오. Fly.getNumberOfWings ()는 0을 반환 할 수 있습니다. Fly.fly (direction, currentPotinion)이 비행 후 새 위치를 반환해야하는 경우 Penguin.fly ()는 currentPosition을 변경하지 않고 그냥 반환 할 수 있습니다. 기술적으로 작동하는 코드로 끝날 수 있지만 몇 가지주의 사항이 있습니다. 첫째, 일부 코드에는 명백한 "아무것도하지 않는"동작이 없을 수 있습니다. 또한 누군가가 x.fly ()를 호출 하면 댓글에 fly ()이 아무것도수 없다고 말하더라도 무언가 를 기대할 수 있습니다. 마지막으로 펭귄 IS-A Flying은 여전히 ​​true를 반환하므로 프로그래머에게는 혼란 스러울 수 있습니다.

8) 5)로 수행하지만 다중 상속이 필요한 경우를 피하려면 구성을 사용하십시오. 이것은 정적 언어에 대해 선호하는 옵션입니다 .6) 더 성가신 것처럼 보입니다 (더 많은 객체가 있기 때문에 더 많은 메모리가 필요할 수 있습니다). 역동적 인 언어는 6) 성가신 일을 덜 만들지 만 5)보다 번거롭지 않을 것입니다.


0

기본 클래스에서 기본 동작 (가상으로 표시)을 정의하고 필요에 따라 재정의하십시오. 그렇게하면 모든 새가 날 수 있습니다.

심지어 펭귄도 제로 고도에서 얼음을 가로 질러 활공합니다!

필요에 따라 비행 동작을 무시할 수 있습니다.

다른 가능성은 플라이 인터페이스입니다. 모든 조류가 해당 인터페이스를 구현하지는 않습니다.

class eagle : bird, IFly
class penguin : bird

속성은 제거 할 수 없으므로 모든 조류에서 공통적 인 속성을 아는 것이 중요합니다. 공통 속성이 기본 수준에서 구현되도록하는 것이 디자인 문제라고 생각합니다.


-1

나는 당신이 찾고있는 패턴이 좋은 오래된 다형성이라고 생각합니다. 일부 언어의 클래스에서 인터페이스를 제거 할 수는 있지만 Péter Török가 제공 한 이유는 좋지 않습니다. 그러나 모든 OO 언어에서 동작 변경 방법을 재정의 할 수 있으며 여기에는 아무 것도하지 않습니다. 예제를 빌리려면 다음 중 하나를 수행하는 Penguin :: fly () 메소드를 제공하십시오.

  • 아무것도
  • 예외를 던지다
  • 대신 펭귄 :: swim () 메소드를 호출합니다.
  • 펭귄이 수중에 있다고 주장한다 (물을 통해“비행”을한다)

미리 계획하면 속성을 쉽게 추가하고 제거 할 수 있습니다. 인스턴스 변수를 사용하는 대신 맵 / 사전 / 연관 배열에 속성을 저장할 수 있습니다. Factory 패턴을 사용하여 이러한 구조의 표준 인스턴스를 생성 할 수 있으므로 BirdFactory에서 오는 Bird는 항상 동일한 속성 세트로 시작합니다. Objective-C의 Key Value Coding은 이러한 종류의 좋은 예입니다.

참고 : 아래 의견의 진지한 교훈은 행동을 제거하기 위해 재정의하는 것이 효과적 일 수 있지만 항상 최선의 해결책은 아니라는 것입니다. 중요한 방법 으로이 작업을 수행 해야하는 경우 상속 그래프에 결함이 있음을 나타내는 강력한 신호를 고려해야합니다. 상속받은 클래스를 리팩토링하는 것이 항상 가능한 것은 아니지만 그것이 더 좋은 해결책 인 경우가 종종 있습니다.

펭귄 예제를 사용하면 리팩터링하는 한 가지 방법은 비행 능력을 Bird 클래스와 분리하는 것입니다. Bird의 fly () 메소드를 포함하여 모든 조류가 날 수있는 것은 아니기 때문에, 여러분이 요구하는 종류의 문제로 직접 이어집니다. 따라서 fly () 메서드 (및 takeoff () 및 land ())를 Aviator 클래스 또는 인터페이스 (언어에 따라 다름)로 이동하십시오. 이를 통해 Bird와 Aviator에서 상속하거나 Bird에서 상속하여 Aviator를 구현하는 FlyingBird 클래스를 만들 수 있습니다. 펭귄은 조류로부터 직접 상속을받을 수 있지만 비행가는 아니므로 문제를 피할 수 있습니다. 이러한 배열을 통해 FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect 등과 같은 다른 비행 물에 대한 클래스를 쉽게 만들 수도 있습니다.


2
Penguin :: swim () 호출을 제안하는 경우에도 -1입니다. 이는 가장 놀랍게도 되는 원칙에 위배되며, 유지 보수 프로그래머가 귀하의 이름을 저주하게 할 것입니다.
DJClayworth

1
@DJClayworth 예제가 처음에 우스운면에 있었기 때문에, fly () 및 swim ()의 유추 된 행동 위반에 대한 하향 조정은 약간 많이 보입니다. 그러나 이것을 진지하게보고 싶다면 다른 방법으로 가서 fly () 관점에서 swim ()을 구현할 가능성이 더 높다는 데 동의합니다. 오리는 발을 노려서 헤엄칩니다. 펭귄은 날개를 펄럭이며 수영을합니다.
Caleb

1
나는 그 질문이 어리 석다는 것에 동의하지만, 사람들이 실제 상황에서 이것을하는 것을 본 문제는 희귀 한 기능을 구현하기 위해 "실제로 아무것도하지 않는"기존 호출을 사용한다는 점이다. 실제로 코드를 망쳐 놓고 보통 "if (! (myBird instanceof Penguin)) fly ();" 많은 곳에서 아무도 타조 수업을 만들지 않기를 바라고 있습니다.
DJClayworth

주장은 더 나빠 flys () 메서드가있는 Birds 배열이있는 경우 fly ()를 호출 할 때 어설 션 오류를 원하지 않습니다.
DJClayworth

1
나는 펭귄 문서를 읽지 못했습니다 . 왜냐하면 나는 많은 새들을 받았고 펭귄 이 그 배열에 있는지 알지 못했습니다 . 나는 fly ()를 호출하면 새가 날아 갔다고 말하는 Bird 문서를 읽었습니다 . 그 문서가 새가 날지 못하는 경우 예외가 발생할 수 있다고 분명히 언급했다면, 나는 그것을 허용했을 것입니다. fly () 호출로 가끔 수영을했다고 말하면 다른 클래스 라이브러리를 사용하기로 변경했을 것입니다. 아니면 매우 큰 음료를 마시 러 갔다.
DJClayworth
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.