Sottoclassi di sola costruzione: si tratta di un anti-pattern?

37

Avevo una discussione con un collega, e abbiamo finito per avere intuizioni contrastanti sullo scopo della sottoclasse. La mia intuizione è che se una funzione primaria di una sottoclasse è di esprimere un intervallo limitato di valori possibili del suo genitore, allora probabilmente non dovrebbe essere una sottoclasse. Ha sostenuto l'intuizione opposta: quella sottoclasse rappresenta l'essere di un oggetto più "specifico", e quindi una relazione sottoclasse è più appropriata.

Per mettere la mia intuizione in modo più concreto, penso che se ho una sottoclasse che estende una classe genitore, ma l'unico codice che la sottoclasse sovrascrive è un costruttore (sì, so che i costruttori generalmente non "sovrascrivono", sopportami ), quindi quello che era veramente necessario era un metodo di supporto.

Ad esempio, considera questa classe un po 'di vita reale:

public class DataHelperBuilder
{
    public string DatabaseEngine { get; set; }
    public string ConnectionString { get; set; }

    public DataHelperBuilder(string databaseEngine, string connectionString)
    {
        DatabaseEngine = databaseEngine;
        ConnectionString = connectionString;
    }

    // Other optional "DataHelper" configuration settings omitted

    public DataHelper CreateDataHelper()
    {
        Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
        DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
        dh.SetConnectionString(ConnectionString);

        // Omitted some code that applies decorators to the returned object
        // based on omitted configuration settings

        return dh;
    }
}

La sua richiesta è che sarebbe del tutto appropriato avere una sottoclasse come questa:

public class SystemDataHelperBuilder
{
    public SystemDataHelperBuilder()
        : base(Configuration.GetSystemDatabaseEngine(),
               Configuration.GetSystemConnectionString())
    {
    }
 }

Quindi, la domanda:

  1. Tra le persone che parlano di modelli di progettazione, quale di queste intuizioni è corretta? La sottoclasse è come descritto sopra un anti-pattern?
  2. Se si tratta di un anti-pattern, come si chiama?

Mi scuso se questa risulta essere stata una risposta facilmente googleable; le mie ricerche su google hanno per lo più restituito informazioni sull'anti-pattern del costruttore telescopico e non proprio quello che stavo cercando.

    
posta HardlyKnowEm 26.02.2015 - 20:27
fonte

11 risposte

54

Se tutto ciò che vuoi fare è creare la classe X con determinati argomenti, la sottoclasse è un modo strano di esprimere quell'intenzione, perché non stai usando nessuna delle caratteristiche che ti danno le classi e l'ereditarietà. Non è proprio un anti-pattern, è solo strano e un po 'inutile (a meno che tu non abbia altre ragioni per farlo). Un modo più naturale di esprimere questo intento sarebbe un metodo di fabbrica , che in questo caso è un nome di fantasia per il tuo "metodo di supporto".

Riguardo all'intuizione generale, sia "più specifico" che "un intervallo limitato" sono modi potenzialmente pericolosi di pensare alle sottoclassi, perché entrambe implicano che fare di Square una sottoclasse di Rettangolo sia una buona idea. Senza fare affidamento su qualcosa di formale come LSP, direi che una intuizione migliore è che una sottoclasse fornisce un'implementazione dell'interfaccia di base, oppure estende l'interfaccia per aggiungere alcune nuove funzionalità .

    
risposta data 26.02.2015 - 20:45
fonte
16

Which of these intuitions is correct?

Il tuo collega è corretto (supponendo che i sistemi di tipo standard).

Pensaci, le classi rappresentano i possibili valori legali. Se la classe A ha un campo byte F , potresti essere propenso a pensare che A abbia 256 valori legali, ma che l'intuizione non è corretta. A sta limitando "ogni permutazione di valori mai" a "deve avere il campo F essere 0-255".

Se estendi A con B che ha un altro campo byte FF , cioè aggiunta di restrizioni . Invece di "valore in cui F è byte", hai "valore dove F è byte e & FF è anche byte". Poiché stai mantenendo le vecchie regole, tutto ciò che ha funzionato per il basetype funziona ancora per il tuo sottotipo. Ma dal momento che stai aggiungendo regole, ti stai ulteriormente specializzando in "potrebbe essere qualsiasi cosa".

O per pensarci in un altro modo, supponiamo che A abbia un gruppo di sottotipi: B , C e D . Una variabile di tipo A potrebbe essere uno qualsiasi di questi sottotipi, ma una variabile di tipo B è più specifica. Non può essere in C o in D .

Is this an anti-pattern?

Enh? Avere oggetti specializzati è utile e non chiaramente dannoso per il codice. Raggiungere quel risultato usando la sottotitoli è forse un po 'discutibile, ma dipende da quali strumenti hai a disposizione. Anche con strumenti migliori questa implementazione è semplice, diretta e solida.

Non considererei questo un anti-pattern poiché non è chiaramente sbagliato e cattivo in qualsiasi situazione.

    
risposta data 26.02.2015 - 20:38
fonte
12

No, non è un anti-pattern. Posso pensare a una serie di casi d'uso pratico per questo:

  1. Se si desidera utilizzare la verifica del tempo di compilazione per assicurarsi che le raccolte di oggetti siano conformi solo a una determinata sottoclasse. Ad esempio, se hai MySQLDao e SqliteDao nel tuo sistema, ma per qualche motivo vuoi assicurarti che una raccolta contenga solo dati da una fonte, se usi sottoclassi mentre descrivi puoi fare in modo che il compilatore verifichi questo particolare correttezza. D'altra parte, se si archivia l'origine dati come un campo, questo diventerebbe un controllo in fase di esecuzione.
  2. La mia applicazione corrente ha una relazione uno a uno tra AlgorithmConfiguration + ProductClass e AlgorithmInstance . In altre parole, se configura FooAlgorithm per ProductClass nel file delle proprietà, puoi ottenere solo un FooAlgorithm per quella classe di prodotto. L'unico modo per ottenere due FooAlgorithm s per un dato ProductClass è creare una sottoclasse FooBarAlgorithm . Questo va bene, perché rende il file delle proprietà facile da usare (non c'è bisogno di sintassi per molte diverse configurazioni in una sezione nel file delle proprietà) ed è molto raro che ci sia più di un'istanza per una data classe di prodotto.
  3. @ Telastyn's answer offre un altro buon esempio.

In conclusione: non c'è davvero alcun danno nel farlo. Un Anti-pattern è definito come:

An anti-pattern (or antipattern) is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive.

Non c'è il rischio di essere altamente controproducenti qui, quindi non è un anti-modello.

    
risposta data 26.02.2015 - 20:44
fonte
9

Se qualcosa è un pattern o un antipattern dipende in modo significativo dal linguaggio e dall'ambiente in cui stai scrivendo. Ad esempio, for loops sono uno schema in assembly, C e lingue simili e un anti-pattern in lisp.

Lascia che ti racconti una storia di un codice che ho scritto molto tempo fa ...

Era in una lingua chiamata LPC e implementato un framework per lanciare incantesimi in un gioco. Avevi una superclasse magica, che aveva una sottoclasse di incantesimi di combattimento che gestivano alcuni assegni, e poi una sottoclasse per incantesimi a danno diretto bersaglio singolo, che era quindi sottoclasse ai singoli incantesimi: missile magico, fulmine e simili.

La natura di come ha funzionato la LPC era che avevi un oggetto principale che era un Singleton. prima di andare 'eww Singleton' - era e non era affatto "eww". Uno non ha fatto copie degli incantesimi, ma piuttosto ha invocato i metodi (effettivamente) statici negli incantesimi. Il codice per il missile magico sembrava:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

E lo vedi? È una classe solo costruttore. Imposta alcuni valori che erano nella sua classe astratta genitore (no, non dovrebbe usare setter - è immutabile) e così è stato. Ha funzionato giusto . È facile da codificare, facile da estendere e facile da capire.

Questo tipo di schema si traduce in altre situazioni in cui si ha una sottoclasse che estende una classe astratta e imposta alcuni valori propri, ma tutte le funzionalità si trovano nella classe astratta che eredita. Dai un'occhiata a StringBuilder in Java per un esempio di questo - non solo per il costruttore, ma sono spinto a trovare molta logica nei metodi stessi (tutto è in AbstractStringBuilder).

Questa non è una brutta cosa, ed è certamente non un anti-pattern (e in alcuni linguaggi potrebbe essere un pattern stesso).

    
risposta data 26.02.2015 - 23:35
fonte
2

L'uso più comune di tali sottoclassi che posso pensare sono le gerarchie di eccezioni, sebbene si tratti di un caso degenerato in cui generalmente non si definisce nulla nella classe, a condizione che il linguaggio ci consenta di ereditare i costruttori. Usiamo l'ereditarietà per esprimere che ReadError è un caso speciale di IOError , e così via, ma ReadError non ha bisogno di sovrascrivere alcun metodo di IOError .

Facciamo questo perché le eccezioni vengono catturate controllandone il tipo. Pertanto, abbiamo bisogno di codificare la specializzazione nel tipo in modo che qualcuno possa catturare solo ReadError senza catturare tutto IOError , se lo desiderano.

Normalmente, controllare il tipo di cose è una forma terribilmente cattiva e cerchiamo di evitarlo. Se riusciamo ad evitarlo, non è necessario esprimere le specializzazioni nel sistema dei tipi.

Potrebbe comunque essere utile, ad esempio se il nome del tipo appare nella forma stringa registrata dell'oggetto: questa è la lingua e possibilmente specifica della struttura. In linea di principio, potresti sostituire il nome del tipo in qualsiasi sistema con una chiamata a un metodo della classe, nel qual caso avrebbe l'override più del solo costruttore in SystemDataHelperBuilder , avremmo anche sostituisci loggingname() . Quindi, se c'è una tale registrazione nel tuo sistema, potresti immaginare che sebbene la tua classe sostituisca letteralmente solo il costruttore, "moralmente" sta anche sovrascrivendo il nome della classe, e quindi per scopi pratici non è una sottoclasse di solo costruttore. Ha un comportamento diverso desiderabile, è solo che ottieni questo comportamento gratuitamente senza scrivere alcun metodo di override, grazie a un uso astuto della riflessione altrove.

Quindi, direi che il suo codice può essere una buona idea se c'è qualche codice altrove che in qualche modo controlla le istanze di SystemDataHelperBuilder , esplicitamente con un ramo (come l'eccezione che cattura i rami in base al tipo) o forse perché c'è qualche codice generico che finirà per utilizzare il nome SystemDataHelperBuilder in un modo utile (anche se si tratta solo di registrazione).

Tuttavia, l'uso di tipi per casi speciali genera confusione, poiché se ImmutableRectangle(1,1) e ImmutableSquare(1) si comportano in modo in qualche modo allora alla fine qualcuno si chiederà perché e forse non lo farà 't. A differenza delle gerarchie di eccezioni, si controlla se un rettangolo è un quadrato controllando se height == width , non con instanceof . Nel tuo esempio, c'è una differenza tra un SystemDataHelperBuilder e un DataHelperBuilder creato con gli stessi stessi argomenti, e questo può causare problemi. Pertanto, una funzione per restituire un oggetto creato con gli argomenti "giusti" mi sembra normalmente la cosa giusta da fare: la sottoclasse genera due diverse rappresentazioni della "stessa" cosa, e aiuta solo se stai facendo qualcosa che tu normalmente proverei a non fare.

Nota che sono non che parlo in termini di modelli di design e anti-pattern, perché non credo che sia possibile fornire una tassonomia di tutte le buone e cattive idee che un programmatore ha mai pensato. Quindi, non ogni idea è un pattern o anti-pattern, diventa solo quando qualcuno identifica che si verifica ripetutamente in diversi contesti e lo nomina ;-) Non sono a conoscenza di alcun nome particolare per questo tipo di sottoclasse.

    
risposta data 27.02.2015 - 13:55
fonte
2

La più rigorosa definizione orientata agli oggetti di una relazione di sottoclasse è conosciuta come «is-a». Usando l'esempio tradizionale di un quadrato e un rettangolo, un quadrato «è-a» rettangolo.

Con questo, dovresti usare la sottoclasse per ogni situazione in cui ritieni che "is-a" sia una relazione significativa per il tuo codice.

Nel tuo caso specifico di una sottoclasse con nient'altro che un costruttore, dovresti analizzarlo da una prospettiva «is-a». È chiaro che un SystemDataHelperBuilder «è-a» DataHelperBuilder, quindi il primo passaggio suggerisce che è un uso valido.

La domanda a cui dovresti rispondere è se uno qualsiasi degli altri codici sfrutta questa relazione "è-a". Qualche altro codice tenta di distinguere SystemDataHelperBuilders dalla popolazione generale di DataHelperBuilders? Se è così, la relazione è abbastanza ragionevole, e dovresti mantenere la sottoclasse.

Tuttavia, se nessun altro codice fa questo, allora quello che hai è un rapporto che potrebbe essere una relazione «è-a», o potrebbe essere implementato con una fabbrica. In questo caso, potreste andare in entrambi i modi, ma raccomanderei una relazione Factory piuttosto che una "is-a". Quando si analizza il codice orientato agli oggetti scritto da un altro individuo, la gerarchia delle classi è una fonte importante di informazioni. Fornisce le relazioni «è-a» di cui avranno bisogno per dare un senso al codice. Di conseguenza, mettere questo comportamento in una sottoclasse promuove il comportamento da "qualcosa che qualcuno troverà se scaverà nei metodi della superclasse" a "qualcosa che tutti guarderanno prima ancora di iniziare a tuffarsi nel codice". Citando Guido van Rossum, "il codice viene letto più spesso di quanto non sia scritto", quindi una riduzione della leggibilità del codice per i futuri sviluppatori è un prezzo difficile da pagare. Vorrei scegliere di non pagarlo, se invece potessi usare una fabbrica.

    
risposta data 27.02.2015 - 18:17
fonte
1

Un sottotipo non è mai "sbagliato" purché sia sempre possibile sostituire un'istanza del suo supertipo con un'istanza del sottotipo e fare in modo che tutto funzioni correttamente. Questo controlla fino a quando la sottoclasse non cerca mai di indebolire nessuna delle garanzie che il supertipo fa. Può rendere più forti (più specifiche) le garanzie, quindi in tal senso l'intuizione del tuo collega è corretta.

Dato che, la sottoclasse che hai creato probabilmente non è sbagliata (dal momento che non so esattamente cosa fanno, non posso dirlo con certezza.) Devi stare attento al fragile problema della classe base, e tu devi anche considerare se un interface ti avvantaggerebbe, ma è vero per tutti gli usi dell'ereditarietà.

    
risposta data 26.02.2015 - 21:49
fonte
1

Penso che la chiave per rispondere a questa domanda sia guardare a questo particolare scenario di utilizzo.

L'ereditarietà viene utilizzata per applicare le opzioni di configurazione del database, come la stringa di connessione.

Questo è un pattern anti perché la classe sta violando il Principio di Responsabilità Unica --- Si sta configurando con una fonte specifica di quella configurazione e non "Facendo una cosa, e facendolo bene". La configurazione di una classe non dovrebbe essere inclusa nella classe stessa. Per impostare le stringhe di connessione del database, la maggior parte delle volte ho visto accadere a livello di applicazione durante un qualche tipo di evento che si verifica all'avvio dell'applicazione.

    
risposta data 26.02.2015 - 22:05
fonte
1

Uso le sottoclassi del costruttore solo abbastanza regolarmente per esprimere concetti diversi.

Ad esempio, considera la seguente classe:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

Possiamo quindi creare una sottoclasse:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

È quindi possibile utilizzare un CapitalizeAndPrintOutputter in qualsiasi punto del codice e sapere esattamente quale tipo di output è. Inoltre, questo modello rende davvero molto facile usare un contenitore DI per creare questa classe. Se il contenitore DI è a conoscenza di PrintOutputMethod e Capitalizer, può anche creare automaticamente CapitalizeAndPrintOutputter per te.

L'utilizzo di questo modello è davvero molto flessibile e utile. Sì, puoi usare i metodi di fabbrica per fare la stessa cosa, ma poi (almeno in C #) perdi parte della potenza del sistema di tipi e certamente l'uso dei contenitori DI diventa più ingombrante.

    
risposta data 27.02.2015 - 00:58
fonte
1

Consentitemi di notare innanzitutto che mentre il codice è scritto, la sottoclasse non è in realtà più specifica del genitore, poiché i due campi inizializzati sono entrambi settabili dal codice client.

Un'istanza della sottoclasse può essere distinta solo utilizzando un downcast.

Ora, se si dispone di un progetto in cui le proprietà DatabaseEngine e ConnectionString sono state reimplementate dalla sottoclasse, che accede a Configuration per farlo, allora si ha un vincolo che ha più senso di avere la sottoclasse.

Detto questo, "anti-pattern" è troppo strong per i miei gusti; non c'è niente di veramente sbagliato qui.

    
risposta data 27.02.2015 - 04:15
fonte
0

Nell'esempio che hai dato, il tuo partner ha ragione. I due costruttori di dati helper sono strategie. Uno è più specifico dell'altro, ma sono entrambi intercambiabili una volta inizializzati.

Fondamentalmente se un client di datahelperbuilder dovesse ricevere il systemdatahelperbuilder, nulla si romperebbe. Un'altra opzione sarebbe stata avere systemdatahelperbuilder come istanza statica della classe datahelperbuilder.

    
risposta data 27.02.2015 - 16:55
fonte