La creazione di sottoclassi per istanze specifiche è una cattiva pratica?

37

Considera il seguente disegno

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

Pensi che ci sia qualcosa di sbagliato qui? Per me le classi Karl e John dovrebbero essere solo istanze anziché classi poiché sono esattamente le stesse di:

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

Perché dovrei creare nuove classi quando le istanze sono sufficienti? Le classi non aggiungono nulla all'istanza.

    
posta Ignacio Soler Garcia 29.09.2014 - 16:57
fonte

9 risposte

77

Non è necessario avere sottoclassi specifiche per ogni persona.

Hai ragione, quelle dovrebbero essere invece istanze.

Obiettivo delle sottoclassi: estendere le classi parent

Le sottoclassi vengono utilizzate per estendere la funzionalità fornita dalla classe genitore. Ad esempio, potresti avere:

  • Una classe genitore Battery che può Power() qualcosa e può avere una proprietà Voltage ,

  • E una sottoclasse RechargeableBattery , che può ereditare Power() e Voltage , ma può anche essere Recharge() d.

Si noti che è possibile passare un'istanza di RechargeableBattery class come parametro a qualsiasi metodo che accetta un Battery come argomento. Questo è chiamato principio di sostituzione di Liskov , uno dei cinque principi SOLID. Allo stesso modo, nella vita reale, se il mio lettore MP3 accetta due batterie AA, posso sostituirle con due batterie AA ricaricabili.

Si noti che a volte è difficile determinare se è necessario utilizzare un campo o una sottoclasse per rappresentare una differenza tra qualcosa. Ad esempio, se devi gestire batterie AA, AAA e 9 Volt, creeresti tre sottoclassi o usassi un enum? "Sostituisci sottoclasse con campi" in Refactoring di Martin Fowler, pagina 232, può dare alcune idee e come spostarti da uno all'altro.

Nel tuo esempio, Karl e John non estendono nulla, né forniscono alcun valore aggiuntivo: puoi avere esattamente la stessa funzionalità utilizzando la classe di Person direttamente. Avere più righe di codice senza alcun valore aggiuntivo non è mai buono.

Un esempio di caso aziendale

Quale potrebbe essere un caso aziendale in cui avrebbe davvero senso creare una sottoclasse per una persona specifica?

Diciamo che costruiamo un'applicazione che gestisce le persone che lavorano in un'azienda. L'applicazione gestisce anche le autorizzazioni, quindi Helen, il contabile, non può accedere al repository SVN, ma Thomas e Mary, due programmatori, non possono accedere ai documenti relativi alla contabilità.

Jimmy, il grande capo (fondatore e CEO dell'azienda) ha privilegi molto specifici che nessuno ha. Può, ad esempio, chiudere l'intero sistema o licenziare una persona. Hai un'idea.

Il modello più scadente per tale applicazione è quello di avere classi come:

perchéladuplicazionedelcodicesipresenteràmoltorapidamente.Anchenell'esempiodibasediquattroimpiegati,duplichereteilcodicetraleclassiThomaseMary.QuestotispingeràacreareunaclassegenitorecomuneProgrammer.Dalmomentochepotrestiaverepiùcontabili,probabilmentecreeraiancheAccountantdiclasse.

Ora, si nota che avere la classe Helen non è molto utile, oltre a mantenere Thomas e Mary : la maggior parte del codice funziona comunque al livello superiore, a livello di ragionieri, programmatori e Jimmy. Al server SVN non interessa se sono Thomas o Mary a dover accedere al registro: deve solo sapere se si tratta di un programmatore o un contabile.

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

Quindi finisci per rimuovere le classi che non usi:

"Ma posso mantenere Jimmy così com'è, dato che ci sarà sempre un solo amministratore delegato, un grande capo, Jimmy", pensi. Inoltre, Jimmy è molto usato nel tuo codice, che in realtà sembra più simile a questo, e non come nell'esempio precedente:

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

Il problema con questo approccio è che Jimmy può ancora essere colpito da un autobus, e ci sarebbe un nuovo CEO. O il consiglio di amministrazione potrebbe decidere che Mary è così brava che dovrebbe essere un nuovo CEO, e Jimmy sarebbe retrocesso alla posizione di un venditore, quindi ora, devi passare attraverso tutti il tuo codice e cambia tutto.

    
risposta data 29.09.2014 - 17:00
fonte
15

È sciocco usare questo tipo di struttura di classe solo per variare i dettagli che dovrebbero ovviamente essere campi impostabili su un'istanza. Ma questo è specifico del tuo esempio.

Rendere diverse classi di Person diverse è quasi certamente una cattiva idea: dovresti cambiare il tuo programma e ricompilare ogni volta che una persona entra nel tuo dominio. Tuttavia, ciò non significa che ereditare da una classe esistente in modo che una stringa diversa venga restituita da qualche parte a volte non è utile.

La differenza è come ti aspetti che le entità rappresentate da quella classe possano variare. Si presume quasi sempre che le persone vadano e vengano e quasi tutte le applicazioni gravi che si occupano di utenti dovrebbero essere in grado di aggiungere e rimuovere utenti in fase di esecuzione. Ma se modellate cose come algoritmi di crittografia differenti, il supporto di una nuova è probabilmente un cambiamento importante in ogni caso, e ha senso inventare una nuova classe il cui metodo myName() restituisce una stringa diversa (e il cui metodo perform() fa presumibilmente qualcosa di diverso ).

    
risposta data 29.09.2014 - 17:03
fonte
12

Why would I create new classes when instances are enough?

Nella maggior parte dei casi, non lo faresti. Il tuo esempio è davvero un buon caso in cui questo comportamento non aggiunge valore reale.

Inoltre viola il principio Open Closed , in quanto le sottoclassi in fondo non sono un'estensione ma modificano quelle dei genitori funzionamento interno. Inoltre, il costruttore pubblico del genitore ora è irritante nelle sottoclassi e l'API diventa quindi meno comprensibile.

Tuttavia, a volte, se si dispone solo di una o due configurazioni speciali che vengono spesso utilizzate in tutto il codice, a volte è più conveniente e richiede meno tempo per creare una sottoclasse di un genitore con un costruttore complesso. In questi casi speciali, non vedo nulla di sbagliato in un simile approccio. Chiamiamolo costruttore che sta valutando j / k

    
risposta data 29.09.2014 - 17:03
fonte
8

Se questa è la portata della pratica, allora sono d'accordo che questa è una cattiva pratica.

Se John e Karl hanno comportamenti diversi, allora le cose cambiano un po '. Potrebbe essere che person abbia un metodo per cleanRoom(Room room) dove si scopre che John è un grande compagno di stanza e pulisce molto efficacemente, ma Karl non lo fa e non pulisce molto la stanza.

In questo caso, avrebbe senso farli essere la propria sottoclasse con il comportamento definito. Ci sono modi migliori per ottenere la stessa cosa (ad esempio, una classe CleaningBehavior ), ma almeno in questo modo non è una terribile violazione dei principi OO.

    
risposta data 29.09.2014 - 18:15
fonte
6

In alcuni casi sai che ci sarà solo un numero stabilito di istanze e quindi sarebbe OK (anche se a mio parere brutto) usare questo approccio. È meglio usare un enum se la lingua lo consente.

Esempio 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;
    }
}
    
risposta data 29.09.2014 - 17:35
fonte
6

Molte persone hanno già risposto. Ho pensato di dare la mia prospettiva personale.

C'era una volta che ho lavorato su un'app (e ancora faccio) che crea musica.

L'app ha una classe Scale astratta con diverse sottoclassi: CMajor , DMinor , ecc. Scale ha un aspetto simile a questo:

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

I generatori di musica hanno lavorato con un'istanza Scale specifica per generare musica. L'utente selezionerebbe una scala da un elenco, per generare musica da.

Un giorno mi è venuta un'idea interessante: perché non consentire all'utente di creare le proprie scale? L'utente seleziona le note da un elenco, preme un pulsante e una nuova scala verrà aggiunta all'elenco delle scale disponibili.

Ma non ero in grado di farlo. Questo perché tutte le scale sono già impostate in fase di compilazione, poiché sono espresse come classi. Poi mi ha colpito:

Spesso è intuitivo pensare in termini di "superclassi e sottoclassi". Quasi tutto può essere espresso attraverso questo sistema: superclasse Person e sottoclassi John e Mary ; superclasse Car e sottoclassi Volvo e Mazda ; superclasse Missile e sottoclasse SpeedRocked , LandMine e TrippleExplodingThingy .

È molto naturale pensare in questo modo, specialmente per la persona relativamente nuova a OO.

Ma dovremmo sempre ricordare che classi sono modelli e oggetti sono contenuti riversati in questi modelli . Puoi riversare qualsiasi contenuto nel modello, creando innumerevoli possibilità.

Non è compito della sottoclasse riempire il modello. È il lavoro dell'oggetto. Il compito della sottoclasse è di aggiungere funzionalità effettiva o espandere il modello .

Ed è per questo che avrei dovuto creare una concreta classe Scale , con un campo Note[] , e lasciare che gli oggetti riempiano questo modello ; possibilmente attraverso il costruttore o qualcosa del genere. E alla fine, così ho fatto.

Ogni volta che progetti un modello in una classe (ad esempio, un membro Note[] vuoto che deve essere riempito, o un campo String name a cui è necessario assegnare un valore), ricorda che è compito degli oggetti di questa classe riempire il modello (o possibilmente quelli che creano questi oggetti). Le sottoclassi hanno lo scopo di aggiungere funzionalità, non di compilare modelli.

Potresti essere tentato di creare un tipo di sistema "superclasse Person , sottoclassi John e Mary ", come hai fatto tu, perché ti piace la formalità che questo ti consente.

In questo modo, puoi semplicemente dire Person p = new Mary() , invece di Person p = new Person("Mary", 57, Sex.FEMALE) . Rende le cose più organizzate e più strutturate. Ma come abbiamo detto, creare una nuova classe per ogni combinazione di dati non è un buon approccio, dal momento che fa gonfiare il codice per nulla e ti limita in termini di capacità di runtime.

Quindi ecco una soluzione: utilizzare una fabbrica di base, potrebbe anche essere statica. Mi piace così:

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

    // ...
}

In questo modo, puoi facilmente utilizzare i 'preset' il 'come con il programma', in questo modo: Person mary = PersonFactory.createMary() , ma ti riservi anche il diritto di progettare nuove persone dinamicamente, ad esempio nel caso in cui tu voglia consentire all'utente di farlo. Per esempio:.

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

O ancora meglio: fai qualcosa del genere:

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

Sono stato portato via. Penso che tu abbia avuto l'idea.

Le sottoclassi non sono pensate per riempire i modelli impostati dalle loro superclassi. Le sottoclassi hanno lo scopo di aggiungere funzionalità . Oggetto è pensato per riempire i modelli, ecco a cosa servono.

Non dovresti creare una nuova classe per ogni possibile combinazione di dati. (Proprio come non avrei dovuto creare una nuova sottoclasse Scale per ogni possibile combinazione di Note s).

Ecco una linea guida: ogni volta che crei una nuova sottoclasse, valuta se aggiunge nuove funzionalità che non esistono nella superclasse. Se la risposta a questa domanda è "no", potresti provare a "riempire il modello" della superclasse, nel qual caso crea un oggetto. (e possibilmente una fabbrica con "preset" ', per rendere la vita più facile).

Spero che ti aiuti.

    
risposta data 01.10.2014 - 14:48
fonte
2

È considerato un cattiva pratica avere il codice duplicato .

Questo è almeno parzialmente perché linee di codici e pertanto la dimensione del codebase è correlata ai costi e ai difetti di manutenzione .

Ecco la logica del senso comune pertinente per me su questo particolare argomento:

1) Se dovessi cambiare questo, quanti posti dovrei visitare? Meno è meglio qui.

2) C'è una ragione per cui è fatta in questo modo? Nel tuo esempio, non potrebbe esserci, ma in alcuni esempi potrebbe esserci qualche motivo per farlo. Una cosa che mi viene in mente è in alcuni framework come i primi Grails in cui l'ereditarietà non ha necessariamente funzionato bene con alcuni plugin per GORM. Pochi e lontani tra.

3) È più pulito e più facile da capire in questo modo? Di nuovo, non è nel tuo esempio - ma ci sono alcuni modelli di ereditarietà che possono essere più di un tratto in cui le classi separate sono effettivamente più facili da mantenere (vengono in mente alcuni usi del comando o dei modelli di strategia).

    
risposta data 29.09.2014 - 22:46
fonte
2

Questa è un'eccezione a "dovrebbe essere un'istanza" qui - anche se leggermente estrema.

Se si desidera che il compiler imponga le autorizzazioni per accedere ai metodi in base al fatto che sia Karl o John (in questo esempio) piuttosto che chiedere all'implementazione del metodo di verificare quale istanza è stata passata, si potrebbe usare classi diverse. Applicando diverse classi piuttosto che un'istanza, si ottengono controlli di differenziazione tra "Karl" e "John" possibili in fase di compilazione piuttosto che in fase di esecuzione e questo potrebbe essere utile, in particolare in un contesto di sicurezza in cui il compilatore può essere più affidabili dei controlli del codice di runtime delle (ad esempio) librerie esterne.

    
risposta data 29.09.2014 - 19:47
fonte
0

C'è un caso d'uso: formare un riferimento a una funzione o un metodo come un oggetto normale.

Puoi vederlo molto nel codice della GUI di Java:

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

Il compilatore ti aiuta qui creando una nuova sottoclasse anonima.

    
risposta data 30.09.2014 - 11:19
fonte