クラスの階層でオブジェクトの動作やプロパティを「削除」できるようにする言語やデザインパターンはありますか?


28

従来のクラス階層のよく知られた欠点は、実世界のモデリングに関しては悪いことです。例として、動物の種をクラスで表現しようとしています。実際にそれを行うといくつかの問題がありますが、解決策を見たことがないのは、サブクラスが、ペンギンが飛べないようなスーパークラスで定義された動作またはプロパティを「失う」場合ですおそらくより良い例ですが、それが私の頭に浮かぶ最初の例です)。

一方では、すべてのプロパティと動作について、それが存在するかどうかを指定するフラグを定義し、その動作またはプロパティにアクセスする前に毎回チェックする必要はありません。Birdクラスでは、鳥が簡単かつ明確に飛ぶことができると言いたいだけです。しかし、その後、どこか恐ろしいハックを使用することなく、「例外」を後で定義できればいいと思います。これは、システムがしばらくの間生産的だったときによく起こります。突然、元の設計にまったく適合しない「例外」が見つかり、それに対応するためにコードの大部分を変更する必要はありません。

それでは、「スーパークラス」とそれを使用するすべてのコードに大きな変更を加えることなく、この問題をきれいに処理できる言語または設計パターンがありますか?ソリューションが特定のケースのみを処理する場合でも、いくつかのソリューションが一緒になって完全な戦略を形成する場合があります。

もっと考えた後、リスコフの代替原則を忘れていたことに気付きました。それができない理由です。すべての主要な「機能グループ」に対して「特性/インターフェース」を定義すると仮定すると、階層のさまざまなブランチに特性を自由に実装できます。たとえば、Flying特性はBirds、いくつかの特殊なリスや魚によって実装できます。

したがって、私の質問は「どのようにして形質を実装しないのですか?」スーパークラスがJava Serializableである場合、たとえば「ソケット」が含まれている場合など、状態をシリアル化する方法がなくても、1つでなければなりません。

それを行う1つの方法は、最初から常にペアですべての特性を定義することです:FlyingとNotFlying(チェックしないとUnsupportedOperationExceptionがスローされます)。Not-traitは新しいインターフェースを定義せず、単純にチェックすることができます。特に最初から使用する場合、「安い」ソリューションのように聞こえます。


3
「どこか恐ろしいハックを使用する必要function save_yourself_from_crashing_airplane(Bird b) { f.fly() }はありません」:動作を無効にすることは恐ろしいハックです。それはそれがはるかに複雑になることを意味します。(ピーターTörökが言ったように、それはLSPに違反しています)
ケプラ

戦略パターンと継承の組み合わせにより、特定のスーパータイプの継承された動作を「構成」することができますか?あなたが言うとき、あなたは" it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"ハッキング行動を制御するファクトリーメソッドを検討しますか?
StuperUser

1
もちろん、単にNotSupportedExceptionfromをスローできPenguin.fly()ます。
フェリックスドンベック

言語に関する限り、子クラスのメソッドを未実装にすることができます。例えば、Rubyで:class Penguin < Bird; undef fly; end;。あなたがすべきかどうかは別の質問です。
ネイサンロング

これは、リスコフの原理を破り、間違いなくOOPの全ポイントを破壊するでしょう。
deadalnix

回答:


17

他の人が述べたように、あなたはLSPに反対しなければなりません。

ただし、サブクラスは単にスーパークラスの任意の拡張であると考えることができます。それ自体が新しいオブジェクトであり、スーパークラスとの唯一の関係は、基盤を使用していることです。

これ、ペンギン鳥だと言うよりも、理にかなっています。あなたの格言ペンギンは、Birdから行動の一部を継承します。

一般に、動的言語ではこれを簡単に表現できます。JavaScriptを使用した例を以下に示します。

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

この特定のケースでPenguinは、オブジェクトに値をBird.fly持つflyプロパティを書き込むことで、継承するメソッドをアクティブにシャドウイングundefinedしています。

今、あなたはそれPenguinBirdもう正常として扱うことができないと言うかもしれません。しかし、前述のように、現実の世界ではそれは不可能です。なぜなら私たちは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である場合、たとえば「ソケット」が含まれている場合など、状態をシリアル化する方法がなくても、1つでなければなりません。

特性を「実装しない」ことはありません。継承の階層を修正するだけです。スーパークラス契約を履行できるか、そのタイプのふりをしてはいけません。

これは、オブジェクトの構成が輝く場所です。

余談ですが、Serializableはすべてをシリアル化することを意味するのではなく、「気になる状態」をシリアル化することを意味するだけです。

「NotX」特性を使用しないでください。それはただひどいコードの膨張です。関数が飛んでいるオブジェクトを期待している場合、マンモスを与えるとクラッシュして燃えます。


10
「現実の世界ではそれは不可能です。」はい、できます。ペンギンは鳥です。飛ぶ能力は​​鳥の特性ではなく、鳥のほとんどの種の単なる偶然の特性です。鳥を定義するプロパティは、「羽毛、翼、二足、吸熱、産卵、脊椎動物」(ウィキペディア)-そこを飛ぶことについては何もありません。
pdr

2
再び@pdrは、あなたの鳥の定義に依存します。「鳥」という用語を使用していたため、フライメソッドを含む鳥を表すために使用するクラスの抽象化を意味していました。また、クラスの抽象化をより具体的にしないこともできると述べました。また、ペンギンは羽毛がありません。
レイノス

2
@Raynos:ペンギンは本当に羽が生えています。もちろん、彼らの羽は非常に短く、密です。
ジョンパーディ

@JonPurdy結構、私はいつも彼らが毛皮を持っていると想像しています。
レイノス

+1、特に「マンモス」の場合。笑!
セバスチャン・ディオ

28

私の知る限り、すべての継承ベースの言語は、リスコフ置換原理に基づいています。サブクラスの基本クラスプロパティを削除または無効にすると、LSPに明らかに違反するため、このような可能性はどこにも実装されていないと思います。実世界は確かに乱雑であり、数学的な抽象化によって正確にモデル化することはできません。

一部の言語は、より柔軟な方法でそのような問題に正確に対処するために、特性またはミックスインを提供します。


1
LSPはクラス用ではなく、タイプ用です。
ヨルグWミットタグ

2
@PéterTörök:そうでなければこの質問は存在しません:-) Rubyの2つの例を考えることができます。ClassサブクラスであるModuleにもかかわらず、ClassIS-NOT-AはModule。ただし、多くのコードを再利用するため、サブクラスであることが依然として理にかなっています。OTOH、StringIOIS-A IOですが、2つはObjectコードを共有しないため、継承関係はありません(もちろん、両方から継承することは別ですが)。クラスはコード共有用で、タイプはプロトコルの説明用です。IOそしてStringIO、同じプロトコル、したがって同じタイプを持っていますが、それらのクラスは無関係です。
ヨルグWミットタグ

1
@JörgWMittag、わかりました、今あなたの言うことをよりよく理解します。しかし、私にとってあなたの最初の例は、あなたが示唆するいくつかの根本的な問題の表現よりも、継承の誤用のように聞こえます。パブリック継承IMOは、サブタイプの関係(is-a)を表現するためだけに、実装を再利用するために使用しないでください。そして、それが悪用される可能性があるという事実は、それを失格にしない-私は、悪用されないドメインから使用可能なツールを想像することはできません。
ペテルトレック

2
この答えを支持する人々に:これは、特に編集された説明の後では、実際に質問に答えないことに注意してください。彼の言うことは非常に真実であり、知ることが重要だからです。しかし、彼は本当に質問に答えなかったのです。
ジョッキング

1
インターフェースのみがタイプであり、クラスがそうではなく、サブクラスがスーパークラスのインターフェースを「実装しない」ことができるJavaを想像してください。おおまかな考えがあると思います。
ヨルグWミットタグ

15

Fly():で最初の例であるヘッドファーストデザインパターンのための戦略パターン、これはあなたがすべき理由として良い状況である「相続オーバー好意組成物。」

ファクトリの適切な動作がインジェクトされ、関連するサブタイプが自動的に取得されるなど、実際に特定のものがファクトリによって処理されるのはもちろんFlyingBird、スーパータイプを持つことにより、構成と継承を混在させることができます。FlightlessBirdPenguin : FlightlessBird


1
回答でデコレーターパターンについて言及しましたが、戦略パターンも非常にうまく機能します。
ジョッキング

1
「継承よりも合成を優先する」ための+1。ただし、静的に型付けされた言語で構成を実装するための特別なデザインパターンの必要性は、Rubyのような動的言語に対する私のバイアスを強化します。
ロイティンカー

11

あなたがメソッドをBird持っていると仮定している本当の問題ではありませんFlyか?何故なの:

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 Substitution Principleに従っておらず、機能の削除を伴う継承を行っていることです。
ドナルドフェローズ

7

まず、はい、オブジェクトの動的な変更を簡単に行える言語であれば、それが可能になります。たとえば、Rubyでは、メソッドを簡単に削除できます。

しかし、PéterTörökが言ったように、それはLSPに違反するでしょう


この部分では、LSPを忘れて、次のように仮定します。

  • 鳥はfly()メソッドを持つクラスです
  • ペンギンは鳥から継承する必要があります
  • ペンギンは飛べない()
  • この質問で提供されている例であるため、それが良いデザインであるか、実際の世界と一致するかどうかは気にしません。

あなたが言った :

一方では、すべてのプロパティと動作に対して、それが存在するかどうかを指定するフラグを定義し、その動作またはプロパティにアクセスする前に毎回チェックしたくない

それは「あなたがPythonのです欲しいもののように見えるというより許可許しを求めて

ペンギンに例外をスローさせるか、例外(擬似コード)をスローする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を使用する必要があります」->それは許可ではなく許しを求めることの重要なポイントです(アヒルでさえ翼を壊して飛ぶことができないためです)。文言を修正します。
デビッド

「それは許可ではなく、許しを求めることの重要なポイントです。」はい、フレームワークが欠落しているメソッドに対して同じタイプの例外をスローできることを除いて、Pythonの「try:except AttributeError:」はC#の「if(X is Y){} else {}」とまったく同じで、すぐに認識可能です。など。しかし、故意にBirdのデフォルトのfly()機能をオーバーライドするためにCannotFlyExceptionをスローした場合、認識できなくなります。
pdr

7

上記のコメントで誰かが指摘したように、ペンギンは鳥であり、ペンギンは飛ばない、すべての鳥が飛べないわけではない。

したがって、Bird.fly()は存在しないか、機能しないようにする必要があります。前者が好きです。

FlyingBirdにBirdを拡張させると、もちろん.fly()メソッドが正しくなります。


私は同意します、Flyは鳥実装できるインターフェースであるべきです。デフォルトの動作をオーバーライドできるメソッドとして実装することもできますが、よりクリーンなアプローチはインターフェースを使用することです。
ジョンレイナー

6

fly()の例の実際の問題は、操作の入力と出力が適切に定義されていないことです。鳥が飛ぶには何が必要ですか?そして、飛行が成功した後はどうなりますか?fly()関数のパラメータータイプと戻り値タイプには、その情報が必要です。それ以外の場合、設計はランダムな副作用に依存し、が起こるかわかりません。何の一部は、インタフェースが適切に定義されていないと実装のすべての種類が許可され、全体の問題を引き起こすものです。

したがって、これの代わりに:

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

次のようなものが必要です。

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

これで、機能の制限を明確に定義します-飛行動作には、決定するフロートが1つしかありません-位置が与えられたときの地面からの距離です。これで、問題全体が自動的に解決されます。飛べない鳥は、その関数から0.0を返すだけで、地面を離れることはありません。それは正しい動作であり、1つのフロートが決定されると、インターフェイスを完全に実装したことがわかります。

実際の動作を型にエンコードするのは難しい場合がありますが、それがインターフェイスを適切に指定する唯一の方法です。

編集:1つの側面を明確にします。fly()関数のこのfloat-> floatバージョンは、パスを定義するためにも重要です。このバージョンは、飛行中に1羽の鳥が魔法のように自分自身を複製できないことを意味します。これが、パラメーターが単一のフロートである理由です-これは、鳥がとるパス内の位置です。より複雑なパスが必要な場合は、Point2d posinpath(float x); fly()関数と同じxを使用します。


1
私はあなたの答えがとても好きです。もっと投票する価値があると思う。
セバスチャン・ディオ

2
素晴らしい答え。問題は、fly()が実際に何をするのかという質問に手を振るだけです。flyの実際の実装には、少なくとも宛先があります。ペンギンの場合、{return currentPosition)を実装するためにオーバーライドできるfly(Coordinate destination)
Chris Cudmore

4

技術的には、ほぼすべての動的/アヒル型言語(JavaScript、Ruby、Luaなど)でこれを行うことができますが、ほとんどの場合、これは本当に悪い考えです。クラスからメソッドを削除することは、グローバル変数を使用することに似たメンテナンスの悪夢です(つまり、あるモジュールでは、グローバル状態が他の場所で変更されていないことを知ることはできません)。

説明した問題に適したパターンは、コンポーネントアーキテクチャを設計するデコレーターまたは戦略です。基本的に、サブクラスから不要な動作を削除するのではなく、必要な動作を追加してオブジェクトを構築します。そのため、ほとんどの鳥を作るには、飛ぶコンポーネントを追加しますが、そのコンポーネントをペンギンに追加しないでください。


3

PeterはLiskov Substitution Principleに言及しましたが、説明が必要だと感じています。

q(x)をT型のオブジェクトxについて証明可能なプロパティとします。次に、SがTのサブタイプであるS型のオブジェクトyに対してq(y)を証明できるようにします。

したがって、鳥(T型のオブジェクトx)が飛ぶことができる場合(q(x))、定義により、ペンギン(S型のオブジェクトy)は飛ぶことができます(q(y))。しかし、明らかにそうではありません。飛べることはできるが、鳥型ではないクリーチャーもいます。

これに対処する方法は、言語によって異なります。言語が多重継承をサポートしている場合、飛行可能なクリーチャーには抽象クラスを使用する必要があります。言語がインターフェースを好む場合、それが解決策です(そしてflyの実装は継承されるのではなくカプセル化されるべきです); または、言語がDuck Typingをサポートしている場合(しゃれは意図していません)、それらのクラスにflyメソッドを実装することができます。

ただし、スーパークラスのすべてのプロパティは、そのすべてのサブクラスに適用する必要があります。

[編集への応答]

CanFlyの「特性」をBirdに適用するのは良くありません。すべての鳥が飛ぶことができることをコードを呼び出すことをまだ示唆しています。

あなたが定義した用語の特徴は、リスコフが「財産」と言ったときのまさにその意味です。


2

まず、(他のすべての人と同様に)リスコフの代替原則に言及してみましょう。これは、なぜそうしないのかを説明しています。ただし、何をすべきかという問題は設計の問題です。場合によっては、ペンギンが実際に飛べないことは重要ではないかもしれません。Bird :: fly()のドキュメントで、飛べない鳥にそれをスローすることが明確である限り、飛ぶように求められたときにペンギンにInsufficientWingsExceptionをスローさせることができます。それが本当に飛ぶことができるかどうかを確認するテストがありますが、それはインターフェースを肥大化させます。

別の方法は、クラスを再構築することです。クラス「FlyingCreature」を作成してみましょう(それを許可する言語を扱っている場合は、より良いインターフェース)。「鳥」はFlyingCreatureから継承しませんが、継承する「FlyingBird」を作成できます。ヒバリ、ハゲタカ、ワシはすべてFlyingBirdから継承します。ペンギンはしません。それはただBirdを継承しています。

単純な構造よりも少し複雑ですが、正確であるという利点があります。期待されるすべてのクラスがあり(鳥)、クリーチャーが飛ぶことができるかどうかが重要でない場合、ユーザーは通常、「発明された」クラス(FlyingCreature)を無視できます。


0

このような状況を処理する一般的な方法は、UnsupportedOperationException(Java)応答のようなものをスローすることです。NotImplementedException(C#)。


この可能性をBirdで文書化する限り。
DJClayworth

0

多くの良い答えと多くのコメントがありますが、それらはすべて同意するものではありません。1つだけ選択できるので、ここで同意するすべての意見を要約します。

0)「静的タイピング」を想定しないでください(Javaをほぼ独占的に行うため、私が尋ねたときに行いました)。基本的に、問題は使用する言語のタイプに大きく依存します。

1)デザインと頭の中で、タイプ階層をコード再利用階層から分離する必要があります(ほとんどが重複している場合でも)。通常、再利用にはクラスを使用し、型にはインターフェイスを使用します。

2)通常、バードIS-Aフライはほとんどの鳥が飛ぶことができるため、コード再利用の観点から実用的ですが、バードIS-Aフライは少なくとも1つの例外があるため実際には間違っていると言っています(ペンギン)。

3)静的言語と動的言語の両方で、例外をスローできます。ただし、これは、機能を宣言するクラス/インターフェイスの「契約」で明示的に宣言されている場合にのみ使用する必要があります。そうでない場合は、「契約違反」です。また、例外をどこでもキャッチできるように準備する必要があるため、呼び出しサイトでより多くのコードを記述しますが、それは見苦しいコードです。

4)一部の動的言語では、スーパークラスの機能を「削除/非表示」することが実際に可能です。機能の存在を確認することが、その言語で「IS-A」を確認する方法である場合、これは適切で賢明なソリューションです。一方、「IS-A」操作が、オブジェクトに現在欠落している機能を実装する必要があることを示す別の何かである場合、呼び出し元のコードは機能が存在すると想定して呼び出し、クラッシュするため、例外をスローする量の種類。

5)より良い代替策は、実際にFly特性をBird特性から分離することです。そのため、飛ぶ鳥は、BirdとFly / Flyingの両方を明示的に拡張/実装する必要があります。何も「削除」する必要がないため、これはおそらく最もクリーンなデザインです。1つの欠点は、ほとんどすべての鳥がBirdとFlyの両方を実装する必要があるため、より多くのコードを記述することです。これを回避する方法は、BirdとFlyの両方を実装し、一般的なケースを表す中間クラスFlyingBirdを使用することですが、この回避策は多重継承なしで使用が制限される場合があります。

6)多重継承を必要としない別の選択肢は、継承の代わりに構成を使用することです。動物の各側面は独立したクラスによってモデル化され、具体的な鳥は鳥の構成であり、場合によってはフライまたはスイムなどです...完全なコード再利用を取得しますが、取得するには1つ以上の追加手順を実行する必要があります具体的な鳥の参照がある場合の飛行機能。また、言語の自然な「オブジェクトIS-Aフライ」と「オブジェクトAS-A(キャスト)フライ」は機能しなくなるため、独自の構文を発明する必要があります(動的言語にはこれを回避する方法があります)。これにより、コードがより面倒になる場合があります。

7)飛ぶことができないものに対して明確な方法を提供するように、飛ぶ特性を定義します。Fly.getNumberOfWings()は0を返す可能性があります。Fly.fly(direction、currentPotinion)がフライト後に新しい位置を返す必要がある場合、Penguin.fly()はそれを変更せずにcurrentPositionを返すだけです。技術的に機能するコードで終わるかもしれませんが、いくつかの警告があります。まず、一部のコードには明らかな「何もしない」動作がない場合があります。また、誰かがx.fly()を呼び出すと、コメントでfly()が何もしないことが許可されいる場合でも、何か行うことが期待されます。最後に、ペンギンIS-Aフライングは依然としてtrueを返しますが、これはプログラマにとって混乱を招く可能性があります。

8)5)として行いますが、構成を使用して、多重継承を必要とするケースを回避します。これは、静的言語に好まれるオプションです。6)より面倒だと思われます(オブジェクトが多いため、おそらくより多くのメモリが必要です)。動的言語は6)面倒を少なくするかもしれませんが、5)ほど面倒ではなくなると思います。


0

基本クラスでデフォルトの動作を定義し(仮想としてマーク)、必要に応じてオーバーライドします。そうすれば、すべての鳥が「飛ぶ」ことができます。

ペンギンでさえ、氷上をゼロ高度で滑空します!

飛行の動作は、必要に応じてオーバーライドできます。

別の可能性は、フライインターフェイスを使用することです。すべての鳥がそのインターフェースを実装するわけではありません。

class eagle : bird, IFly
class penguin : bird

プロパティは削除できないため、すべての鳥に共通するプロパティを知ることが重要です。基本レベルで共通のプロパティが実装されるようにすることは、設計上の問題だと思います。


-1

探しているパターンは古き良き多型だと思います。一部の言語ではクラスからインターフェイスを削除できる場合がありますが、PéterTörökが示した理由から、おそらく良い考えではありません。ただし、どのOO言語でも、メソッドをオーバーライドしてその動作を変更できます。これには何もしないことが含まれます。あなたの例を借りるには、次のいずれかを行うPenguin :: fly()メソッドを提供します。

  • 何もない
  • 例外をスローします
  • 代わりにPenguin :: swim()メソッドを呼び出します
  • ペンギンは水中にいると断言する

事前に計画しておくと、プロパティの追加と削除が少し簡単になります。インスタンス変数を使用する代わりに、マップ/辞書/連想配列にプロパティを保存できます。Factoryパターンを使用して、このような構造の標準インスタンスを作成できます。そのため、BirdFactoryからのBirdは、常に同じプロパティセットで開始されます。Objective-CのKey Value Codingは、この種の良い例です。

注:以下のコメントからの重大な教訓は、動作を削除するためにオーバーライドすることは機能しますが、常に最良のソリューションとは限らないということです。重要な方法でこれを行う必要がある場合は、継承グラフに欠陥があるという強力なシグナルを考慮する必要があります。継承するクラスを常にリファクタリングできるとは限りませんが、それが適切な場合は、より良い解決策であることがよくあります。

ペンギンの例を使用して、リファクタリングする1つの方法は、飛行能力をBirdクラスから分離することです。すべての鳥が飛べるわけではないので、Birdのfly()メソッドは不適切であり、あなたが尋ねている種類の問題に直接つながります。したがって、fly()メソッド(およびおそらくtakeoff()およびland())をAviatorクラスまたはインターフェイス(言語に応じて)に移動します。これにより、BirdとAviatorの両方から継承する(またはBirdから継承してAviatorを実装する)FlyingBirdクラスを作成できます。ペンギンは引き続き鳥から継承できますが、アビエイターからは継承できないため、問題を回避できます。このような配置により、FlyingFish、FlyingMammal、FlyingMachine、AnnoyingInsectなど、他の飛行物のクラスを簡単に作成できる場合もあります。


2
-1は、Penguin :: swim()の呼び出しを提案することさえします。それは最小限の驚き原則に違反し、どこでも保守プログラマーがあなたの名前を呪う原因になります。
DJClayworth

1
@DJClayworth最初の例はばかげた側面にあったので、fly()とswim()の推測された動作の違反に対するダウン投票は少々多いようです。しかし、あなたが本当にこれを真剣に見たいなら、私はあなたが反対に行き、fly()の観点からswim()を実装する可能性が高いことに同意するでしょう。アヒルは足をpadいで泳ぎます。ペンギンは羽ばたいて泳ぎます。
カレブ

1
質問はばかげていることに同意しますが、問題は実際に人々がこれを行うのを見たことです-珍しい機能を実装するために「本当に何もしない」既存の呼び出しを使用してください。それは本当にコードを台無しにして、通常「if(!(myBird instanceof Penguin))fly();」と書く必要があります。多くの場所で、だれもダチョウのクラスを作らないことを望んでいます。
DJClayworth

アサートはさらに悪いです。すべてのfly()メソッドを持つBirdsの配列がある場合、それらに対してfly()を呼び出すときにアサーションエラーが発生するのは望ましくありません。
DJClayworth

1
ペンギンのドキュメントを読んでいませんでした。鳥の配列を渡されたので、ペンギンが配列内にいることを知りませんでした。私は、fly()を呼び出すと鳥が飛ぶという鳥のドキュメントを読みました。その文書が、鳥が飛べない場合は例外が投げられるかもしれないと明確に述べていたなら、私はそれを許したでしょう。fly()を呼び出すと時々泳ぐと言われたら、別のクラスライブラリを使用するように変更します。または非常に大きな飲み物を飲みに行きました。
DJClayworth
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.