È "Se un metodo viene riutilizzato senza modifiche, inserire il metodo in una classe base, altrimenti creare un'interfaccia" una buona regola empirica?

10

Un mio collega ha trovato una regola empirica per scegliere tra creare una classe base o un'interfaccia.

Dice:

Imagine every new method that you are about to implement. For each of them, consider this: will this method be implemented by more than one class in exactly this form, without any change? If the answer is "yes", create a base class. In every other situation, create an interface.

Ad esempio:

Consider the classes cat and dog, which extend the class mammal and have a single method pet(). We then add the class alligator, which doesn't extend anything and has a single method slither().

Now, we want to add an eat() method to all of them.

If the implementation of eat() method will be exactly the same for cat, dog and alligator, we should create a base class (let's say, animal), which implements this method.

However, if it's implementation in alligator differs in the slightest way, we should create an IEat interface and make mammal and alligator implement it.

Insiste che questo metodo copre tutti i casi, ma mi sembra una semplificazione eccessiva.

Vale la pena seguire questa regola pratica?

    
posta sbichenko 12.11.2013 - 21:05
fonte

12 risposte

13

Non penso che questa sia una buona regola empirica. Se sei preoccupato per il riutilizzo del codice, puoi implementare un PetEatingBehavior il cui ruolo è definire le funzioni alimentari per cani e gatti. Quindi puoi avere IEat e riutilizzare il codice insieme.

In questi giorni vedo sempre meno motivi per usare l'ereditarietà. Un grande vantaggio è la facilità d'uso. Prendi una struttura GUI. Un modo popolare di progettare un'API per questo è di esporre un'enorme classe base e documentare quali metodi l'utente può sovrascrivere. Quindi possiamo ignorare tutte le altre cose complesse che la nostra applicazione deve gestire, poiché un'implementazione "predefinita" è data dalla classe base. Ma possiamo ancora personalizzare le cose reimplementando il metodo corretto quando è necessario.

Se ti limiti all'interfaccia per la tua API, il tuo utente ha di solito più lavoro da fare per far funzionare semplici esempi. Tuttavia, le API con interfacce sono spesso meno accoppiate e più facili da gestire per il semplice motivo che IEat contiene meno informazioni rispetto alla classe base Mammal . Quindi i consumatori dipenderanno da un contratto più debole, avrai più opportunità di refactoring, ecc.

Per citare Rich Hickey: Simple! = Easy

    
risposta data 12.11.2013 - 21:42
fonte
11

Is it worth following this rule-of-thumb?

È una buona regola empirica, ma conosco un bel po 'di posti in cui lo violento.

Per essere onesti, uso le classi di base (astratte) molto più dei miei colleghi. Lo faccio come programmazione difensiva.

Per me (in molte lingue), una classe base astratta aggiunge il vincolo chiave che potrebbe esserci solo una classe base. Scegliendo di utilizzare una classe base piuttosto che un'interfaccia, si sta dicendo "questa è la funzionalità di base della classe (e di qualsiasi altro erede), ed è inappropriato mescolare willy-nilly con altre funzionalità". Le interfacce sono l'opposto: "Questo è un tratto di un oggetto, non necessariamente la sua responsabilità principale".

Questo tipo di scelte progettuali crea guide implicite per i tuoi implementatori su come dovrebbero usare il tuo codice, e per estensione scrivono il loro codice. A causa del loro maggiore impatto sul codebase, tendo a prendere in considerazione queste cose più strongmente quando si prende la decisione di progettazione.

    
risposta data 12.11.2013 - 21:14
fonte
9

Il problema con la semplificazione del tuo amico è che determinare se un metodo debba mai cambiare è eccezionalmente difficile. È meglio usare una regola che parli della mentalità dietro le superclassi e le interfacce.

Invece di capire cosa dovresti fare guardando a quello che pensi sia la funzionalità, guarda alla gerarchia che stai cercando di creare.

Ecco come un mio professore ha insegnato la differenza:

Le superclassi devono essere is a e le interfacce sono acts like o is . Quindi un cane is a mammifero e acts like un mangiatore. Questo ci direbbe che il mammifero dovrebbe essere una classe e il mangiatore dovrebbe essere un'interfaccia. Un esempio di is sarebbe The cake is eatable o The book is writable (rendendo entrambe le interfacce eatable e writable ).

L'uso di una metodologia come questa è ragionevolmente semplice e facile e ti porta a codificare la struttura e i concetti piuttosto che a ciò che pensi che farà il codice. Ciò semplifica la manutenzione, rende il codice più leggibile e semplifica la progettazione.

Indipendentemente da cosa potrebbe effettivamente dire il codice, se utilizzi un metodo come questo potresti tornare indietro tra cinque anni e utilizzare lo stesso metodo per tornare sui tuoi passi e capire facilmente come è stato progettato il tuo programma e come interagisce .

Solo i miei due centesimi.

    
risposta data 13.11.2013 - 01:20
fonte
6

Su richiesta di exizt, sto espandendo il mio commento a una risposta più lunga.

L'errore più grande che fanno le persone è credere che le interfacce siano semplicemente classi astratte vuote. Un'interfaccia è un modo per un programmatore di dire: "Non mi importa cosa mi dai, purché segua questa convenzione".

La libreria .NET è un ottimo esempio. Ad esempio, quando scrivi una funzione che accetta IEnumerable<T> , quello che stai dicendo è "Non mi interessa come stai memorizzando i tuoi dati. Voglio solo sapere che posso usare un ciclo foreach .

Questo porta ad un codice molto flessibile. All'improvviso per essere integrato, tutto ciò che devi fare è giocare secondo le regole delle interfacce esistenti. Se l'implementazione dell'interfaccia è difficile o confusa, allora forse è un suggerimento che stai cercando di infilare un piolo quadrato in un buco rotondo.

Ma poi sorge la domanda: "E riguardo al riutilizzo del codice? I miei professori CS mi hanno detto che l'ereditarietà era la soluzione a tutti i problemi di riutilizzo del codice e che l'eredità ti permette di scrivere una volta e di usarla ovunque e curerebbe la menangite nel processo di salvare gli orfani dal mare in aumento e non ci sarebbero più lacrime, ecc. ecc. ecc. "

Utilizzare l'ereditarietà solo perché ti piace il suono delle parole "riutilizzare il codice" è un'idea piuttosto brutta. La guida alla creazione di stili di codice da parte di Google rende questo punto abbastanza conciso:

Composition is often more appropriate than inheritance. ... [B]ecause the code implementing a sub-class is spread between the base and the sub-class, it can be more difficult to understand an implementation. The sub-class cannot override functions that are not virtual, so the sub-class cannot change implementation.

Per illustrare perché l'ereditarietà non è sempre la risposta, userò una classe chiamata MySpecialFileWriter †. Una persona che crede ciecamente che l'ereditarietà sia la soluzione a tutti i problemi sosterrebbe che dovresti provare ad ereditare da FileStream , per evitare di duplicare il codice di FileStream . Le persone intelligenti riconoscono che questo è stupido. Devi solo avere un oggetto FileStream nella tua classe (come variabile locale o membro) e usarne la funzionalità.

L'esempio di FileStream può sembrare forzato, ma non lo è. Se hai due classi che implementano entrambe la stessa interfaccia nello stesso modo, allora dovresti avere una terza classe che incapsula qualsiasi operazione venga duplicata. Il tuo obiettivo dovrebbe essere quello di scrivere classi che sono blocchi riutilizzabili autonomi che possono essere messi insieme come lego.

Questo non significa che l'eredità dovrebbe essere evitata a tutti i costi. Ci sono molti punti da considerare, e la maggior parte sarà trattata dalla ricerca sulla questione "Composizione contro l'ereditarietà". Il nostro overflow dello stack ha alcune buone risposte sull'argomento.

Alla fine della giornata, i sentimenti del tuo collega mancano della profondità o della comprensione necessaria per prendere una decisione informata. Cerca l'argomento e scoprilo da solo.

† Quando si illustra l'ereditarietà, tutti usano gli animali. È inutile In 11 anni di sviluppo, non ho mai scritto una classe chiamata Cat , quindi non la userò come esempio.

    
risposta data 13.11.2013 - 17:33
fonte
4

Gli esempi raramente fanno un punto, specialmente non quando riguardano auto o animali. Il modo in cui un animale mangia (o processa il cibo) non è realmente legato alla sua eredità, non esiste un unico modo di mangiare che si applichi a tutti gli animali. Ciò dipende maggiormente dalle sue capacità fisiche (le modalità di input, elaborazione e output, per così dire). I mammiferi possono essere carnivori, erbivori o onnivori, ad esempio, mentre uccelli, mammiferi e pesci possono mangiare insetti. Questo comportamento può essere catturato con determinati modelli.

In questo caso, in questo caso, è meglio il comportamento di astrazione, come questo:

public interface IFoodEater
{
    void Eat(IFood food);
}

public class Animal : IFoodEater
{
    private IFoodProcessor _foodProcessor;
    public Animal(IFoodProcessor foodProcessor)
    {
        _foodProcessor = foodProcessor;
    }

    public void Eat(IFood food)
    {
        _foodProcessor.Process(food);
    }
}

O implementa un pattern visitatore in cui FoodProcessor prova a nutrire animali compatibili ( ICarnivore : IFoodEater , MeatProcessingVisitor ). Il pensiero di animali vivi potrebbe ostacolarti nel pensare di estrarre la traccia digestiva e sostituirla con una generica, ma gli stai facendo davvero un favore.

Questo renderà il tuo animale una classe base, e ancora migliore, non dovrai mai più cambiare quando aggiungi un nuovo tipo di cibo e un secondo processore, così puoi concentrarti sulle cose che rendono questa specifica classe di animali stai lavorando in modo unico.

Quindi sì, le classi base hanno il loro posto, la regola generale si applica (spesso, non sempre, poiché è una regola empirica), ma continua a cercare un comportamento che puoi isolare.

    
risposta data 12.11.2013 - 21:43
fonte
2

Un'interfaccia è davvero un contratto. Non c'è funzionalità. Spetta all'attore implementarlo. Non c'è scelta come si deve implementare il contratto.

Le classi astratte danno una scelta all'implementatore. Si può scegliere di avere un metodo che tutti devono implementare, fornire un metodo con funzionalità di base che uno potrebbe sovrascrivere o fornire alcune funzionalità di base che tutti gli eredi possono utilizzare.

Ad esempio:

public abstract class Person
    {
        /// <summary>
        /// Inheritors must implement a hello
        /// </summary>
        /// <returns>Hello</returns>
        public abstract string SayHello();
        /// <summary>
        /// Inheritors can use base functionality, or override
        /// </summary>
        /// <returns></returns>
        public virtual string SayGoodBye()
        {
            return "Good Bye";
        }
        /// <summary>
        /// Base Functionality that is inherited 
        /// </summary>
        public void DoSomething()
        {
            //Do Something Here
        }
    }

Direi che la tua regola empirica potrebbe essere gestita anche da una classe base. In particolare, dal momento che molti mammiferi probabilmente "mangiano" lo stesso, l'uso di una classe base potrebbe essere migliore di un'interfaccia, dal momento che si potrebbe implementare un metodo di consumo virtuale che la maggior parte degli eredi potrebbe utilizzare e quindi i casi eccezionali potrebbero scavalcare.

Ora, se la definizione di "mangiare" varia molto da animale ad animale, allora forse l'interfaccia sarebbe migliore. Questa è a volte un'area grigia e dipende dalla situazione individuale.

    
risposta data 12.11.2013 - 22:17
fonte
1

No.

Le interfacce riguardano il modo in cui i metodi sono implementati. Se stai basando la tua decisione su quando utilizzare l'interfaccia su come verranno implementati i metodi, allora stai facendo qualcosa di sbagliato.

Per me, la differenza tra l'interfaccia e la classe base riguarda la posizione dei requisiti. Crei un'interfaccia, quando una parte di codice richiede una classe con specifiche API e comportamenti impliciti che emergono da questa API. Non puoi creare un'interfaccia senza codice che in realtà la chiamerà.

D'altro canto, le classi base sono di solito intese come modo per riutilizzare il codice, quindi raramente ti preoccupi del codice che chiamerà questa classe base e prenderà in considerazione solo la ricerca di oggetti di cui questa classe base fa parte.

    
risposta data 12.11.2013 - 21:15
fonte
1

Non è davvero una buona regola pratica perché se vuoi implementazioni differenti puoi sempre avere una classe base astratta con un metodo astratto. Ogni erede è garantito per avere quel metodo, ma ognuno definisce la propria implementazione. Tecnicamente potresti avere una classe astratta con nient'altro che un insieme di metodi astratti e sarebbe fondamentalmente la stessa di un'interfaccia.

Le classi base sono utili quando si desidera un'implementazione effettiva di alcuni stati o comportamenti che possono essere utilizzati su più classi. Se ti trovi con diverse classi che hanno campi e metodi identici con implementazioni identiche, probabilmente puoi refactoring in una classe base. Devi solo stare attento nel modo in cui erediti. Ogni campo o metodo esistente nella classe base deve avere senso nell'erede, specialmente quando viene lanciato come classe base.

Le interfacce sono utili quando è necessario interagire con un insieme di classi allo stesso modo, ma non è possibile prevedere o non preoccuparsi della loro effettiva implementazione. Prendi ad esempio qualcosa come un'interfaccia Collection. Il Signore conosce solo la miriade di modi in cui qualcuno potrebbe decidere di implementare una collezione di oggetti. Lista collegata? Lista di array? Pila? Coda? Questo metodo SearchAndReplace non interessa, deve solo essere passato qualcosa che può chiamare Aggiungi, Rimuovi e GetEnumerator su.

    
risposta data 12.11.2013 - 22:24
fonte
1

Sto guardando le risposte a questa domanda da solo, per vedere cosa hanno da dire le altre persone, ma dirò tre cose a cui sono propenso a pensare a questo:

  1. Le persone mettono troppa fiducia nelle interfacce e troppo poca fiducia nelle classi. Le interfacce sono essenzialmente classi di base molto annacquate, e ci sono occasioni in cui l'uso di qualcosa di leggero può portare a cose come un codice di codice eccessivo eccessivo.

  2. Non c'è niente di sbagliato con l'override di un metodo, chiamando super.method() al suo interno, e lasciando che il resto del suo corpo faccia solo cose che non interferiscono con la classe base. Ciò non viola il Principio di sostituzione di Liskov .

  3. Come regola generale, essere rigidi e dogmatici nell'ingegneria del software è una cattiva idea, anche se qualcosa in generale è una buona pratica.

Quindi prenderei ciò che è stato detto con un pizzico di sale.

    
risposta data 12.11.2013 - 21:35
fonte
1

Voglio adottare un approccio linguale e semantico verso questo. Un'interfaccia non è presente per DRY . Mentre può essere usato come meccanismo di centralizzazione insieme ad avere una classe astratta che lo implementa, non è pensato per DRY.

Un'interfaccia è un contratto.

L'interfaccia

IAnimal è un contratto tutto o niente tra alligatore, gatto, cane e la nozione di animale. Quello che dice in sostanza è "se vuoi essere un animale, o devi avere tutti questi attributi e caratteristiche, o non sei più un animale".

L'ereditarietà dall'altra parte è un meccanismo per il riutilizzo. In questo caso, penso che avere un buon ragionamento semantico possa aiutarti a decidere quale metodo dovrebbe andare dove. Ad esempio, mentre tutti gli animali potrebbero dormire e dormire nello stesso modo, non userò una classe base per questo. Per prima cosa metto il metodo Sleep() nell'interfaccia IAnimal come enfasi (semantica) per quello che serve per essere un animale, quindi uso una classe astratta di base per gatto, cane e alligatore come tecnica di programmazione per don ' Mi ripeto.

    
risposta data 13.11.2013 - 17:58
fonte
0

Non sono sicuro che questa sia la migliore regola di base. Puoi inserire un metodo abstract nella classe base e far sì che ogni classe lo implementa. Poiché ogni% dimammal deve mangiare, avrebbe senso farlo. Quindi ogni mammal può utilizzare il suo metodo un eat . In genere utilizzo solo le interfacce per la comunicazione tra oggetti o come marcatore per contrassegnare una classe come qualcosa. Penso che l'uso eccessivo delle interfacce crei codice sciatto.

Sono anche d'accordo con @Panzercrisis che una regola empirica dovrebbe essere solo una linea guida generale. Non qualcosa che dovrebbe essere scritto nella pietra. Ci saranno senza dubbio momenti in cui semplicemente non funziona.

    
risposta data 19.11.2013 - 21:39
fonte
-1

Le interfacce sono tipi. Non forniscono alcuna implementazione. Semplicemente definiscono il contratto per la gestione di quel tipo. Questo contratto deve essere soddisfatto da qualsiasi classe che implementa l'interfaccia.

L'uso di interfacce e classi astratte / di base dovrebbe essere determinato dai requisiti di progettazione.

Una singola classe non richiede un'interfaccia.

Se due classi sono intercambiabili all'interno di una funzione, dovrebbero implementare un'interfaccia comune. Qualsiasi funzionalità implementata in modo identico potrebbe quindi essere refactored in una classe astratta che implementa l'interfaccia. L'interfaccia potrebbe essere considerata ridondante in quel punto se non ci sono funzionalità di distinzione. Ma allora qual è la differenza tra le due classi?

L'utilizzo di classi astratte come tipi è generalmente disordinato poiché vengono aggiunte più funzionalità e man mano che una gerarchia si espande.

Favorisci la composizione sull'ereditarietà - tutto questo scompare.

    
risposta data 13.11.2013 - 16:33
fonte

Leggi altre domande sui tag