Una classe base implementa un metodo virtuale per il tipo più comune o classe derivata?

2

Va bene implementare un metodo virtuale in una classe base perché so che la maggior parte delle classi derivate utilizzerà quella particolare implementazione?

Supponiamo che abbia la seguente gerarchia di classi (in C #):

public abstract class BaseClass {
    public virtual void MethodA() {
        //Implement MethodA as DerivedClassA and DerivedClassB
        //need it to avoid code duplication for
        //DerivedClassA.MethodA and DerivedClassB.MethodB
    }
}

public class DerivedClassA : BaseClass {}

public class DerivedClassB : BaseClass {}

public class DerivedClassC : BaseClass {
    public override void MethodA() {
        //Do MethodA in a DerivedClassC kinda way
    }
}

Mi sembra che dovrebbe andare bene perché sembra che questo sia il motivo per cui esistono metodi virtuali, ma qualcosa mi sembra anche un po '"puzzolente" per me. Immagino che mi piacerebbe solo la conferma che questo approccio è ok o che deve essere spiegato perché è cattivo.

    
posta Doug Tait 08.03.2018 - 00:27
fonte

4 risposte

2

Implementerei il metodo solo nella classe base se è un valore predefinito accettabile per le sottoclassi all (attualmente conosciute e future).

Un metodo virtuale è una promessa di essere in grado di eseguire un qualche tipo di comportamento. Ignorare questo comportamento sta dicendo che la sottoclasse richiede più elaborazione, più passaggi, ecc. Per poter adempiere a quel contratto. Come tale, una cosa che mi è sempre stata insegnata è che se hai un metodo virtuale e non stai chiamando la versione di tuo padre dello stesso metodo, probabilmente hai la tua gerarchia di ereditarietà tutta matta (un odore di codice, non per definizione sbagliato, ma qualcosa probabilmente non è giusto). Se la tua classe non fa qualcosa come i suoi genitori, merita davvero di essere una classe figlia di quel genitore? O dovrebbe essere più simile a un fratello?

Vorrei rendere le tue lezioni più simili a questa:

public abstract class BaseClass {
    public abstract void MethodA();
}

public abstract class DerivedClassThatDoesThingsTheFooWay : BaseClass {
    public virtual void MethodA() {
        //Implement MethodA as DerivedClassA and DerivedClassB
        //need it to avoid code duplication for
        //DerivedClassA.MethodA and DerivedClassB.MethodB
    }
}

public class DerivedClassA : DerivedClassThatDoesThingsTheFooWay {}

public class DerivedClassB : DerivedClassThatDoesThingsTheFooWay {}

public class DerivedClassC : BaseClass {
    public override void MethodA() {
        //Do MethodA in a DerivedClassC kinda way
    }
}

In questo modo si spostano le funzionalità comuni in un posto comune che non fa dipendere le altre classi o ha un comportamento irrilevante. Se c'è un default comune e accettabile, la classe base va bene. Se in alcuni casi tale funzionalità risultasse completamente errata, spostala in una classe da qualche parte nel mezzo (oppure utilizza la composizione anziché l'ereditarietà).

    
risposta data 08.03.2018 - 19:50
fonte
1

In linea di principio mi sembra soddisfacente, ma dipende esattamente da quale codice è condiviso, perché viene condiviso e come potrebbe cambiare se aggiungi un DerivedClassD, DerivedClassE, ecc.

Se hai un'implementazione di MethodA che generalmente ti aspetti di condividere con tutte o la maggior parte delle implementazioni delle classi derivate, allora è logico inserirlo nella classe genitore, quindi sovrascriverlo nelle poche occasioni in cui ci sono eccezioni.

Ma se il codice è appena condiviso da due delle classi derivate, ma non sarà necessariamente condiviso da altre che potrebbero essere aggiunte, potrebbe essere più sensato farlo in modo leggermente diverso, ad esempio introdurre una classe intermedia che eredita da BaseClass e ha DerivedClassA e DerivedClassB ereditati da questa classe intermedia mentre DeriveClassC eredita ancora direttamente da BaseClass.

    
risposta data 08.03.2018 - 18:49
fonte
1

Se i metodi riguardano il vero motivo della classe, il tuo approccio va bene. Se i metodi sono solo funzioni di utilità e l'ereditarietà viene utilizzata come mezzo per condividere tali utilità, dovresti favorire la Composizione sull'ereditarietà.

Buona

public class DiscountCalculator
{
    public virtual decimal CalculateDiscount(decimal price)
    {
        return price * 0.1m;
    }

    // other methods
}

public class NewMemberDiscountCalculator : DiscountCalculator
{
    // other methods
}

public class PreferredDiscountCalculator : DiscountCalculator
{
    public override decimal  CalculateDiscount(decimal price)
    {
        return price * 0.25m;
    }

    // other methods
}

Perché questo è ok: queste classi calcolano gli sconti. La classe figlio calcola ancora uno sconto, ma è una versione più specializzata.

Bad

public class DiscountCalculator
{
    public virtual string FormatDiscount(decimal price)
    {
        return CalculateDiscount(price).ToString("C");
    }

    // other methods
}

public class NewMemberDiscountCalculator : DiscountCalculator
{
    // other methods
}

public class PreferredDiscountCalculator : DiscountCalculator
{
    public override string FormatDiscount(decimal price)
    {
        base.FormatDiscount(price) + "!!!";
    }

    // other methods
}

Perché non va bene: questi metodi sono metodi di utilità che formattano lo sconto. Questa classe non dovrebbe essere formattazione lo sconto, poiché il suo scopo è quello di calcolare sconti.

    
risposta data 08.03.2018 - 19:40
fonte
1

Sì, certo. Questo è esattamente il punto delle classi astratte. Una classe astratta consente di definire un comportamento "predefinito" per tutte le sottoclassi. L'alternativa, senza comportamento predefinito, è un'interfaccia.

Contrassegnare le astratte funzioni di classe come astratte consente anche di segnalare dove le sottoclassi devono definire quelle funzioni, piuttosto che solo virtuale dove è facoltativo. Spesso è comune avere funzioni non virtuali nella classe base, che chiamano funzioni astratte per le quali sono necessarie sottoclassi per definire.

Non è un odore di codice. Sta usando le caratteristiche del linguaggio esattamente nel modo in cui avrebbero dovuto funzionare.

    
risposta data 08.03.2018 - 21:32
fonte