Esiste un linguaggio o un modello di progettazione che consenta la * rimozione * del comportamento o delle proprietà degli oggetti in una gerarchia di classi?


28

Un noto difetto delle gerarchie di classi tradizionali è che sono cattive quando si tratta di modellare il mondo reale. Ad esempio, cercando di rappresentare le specie animali con classi. In realtà ci sono diversi problemi nel farlo, ma uno a cui non ho mai visto una soluzione è quando una sottoclasse "perde" un comportamento o una proprietà definita in una superclasse, come un pinguino che non è in grado di volare (lì sono probabilmente esempi migliori, ma questo è il primo che mi viene in mente).

Da un lato, non si desidera definire, per ogni proprietà e comportamento, qualche flag che specifica se è presente, e controllarlo ogni volta prima di accedere a quel comportamento o proprietà. Vorresti solo dire che gli uccelli possono volare, in modo semplice e chiaro, nella classe Bird. Ma sarebbe bello se in seguito si potessero definire "eccezioni", senza dover usare alcuni orribili hack ovunque. Ciò accade spesso quando un sistema è stato produttivo per un po '. All'improvviso trovi una "eccezione" che non si adatta affatto al design originale e non vuoi modificare gran parte del codice per adattarlo.

Quindi, ci sono alcuni linguaggi o schemi di progettazione in grado di gestire in modo chiaro questo problema, senza richiedere importanti modifiche alla "superclasse" e tutto il codice che lo utilizza? Anche se una soluzione gestisce solo un caso specifico, diverse soluzioni potrebbero insieme formare una strategia completa.

Dopo aver pensato di più, mi rendo conto di aver dimenticato il principio di sostituzione di Liskov. Ecco perché non puoi farlo. Supponendo che tu definisca "tratti / interfacce" per tutti i principali "gruppi di caratteristiche", puoi implementare liberamente tratti in diversi rami della gerarchia, come il tratto Volante potrebbe essere implementato da Uccelli e alcuni tipi speciali di scoiattoli e pesci.

Quindi la mia domanda potrebbe equivalere a "Come potrei non implementare un tratto?" Se la tua superclasse è un serializzabile Java, devi esserlo anche tu, anche se non c'è modo di serializzare il tuo stato, ad esempio se contenevi un "socket".

Un modo per farlo è quello di definire sempre tutti i tratti in coppia dall'inizio: Flying e NotFlying (che genererebbe UnsupportedOperationException, se non confrontato). Il Non-tratto non definirebbe alcuna nuova interfaccia e potrebbe essere semplicemente verificato. Sembra una soluzione "economica", in particolare se utilizzata dall'inizio.


3
'senza dover usare alcuni hack orribili ovunque': disabilitare un comportamento È un hack orribile: implicherebbe che function save_yourself_from_crashing_airplane(Bird b) { f.fly() }sarebbe molto più complicato. (come ha detto Peter Török, viola LSP)
keppla

Una combinazione del modello di strategia e dell'ereditarietà potrebbe consentire di "comporre rispetto" al comportamento ereditato per specifici super tipi? Quando dici: " it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"consideri un metodo di fabbrica che controlla il comportamento hacky?
Stuper:

1
Ovviamente si potrebbe semplicemente lanciare un NotSupportedExceptionda Penguin.fly().
Felix Dombek,

Per quanto riguarda le lingue vanno, è certamente possibile un-implementare un metodo in una classe figlia. Per esempio, in Ruby: class Penguin < Bird; undef fly; end;. Se dovresti essere un'altra domanda.
Nathan Long,

Ciò romperebbe il principio di liskov e probabilmente l'intero punto di OOP.
deadalnix,

Risposte:


17

Come altri hanno già detto, dovresti andare contro LSP.

Tuttavia, si può sostenere che una sottoclasse è semplicemente un'estensione arbitraria di una superclasse. È un nuovo oggetto a sé stante e l'unica relazione con la super classe è che ha usato una base.

Questo può avere un senso logico, piuttosto che dire Penguin è un uccello. Il tuo detto Pinguino eredita alcuni sottogruppi di comportamento da Bird.

I linguaggi generalmente dinamici ti consentono di esprimerlo facilmente, un esempio che utilizza JavaScript è il seguente:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

In questo caso particolare, Penguinoscura attivamente il Bird.flymetodo che eredita scrivendo una flyproprietà con valore undefinedsull'oggetto.

Ora puoi dire che Penguinnon può più essere trattato come un normale Bird. Ma come detto, nel mondo reale semplicemente non può. Perché stiamo modellando Birdcome un'entità volante.

L'alternativa è di non fare l'ipotesi diffusa che Bird possa volare. Sarebbe sensato avere Birdun'astrazione che consenta a tutti gli uccelli di ereditare da esso, senza fallimento. Ciò significa solo fare ipotesi che tutte le sottoclassi possano contenere.

Generalmente l'idea di Mixin si applica bene qui. Avere una classe base molto sottile e mescolare tutti gli altri comportamenti in essa.

Esempio:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Se sei curioso, ho un'implementazione diObject.make

aggiunta:

Quindi la mia domanda potrebbe equivalere a "Come potrei non implementare un tratto?" Se la tua superclasse è un serializzabile Java, devi esserlo anche tu, anche se non c'è modo di serializzare il tuo stato, ad esempio se contenevi un "socket".

Non "non implementare" un tratto. Semplicemente correggi la tua gerarchia ereditaria. O puoi adempiere al tuo contratto di super classi o non dovresti far finta di essere di quel tipo.

Qui è dove brilla la composizione dell'oggetto.

A parte questo, serializzabile non significa che tutto dovrebbe essere serializzato, significa solo che "lo stato che ti interessa" dovrebbe essere serializzato.

Non dovresti usare un tratto "NotX". È solo un codice orribile. Se una funzione prevede un oggetto volante, dovrebbe schiantarsi e bruciarsi quando gli dai un mammut.


10
"nel mondo reale semplicemente non può". Sì, può. Un pinguino è un uccello. La capacità di volare non è una proprietà dell'uccello, è semplicemente una proprietà casuale della maggior parte delle specie di uccelli. Le proprietà che definiscono gli uccelli sono "animali piumati, alati, bipedi, endotermici, che depongono le uova, vertebrati" (Wikipedia) - niente di volare lì.
pdr

2
@pdr di nuovo dipende dalla tua definizione di uccello. Mentre usavo il termine "Uccello" intendevo l'astrazione di classe che usiamo per rappresentare gli uccelli, incluso il metodo fly. Ho anche detto che puoi rendere la tua astrazione di classe meno specifica. Anche un pinguino non è piumato.
Raynos,

2
@Raynos: I pinguini sono davvero piumati. Le loro piume sono piuttosto corte e dense, ovviamente.
Jon Purdy,

@JonPurdy abbastanza giusto, immagino sempre che avessero la pelliccia.
Raynos,

+1 in generale, e in particolare per il "mammut". LOL!
Sebastien Diot,

28

AFAIK tutte le lingue basate sull'eredità sono basate sul principio di sostituzione di Liskov . Rimuovere / disabilitare una proprietà della classe base in una sottoclasse violerebbe chiaramente LSP, quindi non credo che tale possibilità sia implementata da nessuna parte. Il mondo reale è davvero disordinato e non può essere modellato con precisione da astrazioni matematiche.

Alcune lingue forniscono tratti o mixin, proprio per affrontare tali problemi in modo più flessibile.


1
LSP è per i tipi , non per le classi .
Jörg W Mittag,

2
@PéterTörök: questa domanda non esisterebbe altrimenti :-) Posso pensare a due esempi di Ruby. Classè una sottoclasse Moduleanche se ClassIS-NOT-A Module. Ma ha ancora senso essere una sottoclasse, poiché riutilizza molto del codice. OTOH, StringIOIS-A IO, ma i due non hanno alcuna relazione ereditaria (a parte l'ovvio di entrambi ereditare Object, ovviamente), perché non condividono alcun codice. Le classi sono per la condivisione del codice, i tipi sono per la descrizione dei protocolli. IOe StringIOhanno lo stesso protocollo, quindi lo stesso tipo, ma le loro classi non sono correlate.
Jörg W Mittag,

1
@ JörgWMittag, OK, ora capisco meglio cosa intendi. Tuttavia, per me il tuo primo esempio sembra più un uso improprio dell'eredità che l'espressione di qualche problema fondamentale che sembra suggerire. Eredità pubblica L'IMO non dovrebbe essere usato per riutilizzare l'implementazione, ma solo per esprimere relazioni di sottotipo (is-a). E il fatto che possa essere utilizzato in modo improprio non lo squalifica - non riesco a immaginare alcun strumento utilizzabile da qualsiasi dominio che non possa essere utilizzato in modo improprio.
Péter Török,

2
Alle persone che votano questa risposta: nota che questo non risponde alla domanda, specialmente dopo il chiarimento modificato. Non credo che questa risposta meriti un voto negativo, perché ciò che dice è molto vero e importante da sapere, ma non ha veramente risposto alla domanda.
deridere il

1
Immagina un Java in cui solo le interfacce sono tipi, le classi non lo sono e le sottoclassi sono in grado di "non implementare" le interfacce della loro superclasse e tu hai un'idea approssimativa, penso.
Jörg W Mittag,

15

Fly()è il primo esempio in: Head First Design Patterns for The Strategy Pattern , e questa è una buona situazione sul perché dovresti "Favorire la composizione sull'eredità". .

Puoi mescolare composizione ed eredità avendo dei supertipi FlyingBird, FlightlessBirdche hanno il comportamento corretto iniettato da una Fabbrica, che i sottotipi rilevanti, ad esempio, Penguin : FlightlessBirdottengono automaticamente, e qualsiasi altra cosa veramente specifica viene gestita dalla Fabbrica come ovvio.


1
Nella mia risposta ho citato il modello Decoratore, ma anche il modello Strategia funziona abbastanza bene.
deridere il

1
+1 per "Favorisci la composizione rispetto all'eredità". Tuttavia, la necessità di speciali modelli di progettazione per implementare la composizione in linguaggi tipicamente statici rafforza la mia propensione verso linguaggi dinamici come Ruby.
Roy Tinker,

11

Il vero problema che stai supponendo non Birdha un Flymetodo? Perchè no:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Ora l'ovvio problema è l'ereditarietà multipla ( Duck), quindi ciò di cui hai veramente bisogno sono le interfacce:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}

3
Il problema è che l'evoluzione non segue il principio di sostituzione di Liskov e eredita la rimozione delle funzionalità.
Donal Fellows,

7

Innanzitutto, SÌ, qualsiasi linguaggio che consenta una facile modifica dinamica degli oggetti ti consentirebbe di farlo. In Ruby, ad esempio, puoi rimuovere facilmente un metodo.

Ma come ha detto Péter Török, ciò violerebbe LSP .


In questa parte, mi dimenticherò di LSP e presumo che:

  • Bird è una classe con un metodo fly ()
  • Il pinguino deve ereditare da Bird
  • Il pinguino non può volare ()
  • Non mi importa se è un buon design o se corrisponde al mondo reale, poiché è l'esempio fornito in questa domanda.

Tu hai detto :

Da un lato, non si desidera definire per ogni proprietà e comportamento qualche flag che specifica se è presente, e controllarlo ogni volta prima di accedere a quel comportamento o proprietà

Sembra che ciò che vuoi sia che " chieda perdono piuttosto che permesso " da parte di Python

Basta fare in modo che il tuo Penguin generi un'eccezione o erediti da una classe NonFlyingBird che genera un'eccezione (pseudo codice):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

A proposito, qualunque cosa tu scelga: sollevare un'eccezione o rimuovere un metodo, alla fine, il seguente codice (supponendo che la tua lingua supporti la rimozione del metodo):

var bird:Bird = new Penguin();
bird.fly();

genererà un'eccezione di runtime.


"Fai semplicemente in modo che il tuo Penguin generi un'eccezione o erediti da una classe NonFlyingBird che genera un'eccezione" Questa è ancora una violazione di LSP. Sta ancora suggerendo che un Pinguino può volare, anche se la sua implementazione della mosca è destinata a fallire. Non dovrebbe mai esserci un metodo fly su Penguin.
pdr

@pdr: non sta suggerendo che un Pinguino possa volare, ma che dovrebbe volare (è un contratto). L'eccezione ti dirà che non può . A proposito, non sto affermando che sia una buona pratica OOP, sto solo dando una risposta a una parte della domanda
David

Il punto è che non ci si può aspettare che un pinguino voli solo perché è un uccello. Se voglio scrivere un codice che dice "Se x può volare, fai questo; altrimenti fallo". Devo usare un tentativo / cattura nella tua versione, in cui dovrei essere in grado di chiedere all'oggetto se può volare (esiste il metodo di fusione o controllo). Potrebbe essere proprio nel testo, ma la tua risposta implica che il lancio di un'eccezione è conforme a LSP.
pdr,

@pdr "Devo usare un tentativo / cattura nella tua versione" -> questo è il punto centrale di chiedere perdono piuttosto che permesso (perché anche un Anatra potrebbe aver rotto le ali e non essere in grado di volare). Riparerò la formulazione.
David,

"questo è il punto centrale di chiedere perdono piuttosto che permesso." Sì, tranne per il fatto che consente al framework di generare lo stesso tipo di eccezione per qualsiasi metodo mancante, quindi il "tentativo: tranne AttributeError:" di Python è esattamente equivalente a quello di C # se (X è Y) {} else {} "e immediatamente riconoscibile come tale. Ma se hai deliberatamente lanciato una CannotFlyException per sovrascrivere la funzionalità fly () predefinita in Bird, diventa meno riconoscibile.
pdr

7

Come qualcuno ha sottolineato sopra nei commenti, i pinguini sono uccelli, i pinguini non volano, ergo non tutti gli uccelli possono volare.

Quindi Bird.fly () non dovrebbe esistere o essere autorizzato a non funzionare. Preferisco il primo.

Avere FlyingBird estende Bird ha un metodo .fly () sarebbe corretto, ovviamente.


Sono d'accordo, Fly dovrebbe essere un'interfaccia che Bird può implementare. Potrebbe essere implementato anche come metodo con comportamenti predefiniti che possono essere sostituiti, ma un approccio più pulito utilizza un'interfaccia.
Jon Raynor,

6

Il vero problema con l'esempio fly () è che l'input e l'output dell'operazione non sono definiti correttamente. Cosa è necessario per far volare un uccello? E cosa succede dopo il volo ha successo? I tipi di parametro e i tipi restituiti per la funzione fly () devono contenere tali informazioni. Altrimenti il ​​tuo design dipende da effetti collaterali casuali e tutto può succedere. La parte qualsiasi è ciò che causa l'intero problema, l'interfaccia non è definita correttamente e sono consentiti tutti i tipi di implementazione.

Quindi, invece di questo:

class Bird {
public:
   virtual void fly()=0;
};

Dovresti avere qualcosa del genere:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

Ora definisce esplicitamente i limiti della funzionalità - il tuo comportamento di volo ha un solo galleggiante per decidere - la distanza da terra, quando viene data la posizione. Ora l'intero problema si risolve automaticamente. Un uccello che non può volare restituisce solo 0,0 da quella funzione, non lascia mai il terreno. È un comportamento corretto per questo, e una volta deciso quel float, sai che hai implementato completamente l'interfaccia.

Il comportamento reale può essere difficile da codificare in base ai tipi, ma è l'unico modo per specificare correttamente le interfacce.

Modifica: voglio chiarire un aspetto. Questa versione float-> float della funzione fly () è importante anche perché definisce un percorso. Questa versione significa che un uccello non può duplicarsi magicamente mentre sta volando. Questo è il motivo per cui il parametro è single float - è la posizione nel percorso che prende l'uccello. Se vuoi percorsi più complessi, allora Point2d posinpath (float x); che utilizza la stessa x della funzione fly ().


1
Mi piace molto la tua risposta. Penso che meriti più voti.
Sebastien Diot,

2
Risposta eccellente. Il problema è che la domanda non fa altro che agitare le mani su ciò che Fly () effettivamente fa. Qualsiasi reale implementazione di fly avrebbe, come minimo, una destinazione - fly (destinazione coordinata) che nel caso del pinguino, potrebbe essere ignorata per implementare {return currentPosition)}
Chris Cudmore

4

Tecnicamente puoi farlo praticamente in qualsiasi linguaggio di tipo dinamico / anatra (JavaScript, Ruby, Lua, ecc.) Ma è quasi sempre una pessima idea. La rimozione di metodi da una classe è un incubo di manutenzione, simile all'utilizzo di variabili globali (cioè non si può dire in un modulo che lo stato globale non è stato modificato altrove).

Buoni motivi per il problema che hai descritto sono Decorator o Strategia, progettando un'architettura componente. Fondamentalmente, anziché rimuovere i comportamenti non necessari dalle sottoclassi, si creano oggetti aggiungendo i comportamenti necessari. Quindi per costruire la maggior parte degli uccelli aggiungerei il componente volante, ma non aggiungere quel componente ai tuoi pinguini.


3

Peter ha menzionato il principio di sostituzione di Liskov, ma ritengo che debba essere spiegato.

Sia q (x) una proprietà dimostrabile per gli oggetti x di tipo T. Quindi q (y) dovrebbe essere dimostrabile per gli oggetti y di tipo S dove S è un sottotipo di T.

Pertanto, se un uccello (oggetto x di tipo T) può volare (q (x)), un pinguino (oggetto y di tipo S) può volare (q (y)), per definizione. Ma chiaramente non è così. Ci sono anche altre creature che possono volare ma non sono di tipo Uccello.

Il modo in cui lo affronti dipende dalla lingua. Se una lingua supporta l'ereditarietà multipla, allora dovresti usare una classe astratta per le creature che possono volare; se una lingua preferisce le interfacce, questa è la soluzione (e l'implementazione di fly dovrebbe essere incapsulata piuttosto che ereditata); oppure, se una lingua supporta Duck Typing (nessun gioco di parole previsto), puoi semplicemente implementare un metodo fly su quelle classi che possono e chiamarlo se è lì.

Ma ogni proprietà di una superclasse dovrebbe applicarsi a tutte le sue sottoclassi.

[In risposta alla modifica]

Applicare un "tratto" di CanFly a Bird non è meglio. Sta ancora suggerendo di chiamare il codice affinché tutti gli uccelli possano volare.

Un tratto nei termini che hai definito è esattamente ciò che Liskov intendeva quando diceva "proprietà".


2

Vorrei iniziare citando (come tutti gli altri) il principio di sostituzione di Liskov, il che spiega perché non dovresti farlo. Tuttavia, il problema di cosa dovresti fare è di design. In alcuni casi potrebbe non essere importante che Penguin non possa effettivamente volare. Forse puoi chiedere a Penguin di lanciare InsufficientWingsException quando ti viene chiesto di volare, a patto che tu sia chiaro nella documentazione di Bird :: fly () che potrebbe lanciarlo per gli uccelli che non possono volare. Di avere un test per vedere se può davvero volare, anche se questo gonfia l'interfaccia.

L'alternativa è ristrutturare le tue classi. Creiamo la classe "FlyingCreature" (o meglio un'interfaccia, se hai a che fare con il linguaggio che lo consente). "Bird" non eredita da FlyingCreature, ma puoi creare "FlyingBird". Lark, Vulture e Eagle ereditano tutti da FlyingBird. Penguin no. Eredita da Bird.

È un po 'più complicato della struttura ingenua, ma ha il vantaggio di essere preciso. Noterai che ci sono tutte le classi previste (Bird) e l'utente può di solito ignorare quelle "inventate" (FlyingCreature) se non è importante che la tua creatura possa volare o meno.


0

Il modo tipico di gestire una situazione del genere è lanciare qualcosa di simile a un UnsupportedOperationExceptionresp (Java). NotImplementedException(C #).


Finché documenti questa possibilità in Bird.
DJClayworth,

0

Molte buone risposte con molti commenti, ma non tutti sono d'accordo, e posso sceglierne solo uno, quindi riassumerò qui tutte le opinioni con cui sono d'accordo.

0) Non dare per scontato la "digitazione statica" (l'ho fatto quando ho chiesto, perché faccio quasi esclusivamente Java). Fondamentalmente, il problema dipende molto dal tipo di linguaggio che si usa.

1) Si dovrebbe separare la gerarchia dei tipi dalla gerarchia del riutilizzo del codice nel disegno e nella propria testa, anche se si sovrappongono per lo più. In genere, utilizzare le classi per il riutilizzo e le interfacce per i tipi.

2) Il motivo per cui normalmente Bird IS-A Fly è perché la maggior parte degli uccelli può volare, quindi è pratico dal punto di vista del riutilizzo del codice, ma dire che Bird IS-A Fly è in realtà sbagliato poiché esiste almeno un'eccezione (Pinguino).

3) In entrambi i linguaggi statici e dinamici, potresti semplicemente lanciare un'eccezione. Ma questo dovrebbe essere usato solo se è esplicitamente dichiarato nel "contratto" della classe / interfaccia che dichiara la funzionalità, altrimenti si tratta di una "violazione del contratto". Significa anche che ora devi essere pronto a catturare l'eccezione ovunque, quindi scrivi più codice sul sito di chiamata ed è brutto codice.

4) In alcuni linguaggi dinamici, è effettivamente possibile "rimuovere / nascondere" la funzionalità di una superclasse. Se verificare la presenza della funzionalità è come verificare "IS-A" in quella lingua, allora questa è una soluzione adeguata e ragionevole. Se, d'altra parte, l'operazione "IS-A" è qualcos'altro che dice ancora che il tuo oggetto "dovrebbe" implementare la funzionalità ora mancante, allora il tuo codice chiamante supporrà che la funzionalità sia presente e chiamarlo e andare in crash, quindi è un po 'come buttare un'eccezione.

5) L'alternativa migliore è quella di separare effettivamente il tratto Fly dal tratto Bird. Quindi un uccello volante deve estendere / implementare esplicitamente sia Bird che Fly / Flying. Questo è probabilmente il design più pulito, in quanto non è necessario "rimuovere" nulla. L'unico svantaggio è ora che quasi tutti gli uccelli devono implementare sia Bird che Fly, quindi scrivi più codice. Il modo per aggirare questo è avere una classe intermedia FlyingBird, che implementa sia Bird che Fly, e rappresenti il ​​caso comune, ma questa soluzione alternativa potrebbe essere di utilità limitata senza eredità multipla.

6) Un'altra alternativa che non richiede l'ereditarietà multipla è usare la composizione anziché le eredità. Ogni aspetto di un animale è modellato da una classe indipendente, e un uccello concreto è una composizione di uccello, e possibilmente vola o nuota, ... Ottieni un riutilizzo completo del codice, ma devi fare uno o più passaggi aggiuntivi per arrivare a la funzionalità Volare, quando si ha un riferimento a un uccello concreto. Inoltre, il linguaggio naturale "oggetto IS-A Fly" e "oggetto AS-A (cast) Fly" non funzioneranno più, quindi devi inventare la tua sintassi (alcuni linguaggi dinamici potrebbero avere un modo per aggirare questo). Questo potrebbe rendere il tuo codice più ingombrante.

7) Definisci la tua caratteristica Fly in modo tale da offrire una chiara via d'uscita per qualcosa che non può volare. Fly.getNumberOfWings () potrebbe restituire 0. Se Fly.fly (direzione, currentPotinion) dovesse restituire la nuova posizione dopo il volo, Penguin.fly () potrebbe semplicemente restituire la posizione corrente senza modificarla. Potresti finire con un codice che tecnicamente funziona, ma ci sono alcuni avvertimenti. Innanzitutto, alcuni codici potrebbero non avere un comportamento ovvio "non fare nulla". Inoltre, se qualcuno chiama x.fly (), si aspetterebbe che faccia qualcosa , anche se il commento dice che fly () non può fare nulla . Infine, il pinguino IS-A Flying tornerebbe comunque vero, il che potrebbe creare confusione per il programmatore.

8) Fai come 5), ma usa la composizione per aggirare i casi che richiederebbero ereditarietà multipla. Questa è l'opzione che preferirei per un linguaggio statico, come 6) sembra più ingombrante (e probabilmente richiede più memoria perché abbiamo più oggetti). Un linguaggio dinamico potrebbe rendere 6) meno ingombrante, ma dubito che diventerebbe meno ingombrante di 5).


0

Definire un comportamento predefinito (contrassegnarlo come virtuale) nella classe base e sovrascriverlo come necessario. In questo modo ogni uccello può "volare".

Persino i pinguini volano, scivolando sul ghiaccio a quota zero!

Il comportamento del volo può essere ignorato, se necessario.

Un'altra possibilità è avere un'interfaccia Fly. Non tutti gli uccelli implementeranno tale interfaccia.

class eagle : bird, IFly
class penguin : bird

Le proprietà non possono essere rimosse, quindi è importante sapere quali sono le proprietà comuni a tutti gli uccelli. Penso che sia più un problema di progettazione assicurarsi che le proprietà comuni siano implementate a livello di base.


-1

Penso che lo schema che stai cercando sia un buon vecchio polimorfismo. Mentre potresti essere in grado di rimuovere un'interfaccia da una classe in alcune lingue, probabilmente non è una buona idea per le ragioni fornite da Péter Török. In qualsiasi linguaggio OO, tuttavia, è possibile sovrascrivere un metodo per modificarne il comportamento e ciò include non fare nulla. Per prendere in prestito il tuo esempio, potresti fornire un metodo Penguin :: fly () che esegue una delle seguenti operazioni:

  • Niente
  • genera un'eccezione
  • chiama invece il metodo Penguin :: swim ()
  • afferma che il pinguino è sott'acqua (fanno una specie di "volo" attraverso l'acqua)

Le proprietà possono essere leggermente più facili da aggiungere e rimuovere se si pianifica in anticipo. È possibile memorizzare le proprietà in una mappa / dizionario / array associativo anziché utilizzare le variabili di istanza. È possibile utilizzare il modello Factory per produrre istanze standard di tali strutture, quindi un Bird proveniente da BirdFactory inizierà sempre con lo stesso set di proprietà. La codifica dei valori chiave di Objective-C è un buon esempio di questo genere di cose.

Nota: la lezione seria dei commenti qui sotto è che, mentre l'override per rimuovere un comportamento può funzionare, non è sempre la soluzione migliore. Se ti ritieni necessario farlo in modo significativo, dovresti considerare che un segnale forte che il tuo grafico dell'eredità è difettoso. Non è sempre possibile riformattare le classi da cui erediti, ma quando è spesso questa è la soluzione migliore.

Usando il tuo esempio Penguin, un modo per refactoring sarebbe quello di separare l'abilità di volo dalla classe Bird. Dal momento che non tutti gli uccelli possono volare, incluso un metodo fly () in Bird era inappropriato e portava direttamente al tipo di problema che stai chiedendo. Quindi, sposta il metodo fly () (e forse takeoff () e land ()) su una classe o interfaccia Aviator (a seconda della lingua). Ciò ti consente di creare una classe FlyingBird che eredita da Bird e Aviator (o eredita da Bird e implementa Aviator). Penguin può continuare a ereditare direttamente da Bird ma non da Aviator, evitando così il problema. Un simile accordo potrebbe anche facilitare la creazione di classi per altre cose volanti: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect e così via.


2
-1 anche per aver suggerito di chiamare Penguin :: swim (). Ciò viola il principio del minimo stupore e farà in modo che i programmatori di manutenzione ovunque imprecino il tuo nome.
DJClayworth,

1
@DJClayworth Come l'esempio era in primo luogo ridicolo in primo luogo, il downvoting per violazione del comportamento inferito di fly () e swim () sembra un po 'troppo. Ma se vuoi davvero guardarlo seriamente, concordo sul fatto che è più probabile che tu vada dall'altra parte e implementi swim () in termini di fly (). Le anatre nuotano remando i loro piedi; i pinguini nuotano sbattendo le ali.
Caleb,

1
Sono d'accordo sul fatto che la domanda sia stata sciocca, ma il problema è che ho visto le persone farlo nella vita reale - utilizzare le chiamate esistenti che "non fanno davvero nulla" per implementare funzionalità rare. In realtà rovina il codice e di solito termina con la necessità di scrivere "if (! (MyBird instanceof Penguin)) fly ();" in molti posti, sperando che nessuno crei una classe di struzzi.
DJClayworth,

L'asserzione è ancora peggio. Se ho un array di Birds, che hanno tutti il ​​metodo fly (), non voglio un errore di asserzione quando chiamo fly () su di essi.
DJClayworth,

1
Non ho letto la documentazione di Penguin , perché mi è stata consegnata una serie di Birds e non sapevo che un Penguin sarebbe stato nella matrice. Ho letto la documentazione di Bird che diceva che quando chiamo fly () l'uccello vola. Se quella documentazione avesse chiaramente affermato che un'eccezione potrebbe essere generata se l'uccello fosse incapace di volare, lo avrei permesso. Se dicesse che a volte chiamare fly () lo avrebbe fatto nuotare, avrei cambiato usando una libreria di classi diversa. O andato per un drink molto grande.
DJClayworth,
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.