Perché viene utilizzata un'interfaccia piuttosto che l'ereditarietà?

-2

Vedi il modello di progettazione delle regole su questa pagina web: link . Ha classi come questa:

public interface IDiscountRule
{
    decimal CalculateCustomerDiscount(Customer customer);
}

public class BirthdayDiscountRule : IDiscountRule
{
    public decimal CalculateCustomerDiscount(Customer customer)
    {
        return customer.IsBirthday() ? 0.10m : 0;
    }
}

Perché le interfacce dovrebbero essere utilizzate per questo invece dell'ereditarietà, ad esempio, se BirthdayDiscountRule erediti IDiscountRule (chiamandolo invece DiscountRule) ?.

    
posta w0051977 10.07.2017 - 11:09
fonte

4 risposte

7

Ci sono una serie di motivi.

  1. In c # puoi ereditare molte interfacce, ma solo una classe base.

  2. L'ereditarietà ha perso popolarità come metodo di condivisione del codice contro la composizione.

Diciamo che abbiamo una logica di base che vogliamo applicare a tutti gli sconti e li inseriamo in una classe BaseDiscount come suggerisci. Ma poi scopriamo che non è così universale come pensavamo e non vogliamo usarlo per tutti gli sconti.

Bene possiamo tornare indietro e aggiungere un BaseBaseDiscount e ridefinire qualsiasi lista di sconti di quel tipo invece di BaseDiscount. Ma la versione più flessibile di questo sarebbe avere una classe Base senza codice, solo il metodo / proprietà esposto, che è essenzialmente un'interfaccia.

Inoltre, può essere difficile determinare quale codice sia effettivamente in esecuzione se si dispone di una catena di ereditarietà profonda.

Con la composizione potrei avere:

public class BirthdayDiscount: IDiscount
{
    public BirthdayDiscount(IDiscount baseDiscount)
    {
         this.baseDiscount = baseDiscount;
    }

    public decimal GiveDiscount(decimal price)
    {
        return baseDiscount.GiveDiscount(price) * birthdayDiscount;
    }
}

Ora posso tagliare e modificare la classe baseDiscount che passo al costruttore in fase di esecuzione, piuttosto che modificare l'ereditarietà della classe al momento della compilazione.

    
risposta data 10.07.2017 - 11:39
fonte
3

Poiché le interfacce dichiarano una funzione specifica firma piuttosto che uno specifico algoritmo per calcolare tale funzione.

Per elaborare: questo è esattamente a cosa servono le interfacce. Presumibilmente ci sono vari motivi per concedere sconti (forse ognuno ottiene l'1% di sconto in vacanza, o c'è una promozione sulle bevande energetiche, o qualsiasi altra cosa) e hanno tutti condizioni e sconti diversi, quindi dovrebbero essere modellati con metodi diversi in diversi classi, che implementano IDuscountRule . Tutti questi metodi danno un decimal , quindi soddisfano il contratto di CCD , ma lo fanno in modi diversi.

L'ereditarietà significherebbe che tutti lo fanno allo stesso modo , e non è quello che il problema richiede.

    
risposta data 10.07.2017 - 11:15
fonte
0

Penso alle interfacce come a un modo per aggirare la limitazione, nei due linguaggi moderni più popolari, che non supporta l'ereditarietà multipla, ad es. una classe non può estendere due classi.

Nel codice di esempio, l'interfaccia è destinata a essere utilizzata per gestire l'elaborazione di uno sconto in base ad alcune regole. Da qualche parte nell'app ci sarà un oggetto di tipo Persona, Cliente, ecc. E quella classe dirà "implements IDiscountRule" il che significa che il resto del codice nell'app può chiamare un metodo "CalculateCustomerDiscount" per ottenere lo sconto.

Potrebbero esserci uno sconto a causa del compleanno, uno sconto a causa dello stato degli studenti, dello stato militare, ecc. Tutti questi sarebbero codificati come estendere "IDiscountRule", ma il "CalculateCustomerDiscount" in quelle classi sarebbe specifico per qualunque lo sconto è fornito.

Infine, l'oggetto che rappresenta un cliente non "si estende" ma invece "implementa" CalculateCustomerDiscount ". Se lo si estendeva, la classe cliente non poteva estendere altro, e la logica dello sconto è solo una delle probabilmente molte dimensioni logiche del cliente.

    
risposta data 10.07.2017 - 14:45
fonte
-1

Le altre due risposte e il collegamento "possibile domanda duplicata" risolvono in modo adeguato i problemi dell'utilizzo delle classi base, piuttosto che delle interfacce. Ma mi piacerebbe indirizzare il codice nella domanda stessa.

La prima cosa da notare è la riga return customer.IsBirthday() ? 0.10m : 0; .

Questo codice rivela il fatto che il metodo IsBirthday() è probabilmente strettamente accoppiato a DateTime.Now . Ciò rende i test più difficili di quanto sia necessario, ad esempio senza modificare il tempo di sistema (o solo eseguendo il test una volta ogni quattro anni), non c'è un modo semplice per verificare che gestisca correttamente un cliente nato il 29 febbraio. La data corrente deve essere passata a IsBirthday() .

In secondo luogo, se la data corrente gli viene passata, CalculateCustomerDiscount diventa una pura funzione, quindi può essere reso statico:

public static decimal CalculateBirthdayDiscount(Customer customer, DateTime date) =>
    customer.IsBirthday(date) ? 0.10m : 0;

E può essere referenziato tramite un Func<Customer, DateTime, decimal> delegato in tutto il codice. Ciò elimina la necessità di definire l'interfaccia IDiscountRule .

Non solo semplifichiamo il codice rimuovendo un'interfaccia inutilizzata, ma eliminiamo la necessità di dover creare simulazioni di quell'interfaccia nel codice di test. È possibile invece utilizzare un semplice metodo di simulazione, che semplifica ancora una volta le cose, eliminando potenzialmente la necessità di utilizzare un framework di simulazione.

    
risposta data 10.07.2017 - 14:20
fonte

Leggi altre domande sui tag