Estratto genitore ed ereditarietà rispetto al modello e alla composizione della strategia

3

Attualmente sto rifattorizzando una classe che sembra (dopo alcuni refactoring e molto semplificata) in qualche modo simile a questa:

class Foo
{
  public:
    Foo(bool someFlag) : m_flag(someFlag) { };

    void doThings();
    void doOtherThings()
    {
        doPrivateThings();
        if (m_flag)
            doFlagThings();
        else
            doNoFlagThings();
    }

  private:
    void doPrivateThings();
    void doFlagThings();
    void doNoFlagThings();
    bool m_flag;
}

Note:
* m_flag essendo lì è il risultato del refactoring (che di gran lunga non viene eseguito) - inizialmente ogni funzione individualmente ramificata ( if (check_something()) ) su qualcosa per cui il risultato era già determinato / fisso durante la costruzione dell'oggetto. < br> * Le funzioni doFlagThings() e doNoFlagThings() sono piuttosto piccole (~ 3-5 linee di codice) e ci sono al massimo 3-4 funzioni che si discostano a seconda di m_flag .

Quindi, il passo successivo è sbarazzarsi di someFlag - l'unica domanda è come.
Un modo sarebbe utilizzare un genitore astratto e l'ereditarietà:

class FooBase
{
  public:
    FooBase();

    void doThings();
    void doOtherThings()
    {
        doPrivateThings();
        doSpecializedThings();
    }

  private:
    void doPrivateThings();
    virtual void doSpecializedThings() = 0;
}

class FlagFoo : public FooBase
{
  public:
    FlagFoo();

  private:
    void doSpecializedThings() override { ... };
}

class NoFlagFoo : public FooBase
{
  public:
    NoFlagFoo();

  private:
    void doSpecializedThings() override { ... };
}

L'altro modo sarebbe utilizzare il modello e la composizione della strategia, cioè

class Foo
{
  public:
    Foo(IDoThingsStrategystrategy strategy) : m_Strategy(strategy) { };

    void doThings();
    void doOtherThings()
    {
        doPrivateThings();
        m_Strategy.doSpecializedThings();
    }

  private:
    void doPrivateThings();
    IDoThingsStrategym_Strategy;
}

class IDoThingsStrategy
{
  public:
    IDoThingsStrategy();
    virtual void doSpecializedThings() = 0;
}

class DoThingsFlagStrategy : public IDoThingsStrategy
{
  public:
    DoThingsFlagStrategy();
    void doSpecializedThings() override;
}

class DoThingsNoFlagStrategy : public IDoThingsStrategy
{
  public:
    DoThingsNoFlagStrategy();
    void doSpecializedThings() override;
}

Ma sembra molto eccessivo e presenta il problema che il creatore di Foo deve ora conoscere le diverse Strategie per crearle e alimentarle a Foo .

Quindi la domanda è: dovrei andare con l'ereditarietà (anche se uno dovrebbe preferire la composizione sull'ereditarietà) o il modello della strategia o c'è un modo migliore che non vedo proprio adesso?

    
posta CharonX 20.08.2018 - 15:15
fonte

3 risposte

1

In breve

Il tuo codice è l'esempio tipico dell'applicazione del modello di progettazione del modello di modello :

Define the skeleton of an algorithm in an operation, deferring some steps to a subclass. Template methtods lets subclasses redefine certain steps of an algorithm without changing the algorithm structure.
- E.Gamma, R.Helm, R.Johnson, J.Vlissides (aka the GoF)

Quindi scegli la tua prima opzione di refactoring, perché è esattamente ciò che intendi fare.

Altre spiegazioni **

Il tuo attuale codice di doFlagThings() e doNoFlagThings() può perfettamente usare membri protetti o privati, perché attualmente sono entrambe funzioni membro.

Con il modello di metodo del modello, non ti preoccupare di questo (tranne forse alcuni membri privati che dovrebbero essere protetti quando refactored nella classe FooBase .

Il modello di strategia intende incapsulare una famiglia di alogritmi per renderli utilizzabili in modo intercambiabile. Nel tuo caso:

  • se il codice in doFlagThings() e doNoFlagThings() sarebbe molto indipendente dal resto della classe, sarebbe sicuramente un'alternativa valida;
  • Tuttavia, se non lo è, è necessario rendere le classi di strategia amiche di Foo e inoltre passare l'oggetto chiamante come parametro in modo che la strategia possa richiamare le giuste funzioni e accedere alle variabili membro corrette. Quindi avresti una strategia falsa, in cui avresti un accoppiamento molto strong tra tutte le classi. Non esattamente l'intento del modello ...

Ma perché esiti? Hai identificato da solo che la strategia sarebbe eccessiva nella tua situazione. È perhap perché hai nella tua testa il mantra " composizione sull'eredità "? Poi sii rassicurato: è solo una regola empirica e non una regola d'oro. Ci sono casi in cui l'ereditarietà è davvero la soluzione migliore n (altrimenti lì non sarebbe un'eredità in OOP ;-))

    
risposta data 20.08.2018 - 19:45
fonte
1

La tua strategia di composizione implica ancora l'ereditarietà con metodi virtuali, in modo tale che non semplifichi realmente l'opzione (prima) di ereditarietà diretta. Quindi, data la scelta tra i due, l'eredità sembra più semplice. E puoi sempre refactoring più tardi se hai bisogno di comporre.

Il fattore principale nella mia mente è in primo luogo rendere le cose semplici per i consumatori che consumano . In questo caso, una buona scelta sembrerebbe utilizzare un metodo factory in modo che i chiamanti non debbano sapere se si suppone che debbano new di alcune sottoclassi specifiche, o, utilizzare un modello di strategia, iniettato nel costruttore. In questo modo puoi rifattorizzare l'implementazione in un secondo momento senza modificare quei client che consumano l'astrazione.

A volte, come spunti di riflessione, se consideriamo un contesto più ampio, possiamo ancora trovare altre alternative. In particolare, se si ha a che fare con un insieme di tali oggetti, possiamo usare due raccolte diverse: una per gli oggetti flag che dovrebbero fare la cosa flag e una per gli oggetti no-flag che non dovrebbero. Qui raggruppando insieme gli oggetti flag e no-flag, a volte siamo in grado di ottimizzare l'elaborazione e la differenziazione tra questi oggetti, elaborando tutti gli oggetti flag e elaborando tutti gli oggetti no-flag. Quindi, ora la scelta è su quale raccolta aggiungere l'oggetto, piuttosto che su quale sottoclasse creare. Ovviamente, data l'espressione semplificata del problema, non si può sapere se questo si applicherebbe, quindi questo è solo uno spunto di riflessione.

    
risposta data 20.08.2018 - 18:27
fonte
1

Una chiave importante per un buon design è la capacità di vedere nel futuro. Devi prevedere (e sarai giudicato in base a quanto bene prevedi) come si evolverà il codice.

Vuoi una generalizzazione PIÙ in cui ti aspetti che il codice si divida di più e diventi più complesso. Generalmente hai bisogno di meno generalizzazioni in cui il codice non cambierà molto.

Un difetto chiave con i nomi 'anonimizzati' in modo che non significano nulla, è che non posso dare a GUESS dove vorresti generalizzare di più in futuro. Devi capirlo.

Il codice che hai ora - con condizionali (m_flag) - in realtà non è male, a seconda di come ti aspetti che il codice si evolva nel tempo. Ma se ti aspetti molte implementazioni doOtherThings () a seconda del sottotipo - lo vedi come un'area produttiva di cambiamento - allora la sottoclasse è una buona idea.

Ciò che hai descritto come IDoThingsStrategy NON sembra una buona idea particolare - almeno per quanto riguarda i codici GENERIZING. È una buona strategia per MODULARIZZARE (ma il lettore - io - non sa se ha senso per la modularizzazione). Quello che stai facendo è permettere che la sottoclasse avvenga in modo separato e senza rispetto per gli altri dati in "Foo". Potrebbe essere un PITA (se generalmente hai bisogno di quelle informazioni) o intelligente, se non lo fai.

    
risposta data 20.08.2018 - 16:05
fonte

Leggi altre domande sui tag