web-dev-qa-db-ja.com

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

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

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");

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

37

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

そうです、代わりにインスタンスにする必要があります。

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

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

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

  • また、サブクラスRechargeableBatteryは、Power()およびVoltageを継承できますが、Recharge() dにすることもできます。

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

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

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

ビジネスケースの例

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

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

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

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

 The class diagram shows Helen, Thomas, Mary and Jimmy classes and their common parent: the abstract Person class.

コードの重複がすぐに発生するためです。 4人の従業員というごく基本的な例でも、ThomasクラスとMaryクラスの間でコードを複製します。これにより、共通の親クラスProgrammerが作成されます。複数の会計士もいる可能性があるため、おそらくAccountantクラスも作成します。

 The class diagram shows an abstract Person with three children: abstract Accountant, an abstract Programmer, and Jimmy. Accountant has a subclass Helen, and Programmer has two subclasses: Thomas and Mary.

ここで、HelenThomasを維持することと同様に、クラスMaryを使用することはあまり役に立たないことに気づきます。ほとんどのコードはとにかく上位レベルで機能します。会計士、プログラマー、ジミーのレベル。 SVNサーバーは、ログにアクセスする必要があるのがThomasかMaryかを気にしません—必要なのは、プログラマーか会計士かを知ることだけです。

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

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

 The class diagram contains Accountant, Programmer and Jimmy subclasses with its common parent class: abstract Person.

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

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

このアプローチの問題は、ジミーが依然としてバスに見舞われる可能性があり、新しいCEOが存在することです。または、取締役会は、メアリーがとても素晴らしいので彼女が新しいCEOになるべきであると判断し、ジミーは営業担当者の地位に降格するので、ここでallコードを変更し、すべてを変更します。

77

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

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

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

15
Kilian Foth

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

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

また、サブクラスは基本的に拡張ではなく、親の内部動作を変更するため、これは オープンクローズの原則 に違反しています。さらに、親のパブリックコンストラクターがサブクラスでイライラするようになり、APIがわかりにくくなりました。

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

12
Falcon

これが慣習の範囲である場合、私はこれが悪い慣習であることに同意します。

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

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

8
corsiKa

場合によっては、インスタンスの数が決まっていることがわかっているので、このアプローチを使用しても(醜いかもしれませんが)ある程度は問題ありません。言語で許可されている場合は、列挙型を使用することをお勧めします。

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
Adam

多くの人がすでに答えました。私は自分自身の個人的な見解を与えると思いました。


むかしむかし、私は音楽を作成するアプリに取り組みました(そして今でもそうです)。

アプリには、ScaleCMajorなどのいくつかのサブクラスを持つ抽象DMinorクラスがありました。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とサブクラスJohnMary;スーパークラスCarおよびサブクラスVolvoおよびMazda;スーパークラスMissileおよびサブクラスSpeedRockedLandMineおよびTrippleExplodingThingy

このように考えるのは自然なことです。特にOOに比較的慣れていない人にとってはそうです。

ただし、クラスはテンプレート、およびオブジェクトであることを常に覚えておく必要がありますこれらのテンプレートにコンテンツが注がれていますか。必要なコンテンツをテンプレートに流し込むことができるため、無数の可能性が生まれます。

テンプレートを満たすのはサブクラスの仕事ではありません。オブジェクトの仕事です。サブクラスの仕事は実際の機能を追加する、またはexpandテンプレート

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

クラスでtemplateを設計するたびに(たとえば、入力する必要がある空の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;
    }
}

私は夢中になりました。あなたはアイデアを理解していると思います。


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

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

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

お役に立てば幸いです。

6
Aviv Cohn

これは、ここでは「インスタンスであるべき」の例外です-少し極端ですが。

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

2

重複するコードを持つのは悪い習慣 と見なされます。

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

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

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

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

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

2
dcgregorya

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

あなたはこれをJava GUIコードでたくさん見ることができます:

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

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

0
Simon Richter