Va bene avere oggetti che si lanciano da soli, anche se inquinano l'API delle loro sottoclassi?


33

Ho una classe di base, Base. Ha due sottoclassi Sub1e Sub2. Ogni sottoclasse ha alcuni metodi aggiuntivi. Ad esempio, Sub1ha Sandwich makeASandwich(Ingredients... ingredients)e Sub2ha boolean contactAliens(Frequency onFrequency).

Poiché questi metodi adottano parametri diversi e fanno cose completamente diverse, sono completamente incompatibili e non posso semplicemente usare il polimorfismo per risolvere questo problema.

Basefornisce la maggior parte delle funzionalità e ho una vasta collezione di Baseoggetti. Tuttavia, tutti gli Baseoggetti sono o a Sub1o a Sub2volte e a volte ho bisogno di sapere quali sono.

Sembra una cattiva idea fare quanto segue:

for (Base base : bases) {
    if (base instanceof Sub1) {
        ((Sub1) base).makeASandwich(getRandomIngredients());
        // ... etc.
    } else { // must be Sub2
        ((Sub2) base).contactAliens(getFrequency());
        // ... etc.
    }
}

Quindi ho escogitato una strategia per evitarlo senza lanciare. Baseora ha questi metodi:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

E, naturalmente, Sub1implementa questi metodi come

boolean isSub1() { return true; }
Sub1 asSub1();   { return this; }
Sub2 asSub2();   { throw new IllegalStateException(); }

E Sub2li implementa in modo opposto.

Purtroppo, ora Sub1e Sub2hanno questi metodi nella loro API. Quindi posso farlo, per esempio, su Sub1.

/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1();   { return this; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2();   { throw new IllegalStateException(); }

In questo modo, se si sa che l'oggetto è solo a Base, questi metodi non sono deprecati e possono essere usati per "lanciarsi" su un tipo diverso in modo da poter invocare i metodi della sottoclasse su di esso. Questo mi sembra elegante in un certo senso, ma d'altra parte sto abusando delle annotazioni obsolete come un modo per "rimuovere" i metodi da una classe.

Dal momento che Sub1un'istanza è davvero una Base, ha senso usare l'ereditarietà piuttosto che l'incapsulamento. Quello che sto facendo è buono? C'è un modo migliore per risolvere questo problema?


12
Tutte e 3 le classi ora devono conoscersi. L'aggiunta di Sub3 comporterebbe molte modifiche al codice e l'aggiunta di Sub10 sarebbe decisamente dolorosa
Dan Pichelman,

15
Sarebbe di grande aiuto se ci fornissi un codice reale. Ci sono situazioni in cui è appropriato prendere decisioni basate sulla particolare classe di qualcosa, ma è impossibile dire se sei giustificato in quello che stai facendo con esempi così inventati. Per quello che vale, quello che vuoi è un Visitatore o un'unione taggata .
Doval,

1
Mi dispiace, ho pensato che sarebbe stato più facile fornire esempi semplificati. Forse posterò una nuova domanda con un ambito più ampio di ciò che voglio fare.
codebreaker,

12
Stai semplicemente reimplementando il casting e instanceof, in un modo che richiede molta digitazione, è soggetto a errori e rende difficile aggiungere più sottoclassi.
user253751

5
Se Sub1e Sub2non possono essere usati in modo intercambiabile, allora perché li trattate come tali? Perché non tenere traccia dei tuoi "produttori di sandwich" e dei "personaggi alieni" separatamente?
Pieter Witvoet,

Risposte:


27

Non ha sempre senso aggiungere funzioni alla classe base, come suggerito in alcune delle altre risposte. L'aggiunta di troppe funzioni di casi speciali può comportare l'associazione di componenti altrimenti non correlati.

Ad esempio potrei avere una Animalclasse, con Cate Dogcomponenti. Se voglio essere in grado di stamparli o mostrarli nella GUI, potrebbe essere eccessivo aggiungere renderToGUI(...)e sendToPrinter(...)alla classe base.

L'approccio che stai usando, usando i controlli di tipo e i cast, è fragile, ma almeno mantiene separate le preoccupazioni.

Tuttavia, se ti ritrovi a fare questi tipi di controlli / cast spesso, un'opzione è quella di implementare il modello visitatore / doppio invio per questo. Sembra un po 'così:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

Ora il tuo codice diventa

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

Il vantaggio principale è che se aggiungi una sottoclasse, otterrai errori di compilazione anziché casi mancanti in silenzio. La nuova classe visitatore diventa anche un bel bersaglio per attirare funzioni. Per esempio potrebbe avere senso per muoversi getRandomIngredients()in ActOnBase.

Puoi anche estrarre la logica del loop: ad esempio il frammento sopra potrebbe diventare

BaseVisitor.applyToArray(bases, new ActOnBase() );

Un altro po 'di massaggio e l'utilizzo delle lambda e dello streaming di Java 8 ti permetterebbero di farlo

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

Quale IMO è più bello e succinto che puoi ottenere.

Ecco un esempio più completo di Java 8:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));

+1: Questo tipo di cose è comunemente usato in linguaggi che hanno un supporto di prim'ordine per tipi di somma e corrispondenze di pattern, ed è un progetto valido in Java, anche se sintatticamente è molto brutto.
Mankarse,

@Mankarse Un po 'di zucchero sintattico può renderlo molto lontano dall'essere brutto - l'ho aggiornato con un esempio.
Michael Anderson,

Diciamo ipoteticamente che il PO cambia idea e decide di aggiungere un Sub3. Il visitatore non ha lo stesso identico problema di instanceof? Ora è necessario aggiungere onSub3al visitatore e si rompe se si dimentica.
Radiodef,

1
@Radiodef Un vantaggio derivante dall'uso diretto del modello visitatore al passaggio diretto al tipo è che una volta aggiunto onSub3all'interfaccia visitatore, si ottiene un errore di compilazione in ogni posizione in cui si crea un nuovo visitatore che non è stato ancora aggiornato. Al contrario, l'attivazione del tipo genererà nel migliore dei casi un errore di runtime, che può essere più complicato da attivare.
Michael Anderson,

1
@FiveNine, se sei felice di aggiungere l'esempio completo alla fine della risposta che potrebbe aiutare gli altri.
Michael Anderson,

83

Dal mio punto di vista: il tuo design è sbagliato .

Tradotto in linguaggio naturale, stai dicendo quanto segue:

Dato che abbiamo animals, ci sono catse fish. animalshanno proprietà comuni a catse fish. Ma non è abbastanza: ci sono alcune proprietà, che si differenziano catda fish, quindi è necessario sottoclassare.

Ora hai il problema, che hai dimenticato di modellare il movimento . Va bene. È relativamente facile:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

Ma questo è un design sbagliato. Il modo corretto sarebbe:

for(Animal a : animals){
    animal.move()
}

Dove movesarebbero condivisi comportamenti attuati in modo diverso da ciascun animale.

Poiché questi metodi adottano parametri diversi e fanno cose completamente diverse, sono completamente incompatibili e non posso semplicemente usare il polimorfismo per risolvere questo problema.

Questo significa: il tuo design è rotto.

La mia raccomandazione: refactoring Base, Sub1e Sub2.


8
Se offri un codice reale, potrei formulare raccomandazioni.
Thomas Junk,

9
@codebreaker Se sei d'accordo che il tuo esempio non è stato buono, ti consiglio di accettare questa risposta e di scrivere una nuova domanda. Non rivedere la domanda per avere un esempio diverso o le risposte esistenti non avranno senso.
Moby Disk,

16
@codebreaker Penso che questo "risolva" il problema rispondendo alla vera domanda. La vera domanda è: come posso risolvere il mio problema? Rifattorizza correttamente il codice. Refactoring in modo che .move()o .attack()e adeguatamente astratto thatparte - invece di avere .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home(), ecc Come si refactoring correttamente? Sconosciuto senza esempi migliori ... ma generalmente la risposta giusta - a meno che il codice di esempio e ulteriori dettagli suggeriscano diversamente.
WernerCD,

11
@codebreaker Se la tua gerarchia di classi ti porta a situazioni come questa, allora per definizione non ha senso
Patrick Collins,

7
@codebreaker In relazione all'ultimo commento di Crisfole, c'è un altro modo di vederlo che potrebbe aiutare. Ad esempio, con movevs fly/ run/ etc, che può essere spiegato in una frase come: Dovresti dire all'oggetto cosa fare, non come farlo. In termini di accessori, come accennate è più simile al caso reale in un commento qui, dovreste porre le domande dell'oggetto, non controllarne lo stato.
Izkata,

9

È un po 'difficile immaginare una circostanza in cui hai un gruppo di cose e vuoi che facciano un panino o contatti gli alieni. Nella maggior parte dei casi in cui trovi tale casting, opererai con un tipo, ad esempio in clang filtrerai un insieme di nodi per le dichiarazioni in cui getAsFunction restituisce un valore non nullo, anziché fare qualcosa di diverso per ciascun nodo nell'elenco.

Potrebbe essere necessario eseguire una sequenza di azioni e in realtà non è rilevante che gli oggetti che eseguono l'azione siano correlati.

Quindi, anziché un elenco di Base, lavorare sull'elenco di azioni

for (RandomAction action : actions)
   action.act(context);

dove

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

Se necessario, puoi fare in modo che Base attui un metodo per restituire l'azione, o qualsiasi altro mezzo tu debba selezionare l'azione dalle istanze nel tuo elenco di base (poiché dici che non puoi usare il polimorfismo, quindi presumibilmente l'azione da intraprendere non è una funzione della classe ma qualche altra proprietà delle basi; altrimenti daresti semplicemente il metodo Base the act (Context))


2
Capisci che il mio tipo di metodi assurdi sono progettati per mostrare le differenze in ciò che le classi possono fare una volta che la loro sottoclasse è nota (e che non hanno nulla a che fare l'una con l'altra), ma penso che avere un Contextoggetto peggiori le cose. Potrei anche passare Objectai metodi, lanciarli e sperare che siano del tipo giusto.
codebreaker,

1
@codebreaker allora hai davvero bisogno di chiarire la tua domanda - nella maggior parte dei sistemi ci sarebbe un motivo valido per chiamare un mucchio di funzioni agnostiche rispetto a ciò che fanno; ad esempio per elaborare regole su un evento o eseguire un passaggio in una simulazione. Di solito tali sistemi hanno un contesto in cui si verificano le azioni. Se 'getRandomIngredients' e 'getFrequency' non sono correlati, allora non dovrebbero essere nello stesso oggetto e hai bisogno di un approccio diverso, come fare in modo che le azioni catturino la fonte degli ingredienti o la frequenza.
Pete Kirkham,

1
Hai ragione e non credo di poter salvare questa domanda. Ho finito per scegliere un cattivo esempio, quindi probabilmente ne posterò un altro. GetFrequency () e getIngredients () erano solo segnaposto. Probabilmente avrei dovuto semplicemente mettere "..." come argomento per rendere più evidente che non avrei saputo quali fossero.
codebreaker,

Questa è l'IMO la risposta giusta. Comprendi che Contextnon è necessario che sia una nuova classe o addirittura un'interfaccia. Di solito il contesto è solo il chiamante, quindi le chiamate sono nel modulo action.act(this). Se getFrequency()e getRandomIngredients()sono metodi statici, potresti non aver nemmeno bisogno di un contesto. È così che direi, una coda di lavoro, che il tuo problema sembra molto terribile.
Domanda

Anche questo è per lo più identico alla soluzione del modello di visitatore. La differenza sta implementando i metodi Visitor :: OnSub1 () / Visitor :: OnSub2 () come Sub1 :: act () / Sub2 :: act ()
Domanda

4

Che ne dici se le tue sottoclassi implementano una o più interfacce che definiscono cosa possono fare? Qualcosa come questo:

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Quindi le tue lezioni saranno così:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

E il tuo for-loop diventerà:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

Due principali vantaggi di questo approccio:

  • nessun casting coinvolto
  • puoi avere ciascuna sottoclasse implementare tutte le interfacce che desideri, il che fornisce una certa flessibilità per le modifiche future.

PS: Mi dispiace per i nomi delle interfacce, non riuscivo a pensare a niente di più interessante in quel particolare momento: D.


3
Questo in realtà non risolve il problema. Hai ancora bisogno del cast nel ciclo for. (Se non lo fai, non hai nemmeno bisogno del cast nell'esempio di OP).
Taemyr,

2

L'approccio può essere una buona nei casi in cui qualsiasi tipo di un famiglia sia direttamente utilizzabile come attuazione di alcune interfaccia che soddisfa un criterio o può essere usato per creare un'implementazione di tale interfaccia. I tipi di raccolta incorporati avrebbero beneficiato di questo modello IMHO, ma dal momento che non a scopo di esempio inventerò un'interfaccia di raccolta BunchOfThings<T>.

Alcune implementazioni di BunchOfThingssono mutabili; alcuni non lo sono. In molti casi, un oggetto Fred potrebbe voler contenere qualcosa che può usare come un BunchOfThingse sapere che nient'altro che Fred sarà in grado di modificarlo. Questo requisito può essere soddisfatto in due modi:

  1. Fred sa che contiene gli unici riferimenti a ciò BunchOfThings, e nessun riferimento esterno a quello dei BunchOfThingssuoi interni esiste ovunque nell'universo. Se nessun altro ha un riferimento al BunchOfThingssuo interno o, nessun altro sarà in grado di modificarlo, quindi il vincolo sarà soddisfatto.

  2. Né il suo BunchOfThings, né alcuno dei suoi interni a cui esistono riferimenti esterni, può essere modificato con qualsiasi mezzo. Se assolutamente nessuno può modificare a BunchOfThings, il vincolo sarà soddisfatto.

Un modo per soddisfare il vincolo sarebbe copiare incondizionatamente qualsiasi oggetto ricevuto (elaborazione ricorsiva di qualsiasi componente nidificato). Un altro sarebbe verificare se un oggetto ricevuto promette immutabilità e, in caso contrario, crearne una copia e fare altrettanto con qualsiasi componente nidificato. Un'alternativa, che dovrebbe essere più pulita della seconda e più veloce della prima, è quella di offrire un AsImmutablemetodo che richiede a un oggetto di fare una copia immutabile di se stesso (usando AsImmutablesu tutti i componenti nidificati che lo supportano).

Potrebbero anche essere previsti metodi correlati asDetached(da utilizzare quando il codice riceve un oggetto e non sa se vorrà mutarlo, nel qual caso un oggetto mutabile dovrebbe essere sostituito con un nuovo oggetto mutabile, ma un oggetto immutabile può essere mantenuto così com'è) asMutable(nel caso in cui un oggetto sappia che conterrà un oggetto precedentemente restituito asDetached, ovvero un riferimento non condiviso a un oggetto mutabile o un riferimento condivisibile a un oggetto mutabile) e asNewMutable(nei casi in cui il codice riceve un oggetto esterno riferimento e sa che vorrà mutare una copia dei dati al suo interno - se i dati in arrivo sono mutabili non c'è motivo di iniziare facendo una copia immutabile che verrà immediatamente utilizzata per creare una copia mutabile e quindi abbandonata).

Si noti che mentre i asXXmetodi possono restituire tipi leggermente diversi, il loro vero ruolo è garantire che gli oggetti restituiti soddisfino le esigenze del programma.


0

Ignorando la questione del fatto che tu abbia un buon design o meno, e supponendo che sia buono o almeno accettabile, vorrei considerare le capacità delle sottoclassi, non il tipo.

Pertanto, sia:


Sposta alcune conoscenze dell'esistenza di sandwich e alieni nella classe base, anche se sai che alcune istanze della classe base non possono farlo. Implementalo nella classe base per generare eccezioni e modifica il codice in:

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

Quindi una o entrambe le sottoclassi hanno la precedenza canMakeASandwich()e solo una implementa ciascuna di makeASandwich()e contactAliens().


Utilizzare le interfacce, non le sottoclassi concrete, per rilevare le capacità di un tipo. Lasciare sola la classe base e modificare il codice in:

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

o eventualmente (e sentiti libero di ignorare questa opzione se non si adatta al tuo stile, o del resto qualsiasi stile Java che considereresti sensato):

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

Personalmente non di solito come quest'ultimo stile di catturare un'eccezione mezzo del previsto, a causa del rischio di impropriamente cattura di un ClassCastExceptionuscendo getRandomIngredientso makeASandwich, ma YMMV.


2
La cattura di ClassCastException è solo ew. Questo è l'intero scopo di instanceof.
Radiodef,

@Radiodef: vero, ma uno scopo <è quello di evitare di catturare ArrayIndexOutOfBoundsException, e alcune persone decidono di farlo. Nessuna considerazione per il gusto ;-) Fondamentalmente il mio "problema" qui è che lavoro in Python, dove lo stile preferito di solito è che catturare qualsiasi eccezione è preferibile testare in anticipo se si verificherà l'eccezione, non importa quanto semplice sarebbe il test avanzato . Forse la possibilità non vale la pena menzionare in Java.
Steve Jessop,

@SteveJessop »È più facile chiedere perdono che autorizzazione« è lo slogan;)
Thomas Junk

0

Qui abbiamo un caso interessante di una classe base che si riduce alle proprie classi derivate. Sappiamo benissimo che questo è normalmente negativo, ma se vogliamo dire che abbiamo trovato una buona ragione, cerchiamo di guardare e vedere quali potrebbero essere i vincoli su questo:

  1. Possiamo coprire tutti i casi nella classe base.
  2. Nessuna classe di derivazione straniera dovrebbe mai aggiungere un nuovo caso.
  3. Possiamo convivere con la politica per la quale chiamare per essere sotto il controllo della classe base.
  4. Ma sappiamo dalla nostra scienza che la politica di cosa fare in una classe derivata per un metodo non nella classe base è quella della classe derivata e non della classe base.

Se 4, allora abbiamo: 5. La politica della classe derivata è sempre sotto lo stesso controllo politico della politica della classe base.

2 e 5 implicano entrambi direttamente che possiamo enumerare tutte le classi derivate, il che significa che non si suppone che ci siano classi derivate esterne.

Ma ecco la cosa. Se sono tutti tuoi, puoi sostituire l'if con un'astrazione che è una chiamata di metodo virtuale (anche se è una sciocchezza) e sbarazzarti dell'if e del cast personale. Pertanto, non farlo. È disponibile un design migliore.


-1

Inserisci un metodo astratto nella classe base che fa una cosa in Sub1 e l'altra in Sub2 e chiama quel metodo astratto nei tuoi loop misti.

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

Si noti che ciò potrebbe richiedere altre modifiche a seconda della definizione di getRandomIngredients e getFrequency. Ma, onestamente, all'interno della classe che contatta gli alieni è probabilmente un posto migliore per definire getFrequency comunque.

Per inciso, i tuoi metodi asSub1 e asSub2 sono decisamente cattive pratiche. Se hai intenzione di farlo, fallo lanciando. Non c'è niente che questi metodi ti diano che il casting non lo fa.


2
La tua risposta suggerisce che non hai letto completamente la domanda: il polimorfismo non la taglierà qui. Esistono diversi metodi su ciascuno di Sub1 e Sub2, questi sono solo esempi e "doAThing" non significa nulla. Non corrisponde a nulla di ciò che farei. Inoltre, come per la tua ultima frase: che ne pensi della sicurezza garantita del tipo?
codebreaker,

@codebreaker: corrisponde esattamente a quello che stai facendo nel loop nell'esempio. Se ciò non ha significato, non scrivere il ciclo. Se non ha senso come metodo, non ha senso come un corpo ad anello.
Casuale 832,

1
E i tuoi metodi "garantiscono" la sicurezza dei tipi allo stesso modo del casting: lanciando un'eccezione se l'oggetto è del tipo sbagliato.
Casuale 832,

Mi rendo conto di aver scelto un cattivo esempio. Il problema è che la maggior parte dei metodi nelle sottoclassi sono più simili ai getter che ai metodi mutativi. Queste classi non fanno le cose da sole, forniscono solo dati, per lo più. Il polimorfismo non mi aiuterà lì. Ho a che fare con produttori, non consumatori. Inoltre: generare un'eccezione è una garanzia di runtime, che non è buona quanto il tempo di compilazione.
codebreaker,

1
Il mio punto è che il tuo codice fornisce anche una garanzia di runtime. Non c'è nulla che impedisca a qualcuno di non controllare isSub2 prima di chiamare asSub2.
Casuale 832,

-2

Si potrebbe spingere sia makeASandwiche contactAliensnella classe di base e poi implementare con le implementazioni fittizie. Poiché i tipi / argomenti di ritorno non possono essere risolti in una classe base comune.

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

Ci sono evidenti inconvenienti in questo genere di cose, come ciò che ciò significa per il contratto del metodo e ciò che si può e non si può dedurre dal contesto del risultato. Non puoi pensare da quando non sono riuscito a fare un panino gli ingredienti si sono consumati nel tentativo ecc.


1
Ho pensato di farlo, ma viola il principio di sostituzione di Liskov e non è così bello lavorare con, ad esempio, quando si digita "sub1". e il completamento automatico viene visualizzato, vedi makeASandwich ma non contactAliens.
codebreaker,

-5

Quello che stai facendo è perfettamente legittimo. Non prestare attenzione agli oppositori che semplicemente ribadiscono il dogma perché lo leggono in alcuni libri. Dogma non ha posto nell'ingegneria.

Ho utilizzato lo stesso meccanismo un paio di volte e posso dire con sicurezza che il runtime java avrebbe potuto fare la stessa cosa in almeno un posto a cui riesco a pensare, migliorando così le prestazioni, l'usabilità e la leggibilità del codice che lo usa.

Prendiamo ad esempio java.lang.reflect.Member, che è la base di java.lang.reflect.Fielde java.lang.reflect.Method. (La gerarchia attuale è un po 'più complicata di così, ma questo è irrilevante.) Campi e metodi sono animali molto diversi: uno ha un valore che puoi ottenere o impostare, mentre l'altro non ha tale cosa, ma può essere invocato con un numero di parametri e può restituire un valore. Quindi, campi e metodi sono entrambi membri, ma le cose che puoi fare con loro sono diverse l'una dall'altra quanto fare sandwich e contattare alieni.

Ora, quando scriviamo un codice che usa la riflessione abbiamo molto spesso un Membernelle nostre mani e sappiamo che è o a Methodo Field(o, raramente, qualcos'altro), eppure dobbiamo fare tutto il noioso instanceofper capire con precisione di cosa si tratta e quindi dobbiamo lanciarlo per ottenere un riferimento adeguato ad esso. (E questo non è solo noioso, ma non funziona molto bene.) La Methodclasse avrebbe potuto implementare molto facilmente il modello che stai descrivendo, rendendo così più facile la vita di migliaia di programmatori.

Naturalmente, questa tecnica è praticabile solo in piccole gerarchie ben definite di classi strettamente accoppiate di cui hai (e avrai sempre) il controllo a livello di sorgente di: non vuoi fare una cosa del genere se la tua gerarchia di classi è rischia di essere esteso da persone che non sono libere di riformattare la classe base.

Ecco come ciò che ho fatto differisce da quello che hai fatto:

  • La classe base fornisce un'implementazione predefinita per l'intera asDerivedClass()famiglia di metodi, con ognuno di essi restituito null.

  • Ogni classe derivata sovrascrive solo uno dei asDerivedClass()metodi, restituendo thisinvece di null. Non ha la precedenza su tutto il resto, né vuole sapere nulla su di loro. Quindi, non IllegalStateExceptionvengono lanciate s.

  • La classe base fornisce anche finalimplementazioni per l'intera isDerivedClass()famiglia di metodi, codificata come segue: In return asDerivedClass() != null; questo modo, il numero di metodi che devono essere sostituiti da classi derivate è ridotto al minimo.

  • Non ho usato @Deprecatedquesto meccanismo perché non ci avevo pensato. Ora che mi hai dato l'idea, la metterò in pratica, grazie!

C # ha un meccanismo correlato incorporato tramite l'uso della asparola chiave. In C # puoi dire DerivedClass derivedInstance = baseInstance as DerivedClasse otterrai un riferimento a DerivedClassse tu fossi baseInstancedi quella classe o nullse non lo fosse. Questo (teoricamente) ha prestazioni migliori di quelle isseguite dal cast ( isè certamente la parola chiave C # meglio denominata per instanceof), ma il meccanismo personalizzato che abbiamo realizzato a mano funziona ancora meglio: la coppia di instanceofoperazioni -e-cast di Java in quanto l' asoperatore di C # non ha la stessa velocità del singolo metodo virtuale del nostro approccio personalizzato.

Con la presente ho avanzato la proposta secondo cui questa tecnica dovrebbe essere dichiarata come modello e che dovrebbe essere trovato un bel nome.

Accidenti, grazie per il voto negativo!

Un riassunto della controversia, per salvarti dalla difficoltà di leggere i commenti:

L'obiezione della gente sembra essere che il progetto originale fosse sbagliato, nel senso che non dovresti mai avere classi molto diverse derivanti da una classe base comune, o che anche se lo fai, il codice che utilizza tale gerarchia non dovrebbe mai essere nella posizione di avere un riferimento di base e la necessità di capire la classe derivata. Pertanto, dicono, il meccanismo di autoprofusione proposto da questa domanda e dalla mia risposta, che migliora l'uso del design originale, non avrebbe mai dovuto essere necessario in primo luogo. (In realtà non dicono nulla sul meccanismo stesso di auto-fusione, si lamentano solo della natura dei disegni a cui il meccanismo dovrebbe essere applicato.)

Tuttavia, nell'esempio di cui sopra mi hanno già dimostrato che i creatori del runtime Java hanno infatti scegliere il disegno di un proprio come per il java.lang.reflect.Member, Field, Methodgerarchia e nei commenti qui sotto ho anche mostrano che i creatori del C # runtime sono arrivati in modo indipendente ad un equivalente di progettazione per il System.Reflection.MemberInfo, FieldInfo, MethodInfogerarchia. Quindi, questi sono due diversi scenari del mondo reale che si trovano proprio sotto il naso di tutti e che hanno soluzioni dimostrabili realizzabili usando esattamente tali progetti.

Questo è ciò a cui si riducono tutti i seguenti commenti. Il meccanismo di autofusione è appena menzionato.


13
È legittimo se ti sei disegnato in una scatola, certo. Questo è il problema con molti di questi tipi di domande. Le persone prendono decisioni in altre aree del design che costringono questo tipo di soluzioni hacker quando la vera soluzione è capire perché sei finito in questa situazione in primo luogo. Un design decente non ti porterà qui. Attualmente sto guardando un'applicazione SLOC 200K e non vedo da nessuna parte che abbiamo bisogno di farlo. Quindi non è solo qualcosa che la gente legge in un libro. Non entrare in questo tipo di situazioni è semplicemente il risultato finale del seguire le buone pratiche.
Dunk,

3
@Dunk Ho appena dimostrato che esiste un bisogno di un tale meccanismo, ad esempio nella java.lang.reflection.Membergerarchia. I creatori del runtime Java si sono imbattuti in questo tipo di situazione, suppongo che non diresti che si sono progettati in una scatola o li incolpano per non aver seguito un qualche tipo di best practice? Quindi, il punto che sto sottolineando qui è che una volta che hai questo tipo di situazione questo meccanismo è un buon modo per migliorare le cose.
Mike Nakis,

6
@MikeNakis I creatori di Java hanno preso molte decisioni sbagliate, quindi da solo non dice molto. Ad ogni modo, usare un visitatore sarebbe meglio che fare un test ad-hoc-poi-cast.
Doval,

3
Inoltre, l'esempio è difettoso. C'è Memberun'interfaccia, ma serve solo a controllare i qualificatori di accesso. In genere, ottieni un elenco di metodi o proprietà e lo usi direttamente (senza mescolarli, quindi non List<Member>appare), quindi non è necessario eseguire il cast. Si noti che Classnon fornisce un getMembers()metodo. Certo, un programmatore indifferente potrebbe creare il List<Member>, ma avrebbe poco senso che renderlo un List<Object>e aggiungerne un po ' Integero Stringad esso.
SJuan76,

5
@MikeNakis "Molte persone lo fanno" e "Famoso lo fa" non sono veri argomenti. Gli antipatri sono sia cattive idee che ampiamente usati per definizione, eppure quelle scuse ne giustificherebbero l'uso. Fornisci veri motivi per usare un disegno o un altro. Il mio problema con il casting ad hoc è che ogni utente della tua API deve ottenere il casting giusto ogni volta che lo fa. Un visitatore deve essere corretto solo una volta. Nascondere il casting dietro alcuni metodi e tornare al nullposto di un'eccezione non lo rende molto meglio. A parità di tutto il resto, il visitatore è più sicuro mentre fa lo stesso lavoro.
Doval,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.