it-swarm.com.de

Ist das Erstellen von Unterklassen für bestimmte Instanzen eine schlechte Praxis?

Betrachten Sie das folgende Design

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

Glaubst du, hier stimmt etwas nicht? Für mich sollten Karl- und John-Klassen nur Instanzen anstelle von Klassen sein, da sie genau die gleichen sind wie:

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

Warum sollte ich neue Klassen erstellen, wenn Instanzen ausreichen? Die Klassen fügen der Instanz nichts hinzu.

37

Es ist nicht erforderlich, für jede Person bestimmte Unterklassen zu haben.

Sie haben Recht, das sollten stattdessen Instanzen sein.

Ziel der Unterklassen: Erweiterung der übergeordneten Klassen

Unterklassen werden verwendet, um die von der übergeordneten Klasse bereitgestellte Funktionalität zu erweitern. Zum Beispiel können Sie haben:

  • Eine übergeordnete Klasse Battery, die Power() etwas kann und eine Voltage Eigenschaft haben kann,

  • Und eine Unterklasse RechargeableBattery, die Power() und Voltage erben kann, aber auch Recharge() d sein kann.

Beachten Sie, dass Sie eine Instanz der Klasse RechargeableBattery als Parameter an jede Methode übergeben können, die ein Battery als Argument akzeptiert. Dies nennt man Liskov-Substitutionsprinzip, eines der fünf SOLID -Prinzipien. Wenn mein MP3-Player im wirklichen Leben zwei AA-Batterien akzeptiert, kann ich diese durch zwei wiederaufladbare AA-Batterien ersetzen.

Beachten Sie, dass es manchmal schwierig ist zu bestimmen, ob Sie ein Feld oder eine Unterklasse verwenden müssen, um einen Unterschied zwischen etwas darzustellen. Wenn Sie beispielsweise mit AA-, AAA- und 9-Volt-Batterien umgehen müssen, würden Sie drei Unterklassen erstellen oder eine Aufzählung verwenden? "Unterklasse durch Felder ersetzen" in Refactoring von Martin Fowler, Seite 232, gibt Ihnen möglicherweise einige Ideen und Möglichkeiten, wie Sie von einer zur anderen wechseln können.

In Ihrem Beispiel erweitern Karl und John nichts und bieten keinen zusätzlichen Wert: Sie können genau dieselbe Funktionalität verwenden, indem Sie die Klasse Person direkt verwenden. Es ist niemals gut, mehr Codezeilen ohne zusätzlichen Wert zu haben.

Ein Beispiel für einen Business Case

Was könnte ein Business Case sein, bei dem es tatsächlich sinnvoll wäre, eine Unterklasse für eine bestimmte Person zu erstellen?

Angenommen, wir erstellen eine Anwendung, mit der Personen verwaltet werden, die in einem Unternehmen arbeiten. Die Anwendung verwaltet auch Berechtigungen, sodass Helen, die Buchhalterin, nicht auf das SVN-Repository zugreifen kann, Thomas und Mary, zwei Programmierer, jedoch nicht auf buchhaltungsbezogene Dokumente zugreifen können.

Jimmy, der große Chef (Gründer und CEO des Unternehmens), hat ganz bestimmte Privilegien, die niemand hat. Er kann zum Beispiel das gesamte System herunterfahren oder eine Person entlassen. Du hast die Idee.

Das schlechteste Modell für eine solche Anwendung sind Klassen wie:

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

weil Code-Duplikationen sehr schnell auftreten werden. Selbst im einfachen Beispiel von vier Mitarbeitern duplizieren Sie Code zwischen den Klassen Thomas und Mary. Dadurch werden Sie aufgefordert, eine gemeinsame übergeordnete Klasse Programmer zu erstellen. Da Sie möglicherweise auch mehrere Buchhalter haben, werden Sie wahrscheinlich auch die Klasse Accountant erstellen.

 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.

Jetzt stellen Sie fest, dass es nicht sehr nützlich ist, die Klasse Helen zu haben und Thomas und Mary beizubehalten: Der größte Teil Ihres Codes funktioniert sowieso auf der oberen Ebene - auf der Niveau der Buchhalter, Programmierer und Jimmy. Dem SVN-Server ist es egal, ob Thomas oder Mary auf das Protokoll zugreifen müssen - er muss nur wissen, ob es sich um einen Programmierer oder einen Buchhalter handelt.

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

Am Ende entfernen Sie also die Klassen, die Sie nicht verwenden:

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

"Aber ich kann Jimmy so lassen, wie er ist, da es immer nur einen CEO, einen großen Chef geben würde - Jimmy", denken Sie. Darüber hinaus wird Jimmy häufig in Ihrem Code verwendet, was tatsächlich eher so aussieht und nicht wie im vorherigen Beispiel:

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

Das Problem bei diesem Ansatz ist, dass Jimmy immer noch von einem Bus angefahren werden kann und es einen neuen CEO geben würde. Oder der Verwaltungsrat entscheidet, dass Mary so großartig ist, dass sie eine neue Geschäftsführerin sein sollte, und Jimmy würde in die Position eines Verkäufers herabgestuft. Jetzt müssen Sie all durchlaufen Ihren Code und ändern Sie alles.

77

Es ist albern, diese Art von Klassenstruktur zu verwenden, um Details zu variieren, die offensichtlich einstellbare Felder für eine Instanz sein sollten. Aber das ist speziell für Ihr Beispiel.

Es ist mit ziemlicher Sicherheit eine schlechte Idee, verschiedene Persons verschiedene Klassen zu erstellen - Sie müssten Ihr Programm ändern und jedes Mal neu kompilieren, wenn eine neue Person Ihre Domain betritt. Dies bedeutet jedoch nicht, dass das Erben von einer vorhandenen Klasse, sodass irgendwo eine andere Zeichenfolge zurückgegeben wird, manchmal nicht sinnvoll ist.

Der Unterschied besteht darin, wie Sie erwarten, dass die von dieser Klasse dargestellten Entitäten variieren. Es wird fast immer angenommen, dass Menschen kommen und gehen, und von fast jeder ernsthaften Anwendung, die sich mit Benutzern befasst, wird erwartet, dass sie zur Laufzeit Benutzer hinzufügen und entfernen können. Wenn Sie jedoch verschiedene Verschlüsselungsalgorithmen modellieren, ist die Unterstützung eines neuen wahrscheinlich ohnehin eine große Änderung, und es ist sinnvoll, eine neue Klasse zu erfinden, deren myName() -Methode eine andere Zeichenfolge zurückgibt (und deren perform() Methode macht vermutlich etwas anderes).

15
Kilian Foth

Warum sollte ich neue Klassen erstellen, wenn Instanzen ausreichen?

In den meisten Fällen würden Sie nicht. Ihr Beispiel ist wirklich ein guter Fall, in dem dieses Verhalten keinen wirklichen Mehrwert bringt.

Es verstößt auch gegen das Open Closed-Prinzip , da die Unterklassen im Grunde keine Erweiterung sind, sondern das Innenleben der Eltern verändern. Darüber hinaus ist der öffentliche Konstruktor des Elternteils jetzt in den Unterklassen irritierend und die API wurde dadurch weniger verständlich.

Wenn Sie jedoch immer nur eine oder zwei spezielle Konfigurationen haben, die häufig im gesamten Code verwendet werden, ist es manchmal bequemer und weniger zeitaufwendig, nur ein übergeordnetes Element mit einem komplexen Konstruktor zu unterordnen. In solchen Sonderfällen kann ich an einem solchen Ansatz nichts auszusetzen sehen. Nennen wir es Konstruktor-Currying j/k

12
Falcon

Wenn dies der Umfang der Praxis ist, dann stimme ich zu, dass dies eine schlechte Praxis ist.

Wenn John und Karl sich unterschiedlich verhalten, ändern sich die Dinge ein wenig. Es könnte sein, dass person eine Methode für cleanRoom(Room room) hat, bei der sich herausstellt, dass John ein großartiger Mitbewohner ist und sehr effektiv putzt, aber Karl ist nicht und putzt den Raum nicht sehr.

In diesem Fall wäre es sinnvoll, wenn sie ihre eigene Unterklasse mit dem definierten Verhalten wären. Es gibt bessere Möglichkeiten, dasselbe zu erreichen (zum Beispiel eine CleaningBehavior -Klasse), aber zumindest ist diese Methode keine schreckliche Verletzung der Prinzipien von OO).

8
corsiKa

In einigen Fällen wissen Sie, dass es nur eine festgelegte Anzahl von Instanzen gibt, und dann wäre es in Ordnung (obwohl meiner Meinung nach hässlich), diesen Ansatz zu verwenden. Es ist besser, eine Aufzählung zu verwenden, wenn die Sprache dies zulässt.

Java-Beispiel:

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

Viele Leute haben bereits geantwortet. Ich dachte, ich würde meine persönliche Perspektive geben.


Es war einmal eine App, die Musik macht (und immer noch macht).

Die App hatte eine abstrakte Scale Klasse mit mehreren Unterklassen: CMajor, DMinor usw. Scale sah ungefähr so ​​aus:

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. */
}

Die Musikgeneratoren arbeiteten mit einer bestimmten Scale -Instanz, um Musik zu generieren. Der Benutzer würde eine Skala aus einer Liste auswählen, aus der Musik generiert werden soll.

Eines Tages kam mir eine coole Idee in den Sinn: Warum nicht dem Benutzer erlauben, seine eigenen Skalen zu erstellen? Der Benutzer wählt Notizen aus einer Liste aus, drückt eine Taste und fügt der Liste der verfügbaren Skalen eine neue Skala hinzu.

Aber ich konnte das nicht. Das lag daran, dass alle Skalen bereits zur Kompilierungszeit festgelegt wurden - da sie als Klassen ausgedrückt werden. Dann traf es mich:

Es ist oft intuitiv, in „Ober- und Unterklassen“ zu denken. Fast alles kann durch dieses System ausgedrückt werden: Oberklasse Person und Unterklassen John und Mary; Oberklasse Car und Unterklassen Volvo und Mazda; Oberklasse Missile und Unterklassen SpeedRocked, LandMine und TrippleExplodingThingy.

Es ist sehr natürlich, auf diese Weise zu denken, insbesondere für die Person, die für OO relativ neu ist.

Wir sollten uns jedoch immer daran erinnern, dass Klassen Vorlagen und Objekte sind werden Inhalte in diese Vorlagen gegossen . Sie können jeden gewünschten Inhalt in die Vorlage einfügen und so unzählige Möglichkeiten schaffen.

Es ist nicht die Aufgabe der Unterklasse, die Vorlage zu füllen. Es ist die Aufgabe des Objekts. Die Aufgabe der Unterklasse besteht darin, tatsächliche Funktionalität hinzuzufügen oder erweitern) die Vorlage.

Und deshalb hätte ich eine konkrete Scale Klasse mit einem Note[] - Feld erstellen und Objekte diese Vorlage ausfüllen lassen sollen ; möglicherweise durch den Konstruktor oder so. Und schließlich tat ich es auch.

Jedes Mal, wenn Sie eine Vorlage in einer Klasse entwerfen (z. B. ein leeres Note[] - Mitglied, das gefüllt werden muss, oder ein String name - Feld, dem ein Wert zugewiesen werden muss), denken Sie daran, dass es die Aufgabe der Objekte dieser Klasse ist, die Vorlage (oder möglicherweise) auszufüllen diejenigen, die diese Objekte erstellen). Unterklassen sollen Funktionen hinzufügen und keine Vorlagen ausfüllen.


Sie könnten versucht sein, ein System der Art "Superklasse Person, Unterklassen John und Mary" zu erstellen, wie Sie es getan haben, weil Ihnen die Formalität gefällt, die Sie dadurch erhalten.

Auf diese Weise können Sie einfach Person p = new Mary() anstelle von Person p = new Person("Mary", 57, Sex.FEMALE) sagen. Es macht die Dinge organisierter und strukturierter. Wie bereits erwähnt, ist das Erstellen einer neuen Klasse für jede Datenkombination kein guter Ansatz, da der Code umsonst aufgebläht wird und Sie hinsichtlich der Laufzeitfähigkeiten eingeschränkt werden.

Hier ist eine Lösung: Verwenden Sie eine Basisfabrik, möglicherweise sogar eine statische. Wie so:

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

    // ...
}

Auf diese Weise können Sie ganz einfach die 'Voreinstellungen' und 'Mit dem Programm kommen' verwenden, wie folgt: Person mary = PersonFactory.createMary(), aber Sie behalten sich auch das Recht vor, neue Personen dynamisch zu gestalten, beispielsweise in dem von Ihnen gewünschten Fall um dem Benutzer dies zu ermöglichen. Z.B.:

// .. 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);

Oder noch besser: mach so etwas:

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;
    }
}

Ich wurde weggetragen. Ich denke du kommst auf die Idee.


nterklassen sollen nicht die von ihren Oberklassen festgelegten Vorlagen ausfüllen. Unterklassen sollen Funktionen hinzufügen . Objekt soll die Vorlagen ausfüllen, dafür sind sie da.

Sie sollten nicht für jede mögliche Datenkombination eine neue Klasse erstellen. (Genau wie ich nicht für jede mögliche Kombination von Scales eine neue Unterklasse Note hätte erstellen sollen).

Hier ist eine Richtlinie: Wenn Sie eine neue Unterklasse erstellen, prüfen Sie, ob neue Funktionen hinzugefügt werden, die in der Oberklasse nicht vorhanden sind. Wenn die Antwort auf diese Frage "Nein" lautet, versuchen Sie möglicherweise, die Vorlage der Oberklasse auszufüllen. In diesem Fall erstellen Sie einfach ein Objekt. (Und möglicherweise eine Factory mit Voreinstellungen ', um das Leben leichter zu machen).

Ich hoffe, das hilft.

6
Aviv Cohn

Dies ist eine Ausnahme von den "sollte Instanzen sein" hier - wenn auch etwas extrem.

Wenn Sie möchten, dass der Compiler Berechtigungen für den Zugriff auf Methoden erzwingt, basierend darauf, ob es sich um Karl oder John handelt (in diesem Beispiel), anstatt die Methodenimplementierung zu bitten, zu überprüfen, für welche Instanz übergeben wurde, können Sie eine andere verwenden Klassen. Indem Sie verschiedene Klassen anstelle der Instanziierung erzwingen, ermöglichen Sie Differenzierungsprüfungen zwischen 'Karl' und 'John' zur Kompilierungszeit und nicht zur Laufzeit. Dies möglicherweise ist nützlich - insbesondere in einem Sicherheitskontext, in dem der Compiler dies möglicherweise tut Seien Sie vertrauenswürdiger als die Überprüfung des Laufzeitcodes von (zum Beispiel) externen Bibliotheken.

2

Es wird als schlechte Praxis angesehen, Code zu duplizieren.

Dies liegt zumindest teilweise daran, dass Codezeilen und damit die Größe der Codebasis mit Wartungskosten und Fehlern korrelieren .

Hier ist die für mich relevante Logik des gesunden Menschenverstandes zu diesem speziellen Thema:

1) Wenn ich dies ändern müsste, wie viele Orte müsste ich besuchen? Weniger ist hier besser.

2) Gibt es einen Grund, warum dies so gemacht wird? In Ihrem Beispiel - das konnte es nicht geben - könnte es in einigen Beispielen einen Grund dafür geben. Eine, die ich mir auf Anhieb vorstellen kann, ist in einigen Frameworks wie Early Grails, in denen die Vererbung mit einigen Plugins für GORM nicht unbedingt gut funktioniert hat. Dünn gesät.

3) Ist es sauberer und leichter zu verstehen? Auch dies ist nicht in Ihrem Beispiel der Fall - aber es gibt einige Vererbungsmuster, die möglicherweise eher eine Strecke darstellen, in der getrennte Klassen tatsächlich einfacher zu pflegen sind (bestimmte Verwendungen der Befehls- oder Strategiemuster fallen mir ein).

2
dcgregorya

Es gibt einen Anwendungsfall: einen Verweis auf eine Funktion oder Methode als reguläres Objekt bilden.

Sie können dies in Java GUI-Code viel sehen:

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

Der Compiler hilft Ihnen hier, indem er eine neue anonyme Unterklasse erstellt.

0
Simon Richter