特定のインスタンスのサブクラスを作成するのは悪い習慣ですか?


37

次の設計を検討してください

public class Person
{
    public virtual string Name { get; }

    public Person (string name)
    {
        this.Name = name;
    }
}

public class Karl : Person
{
    public override string Name
    {
        get
        {
            return "Karl";
        }
    }
}

public class John : Person
{
    public override string Name
    {
        get
        {
            return "John";
        }
    }
}

ここに何か問題があると思いますか?私にとって、KarlとJohnのクラスは、クラスではなく単なるインスタンスである必要があります。

Person karl = new Person("Karl");
Person john = new Person("John");

インスタンスが十分なときに新しいクラスを作成するのはなぜですか?クラスはインスタンスに何も追加しません。


17
残念ながら、これは多くの製品コードで見つかります。可能な場合は片付けてください。
アダムザッカーマン14

14
これは素晴らしいデザインです。開発者がビジネスデータのすべての変更に欠かせないものにしたい場合、24時間年中無休で利用できる問題はありません;-)
Doc Brown 14

1
OPが従来の知恵の反対を信じているように見える、関連する質問の一種です。programmers.stackexchange.com/questions/253612/...
ダンク

11
データではなく動作に応じてクラスを設計します。私にとって、サブクラスは新しい動作を階層に追加しません。
ソンゴ

2
これは、ラムダがJava 8に到達する前に、Javaの匿名サブクラス(イベントハンドラーなど)で広く使用されています(異なる構文でも同じです)。
マルツェルム14

回答:


77

すべての人に特定のサブクラスを用意する必要はありません。

あなたは正しい、それらは代わりにインスタンスであるべきです。

サブクラスの目標:親クラスを拡張する

サブクラスは、親クラスによって提供される機能拡張するために使用されます。たとえば、次のものがあります。

  • 何かを持つことができ、プロパティを持つことがBatteryできる親クラス、Power()Voltage

  • そして、サブクラスRechargeableBattery、缶継承Power()Voltage、だけでなく、することができRecharge()D。

RechargeableBatteryクラスのインスタンスをBattery、引数としてを受け入れる任意のメソッドにパラメーターとして渡すことができることに注意してください。これはリスコフ置換原理と呼ばれ、5つの固体原理の1つです。同様に、実際には、MP3プレーヤーが2つの単三電池を受け入れる場合、2つの充電式単三電池でそれらを置き換えることができます。

何かの違いを表すためにフィールドまたはサブクラスを使用する必要があるかどうかを判断するのは難しい場合があることに注意してください。たとえば、AA、AAA、9ボルトのバッテリーを処理する必要がある場合、3つのサブクラスを作成するか、enumを使用しますか?232ページのMartin Fowlerによる「リファクタリング」の「サブクラスをフィールドに置き換える」は、いくつかのアイデアと、あるクラスから別のクラスに移動する方法を示しています。

あなたの例では、KarlJohn何も拡張しない、また彼らは、任意の付加価値を提供します:あなたが使用して、まったく同じ機能を持つことができPerson、直接クラスを。追加の値を持たないコード行を増やすことは決して良くありません。

ビジネスケースの例

特定の人のサブクラスを作成することが実際に理にかなっているビジネスケースとは何でしょうか。

会社で働く人を管理するアプリケーションを構築するとしましょう。アプリケーションも権限を管理するため、会計士のヘレンはSVNリポジトリにアクセスできませんが、2人のプログラマーであるトーマスとメアリーは会計関連のドキュメントにアクセスできません。

大ボス(会社の創設者兼CEO)であるジミーは、誰も持っていない特別な特権を持っています。たとえば、システム全体をシャットダウンしたり、人を解雇したりできます。あなたはアイデアを得る。

そのようなアプリケーションの最も貧弱なモデルは、次のようなクラスを持つことです。

          クラス図は、Helen、Thomas、Mary、Jimmyの各クラスと、それらの共通の親である抽象Personクラスを示しています。

コードの重複は非常に迅速に発生するためです。4人の従業員という非常に基本的な例でも、ThomasクラスとMaryクラスの間でコードを複製します。これにより、共通の親クラスを作成するようにプッシュされますProgrammer。あなたにも、複数の会計士を持っているかもしれないので、あなたはおそらく、作成されますAccountantクラスを同様。

          クラス図は、抽象会計士、抽象プログラマー、およびジミーの3人の子を持つ抽象Personを示しています。 AccountantにはサブクラスHelenがあり、ProgrammerにはThomasとMaryという2つのサブクラスがあります。

さて、あなたはクラスを有することがわかりHelen非常に便利なだけでなく、維持ではないThomasMary:あなたのコードの作品のほとんどを上位レベルでとにかく-で会計士、プログラマーやジミーのレベル。SVNサーバーは、ログにアクセスする必要があるのがトーマスかメアリーかを気にしません。プログラマーか会計士かを知るだけで済みます。

if (person is Programmer)
{
    this.AccessGranted = true;
}

したがって、使用しないクラスを削除することになります。

          クラス図には、Accountant、Programmer、およびJimmyのサブクラスとその共通の親クラスである抽象Personが含まれています。

「しかし、私はジミーをそのままにしておくことができます。なぜなら、常に1人のCEO、1人の大ボス、つまりジミーしかいないからです」とあなたは思います。さらに、Jimmyはコードでよく使用されますが、実際には前の例とは異なり、次のようになります。

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

このアプローチの問題点は、ジミーがバスに襲われる可能性があり、新しいCEOがいることです。または、取締役会は、メアリーが新しいCEOになれるほど優秀だと判断する場合があり、ジミーは営業担当者の地位に降格するため、すべてのコードを調べてすべてを変更する必要があります。


6
@DocBrown:間違っていないが十分に深くない短い回答が、主題を詳細に説明する回答よりもはるかに高いスコアを持っているのを見て、私はしばしば驚きます。おそらくあまりにも多くの人がたくさん読みたがらないので、非常に短い答えはより魅力的に見えます。それでも、少なくともいくつかの説明を提供するために答えを編集しました。
Arseni Mourzenko

3
短い回答が最初に投稿される傾向があります。以前の投稿は、より多くの賛成票を獲得します。
ティムB 14

1
@TimB:私は通常、中規模の回答から始めて、非常に完全になるように編集します(おそらく15のリビジョンがあり、4つしか表示されていないので、例を示します)。時間が経つにつれて回答が長くなるほど、受信する賛成票が少なくなることに気付きました。短いままである同様の質問は、より多くの賛成票を集めます。
Arseni Mourzenko

@DocBrownに同意します。たとえば、適切な答えは、「コンテンツではなく、動作のサブクラス」のようなもので、このコメントでは説明しないリファレンスを参照します。
user949300 14

2
最後の段落から明確でない場合:無制限のアクセスを持つ人が1人しかいない場合でも、その人を無制限のアクセスを実装する別のクラスのインスタンスにする必要があります。人自身をチェックすると、コードとデータの相互作用が発生し、ワームの缶全体が開かれる可能性があります。たとえば、取締役会が最高経営責任者をCEOに任命することにした場合はどうなりますか?「Unrestricted」クラスがない場合、既存のCEOを更新するだけでなく、他のメンバーのチェックをさらに2つ追加する必要があります。持っていた場合は、「無制限」として設定するだけです。
Nzall 14

15

インスタンスの設定可能なフィールドであるべき詳細を変えるためだけに、この種のクラス構造を使用するのはばかげています。しかし、それはあなたの例に特有です。

Person異なるクラスを作成することはほぼ間違いなく悪い考えです。新しいPersonがドメインに入るたびにプログラムを変更し、再コンパイルする必要があります。ただし、既存のクラスから継承して別の文字列がどこかに返されるようにすることは、時には役に立たないという意味ではありません。

違いは、そのクラスによって表されるエンティティが変化することを期待する方法です。人々はほとんど常に行き来するものと想定されており、ユーザーを扱うほとんどすべての本格的なアプリケーションは、実行時にユーザーを追加および削除できることが期待されています。しかし、異なる暗号化アルゴリズムのようなものをモデル化する場合、新しいアルゴリズムをサポートすることはおそらく大きな変更となるでしょう。また、myName()メソッドが異なる文字列を返す(そしてperform()おそらくメソッドが異なる何かをする)新しいクラスを発明することは理にかなっています。


12

インスタンスが十分なときに新しいクラスを作成するのはなぜですか?

ほとんどの場合、そうしません。あなたの例は、この振る舞いが本当の価値を加えない本当に良いケースです。

また、サブクラスは基本的に拡張機能ではなく、親の内部動作を変更するため、Open Closed原則に違反します。さらに、親のパブリックコンストラクターはサブクラスをいらいらさせているため、APIの理解が難しくなりました。

ただし、コード全体で頻繁に使用される特別な構成が1つまたは2つしかない場合は、複雑なコンストラクターを持つ親をサブクラス化する方が便利で時間がかかりません。そのような特別なケースでは、そのようなアプローチに何の問題もないことがわかります。j / kをカリー化 するコンストラクターと呼びましょう


OCPの段落に同意するかどうかはわかりません。クラスがプロパティセッターをその子孫に公開している場合-優れた設計原則に従っていると仮定して
ベンアーロンソン14

@BenAaronsonプロパティ自体の動作が変更されない限り、OCPに違反することはありませんが、ここでは変更されます。もちろん、そのプロパティを内部動作で使用できます。ただし、既存の動作を変更しないでください!継承だけで変更するのではなく、振る舞いを拡張するだけにしてください。もちろん、すべてのルールには例外があります。
ファルコン14

1
仮想メンバーの使用はOCP違反ですか?
ベンアーロンソン14

3
@Falcon複雑なコンストラクターの繰り返し呼び出しを避けるためだけに、新しいクラスを派生する必要はありません。よくあるケースのファクトリを作成し、小さなバリエーションをパラメーター化するだけです。
キーン14

@BenAaronson実際に実装されている仮想メンバーを持つことは、このような違反だと思います。抽象メンバー、または何もしないメソッドは、有効な拡張ポイントになります。基本的に、基本クラスのコードを見ると、すべての非最終メソッドがまったく異なるものに置き換えられる候補ではなく、そのまま実行されることがわかります。または、別の言い方をすれば、継承クラスが基本クラスの動作に影響を与える方法は明示的であり、「加算的」に制限されます。
ミリムース14年

8

これが慣行の範囲であれば、これは悪い慣行であることに同意します。

ジョンとカールの行動が異なる場合、状況は少し変わります。それpersonはのためのメソッドを持っている可能性がありますcleanRoom(Room room)は、ジョンが素晴らしいルームメイトであり、非常に効果的に掃除することが判明ませんが、カールはそうではなく、部屋をあまり掃除しません。

この場合、動作を定義した独自のサブクラスにすることが理にかなっています。同じことを達成するためのより良い方法(CleaningBehaviorクラスなど)がありますが、少なくともこの方法はオブジェクト指向の原則のひどい違反ではありません。


異なる動作ではなく、異なるデータのみ。ありがとう。
イグナシオソレルガルシア14

6

場合によっては、設定された数のインスタンスしかないことを知っているので、このアプローチを使用することは(私の意見ではinいですが)なんとかなるでしょう。言語で許可されている場合は、enumを使用することをお勧めします。

Javaの例:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}

6

多くの人がすでに答えました。私自身の個人的な視点を与えると思った。


むかしむかし、私は音楽を作成するアプリを開発しました(そして今でもそうしています)。

アプリが抽象的だったScale:いくつかのサブクラスを持つクラスをCMajorDMinorなど、Scaleそのようなものに見えました。

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

音楽ジェネレーターは、特定のScaleインスタンスと連携して音楽​​を生成しました。ユーザーはリストから音階を選択し、そこから音楽を生成します。

ある日、クールなアイデアが思い浮かびました。ユーザーが自分のスケールを作成できるようにしてみませんか?ユーザーはリストからノートを選択し、ボタンを押すと、リストに新しいスケールが追加されます。

しかし、私はこれを行うことができませんでした。これは、すべてのスケールがコンパイル時にすでに設定されているためです-クラスとして表現されているからです。それから私にぶつかった:

多くの場合、「スーパークラスとサブクラス」の観点から考えるのは直感的です。ほとんどすべてのものは、このシステムを介して表現できます。スーパークラスPersonとサブクラスJohnおよびMary; スーパークラスCarとサブクラスVolvoおよびMazda; スーパークラスMissileとサブクラスSpeedRockedLandMineおよびTrippleExplodingThingy

このように考えることは非常に自然です。特にオブジェクト指向に比較的慣れていない人にとっては。

しかし、クラスはテンプレートであり、オブジェクトはこれらのテンプレートに注がれるコンテンツであることを常に覚えておく必要があります。必要なコンテンツをテンプレートに注ぎ、無数の可能性を作成できます。

テンプレートを埋めるのはサブクラスの仕事ではありません。それはオブジェクトの仕事です。サブクラスの役割は実際の機能追加する、テンプレートを展開することです。

それがScaleNote[]フィールドを持つ具体的なクラスを作成し、オブジェクトにこのテンプレートを入力させるべきだった理由です。おそらくコンストラクターか何かを通して。そして最終的に、私はそうしました。

クラスでテンプレートを設計するたびに(たとえば、入力するNote[]必要がある空のメンバー、またはString name値を割り当てる必要があるフィールド)、テンプレートに入力するのはこのクラスのオブジェクトの仕事であることに注意してください(または、おそらくこれらのオブジェクトを作成する人)。サブクラスは、テンプレートに入力するためではなく、機能を追加するためのものです。


あなたがしたように「スーパークラスPerson、サブクラスJohnMary」種類のシステムを作成したくなるかもしれません。

このようにするとPerson p = new Mary()、の代わりにと言うことができますPerson p = new Person("Mary", 57, Sex.FEMALE)。それは物事をより組織化し、より構造化します。しかし、前述したように、データのあらゆる組み合わせに対して新しいクラスを作成することは、コードを無制限に肥大化し、実行時の能力の面で制限されるため、良いアプローチではありません。

そのため、ここに解決策があります。基本的なファクトリーを使用します。そのようです:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

このように、次のように「プログラムに来る」「プリセット」を簡単に使用できますがPerson mary = PersonFactory.createMary()、たとえばユーザーにそうすることを許可したい場合など、新しい人を動的に設計する権利も留保します。例えば:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

またはさらに良い:そのような何かをする:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

私は夢中になりました。あなたはそのアイデアを得ると思います。


サブクラスは、スーパークラスによって設定されたテンプレートを埋めるためのものではありません。サブクラスは機能追加するためのものです。オブジェクトはテンプレートを埋めるためのものであり、それが目的です。

データのあらゆる可能な組み合わせに対して新しいクラスを作成するべきではありません。(sのScaleすべての可能な組み合わせに対して新しいサブクラスを作成するべきではなかったようにNote)。

彼女のガイドラインです。新しいサブクラスを作成するときは、スーパークラスに存在しない新しい機能を追加するかどうかを検討してください。その質問に対する答えが「いいえ」の場合、スーパークラスの「テンプレートに入力」しようとしている場合よりも、その場合はオブジェクトを作成するだけです。(そしておそらく、人生を楽にするための「プリセット」を備えた工場)。

お役に立てば幸いです。


これは素晴らしい例です、ありがとうございます。
イグナシオソレルガルシア14年

@SoMoSうれしい
アビブコーン14年

2

これは、「極端な例」の例外です。

あなたは望んでいた場合は、コンパイラには(この例では)カールやジョンであるかどうかに基づいてアクセス方法へのアクセス権を強制するのではなく対象のインスタンスが、あなたは異なるクラスを使用する場合があります渡されたチェックするメソッドの実装を依頼するが。インスタンス化ではなく異なるクラスを適用することにより、実行時ではなくコンパイル時に「カール」と「ジョン」の区別チェックが可能になり、これ特にコンパイラが実行時よりも信頼できるセキュリティコンテキストで役立ちます(たとえば)外部ライブラリのコードチェック。


2

コードを複製することは悪い習慣と考えられています

これは少なくとも部分的には 、コードの行とコードベースのサイズが保守コストと欠陥に相関するためです。

この特定のトピックに関する私に関連する常識ロジックは次のとおりです。

1)これを変更する必要がある場合、いくつの場所を訪問する必要がありますか?ここでは少ない方が良いです。

2)この方法で実行される理由はありますか?あなたの例では-ありえませんでした-いくつかの例では、これを行う何らかの理由があるかもしれません。頭の中で思い浮かぶのは、初期のGrailsのようないくつかのフレームワークで、GORMのプラグインの一部で必ずしも継承がうまく機能しなかった場合です。ほんの少しです。

3)この方法はわかりやすく、理解しやすいですか?繰り返しますが、これはあなたの例ではありません-しかし、いくつかの継承パターンがあり、分離されたクラスが実際に維持しやすいストレッチのほうがあります(コマンドまたは戦略パターンの特定の使用法が思い浮かびます)。


1
悪いかどうかは決まっていない。たとえば、オブジェクト作成およびメソッド呼び出しの余分なオーバーヘッドがパフォーマンスに悪影響を及ぼし、アプリケーションが仕様を満たさなくなるシナリオがあります。
jwenting

ポイント#2を参照してください。確かに、時々、理由があります。だから、誰かのコードを吐き出す前に尋ねる必要があります。
dcgregorya 14

0

ユースケースがあります:関数またはメソッドへの参照を通常のオブジェクトとして作成します。

これはJava GUIコードでよく見ることができます:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

コンパイラは、新しい匿名サブクラスを作成することにより、ここで役立ちます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.