Domanda di progettazione riguardante l'incapsulamento corretto e pratico

4

Ho lavorato sul refactoring del vecchio codice e ho trovato molte istanze del seguente tipo di situazione: c'è un oggetto master che chiamiamo "Application" e ce n'è solo uno. L'applicazione creerà al suo interno altri oggetti denominati "Funzionalità" in base all'input dell'utente, ecc. L'oggetto Feature potrebbe avere alcuni metodi che prendono parametri void ma in realtà operano o utilizzano alcune variabili nell'ambito di applicazione. Questo perché le funzioni definite nella feature di classe sono definite nello stesso progetto e includono l'intestazione da Application (questo è C ++). Ecco un rudimentale tentativo di mostrare un semplice esempio di pseudocodice:

int main()
{
    Application theApp = new Application()

    the.App.someSetting = foo;
    the.App.someVariable = bar;

    the.App.RunSomeFeature();
}
// RunSomeFeature function def in class Application
void Application::RunSomeFeature()
{
    // create object of type feature
     Feature feature = new Feature();

    feature.SomeFunction(void)
}
// SomeFunction def in class Feature
void Feature::SomeFunction(void)
{
    // global variable from Application "creeping in"
    someLocalVariable = theApp.someSetting;
    otherLocalVariable = theApp.someVariable;
}  

Spero che questo illustri un po 'cosa intendo. Il mio obiettivo è essenzialmente quello di spostare la classe "Feature" in una posizione diversa dallo spazio dei nomi di "Application" (ad esempio in una dll) e non posso includere l'intestazione Application (poiché ciò vanificherebbe lo scopo della separazione). Ma dal momento che ci sono questi tipi di globals ovunque nel vecchio codice questo compito è piuttosto complicato. Questa potrebbe essere una cosa del C ++ per quanto ne so, è facile (o posible) fare questo errore di codifica, ma non ne sono sicuro.

Questo mi ha fatto riflettere su alcune domande riguardanti il design OOP. Il codice OOP ben progettato avrebbe solo classi e funzioni completamente autonome con tutte le variabili esterne passate esplicitamente? Anche se un oggetto verrà creato all'interno di un altro? Posso vedere in questo modo l'intero codice sarebbe molto più facile da "rimescolare" internamente. Ma a volte ci possono essere molti parametri da passare e dal momento che "Feature" è creato da "Application" comunque vedo perché la mia situazione potrebbe sorgere. Ma il codice scritto come nell'esempio collega implicitamente la classe Application e Feature allo stesso "blocco" o ambito globale e prende il lavoro riga per riga ogni volta per separarli.

Tuttavia l'altro estremo sarebbe che ogni classe è essenzialmente quasi un'applicazione autonoma a sé stante (e questo può andare un po 'contro l'idea di ereditarietà). Quando leggo dei principi di progettazione OOP, di solito non trovo questa sfumatura particolare di cui ho parlato molto o ho solo l'idea che tutto dovrebbe essere fondamentalmente autonomo, ma trovo che in pratica questo è raramente vero.

Vedo che questo potrebbe essere un po 'aperto, ma qualsiasi regola empirica su quale livello dovremmo "incapsulare completamente" al fine di avere pezzi di codice che possiamo rimescolare facilmente? O qualsiasi filosofia progettuale che qualcuno condividerebbe su questo tipo di situazione?

    
posta virtore 31.03.2015 - 19:25
fonte

4 risposte

4

Quello che cerchi non è molto specifico per OOP e non ha assolutamente nulla a che fare con l'ereditarietà. Sei dopo la corretta modularizzazione. Ciascuna delle tue funzioni dovrebbe essere un componente , ovvero una singola classe o un gruppo di classi, con un'interfaccia ben definita e non direttamente dipendente dall'oggetto Application.

Nella situazione attuale ogni funzionalità dipende dall'applicazione e l'applicazione dipende da ciascuna funzione, quindi ogni funzionalità dipende essenzialmente da ogni altra funzionalità, il che rende impossibile testare singole funzionalità separatamente. Eliminando le dipendenze dirette delle funzionalità dall'oggetto Application e passando i dati in entrata e in uscita attraverso le interfacce dei componenti, la situazione cambierà. Se lo fai bene, ti ritroverai con molti componenti indipendenti, in cui puoi cambiare e testare ognuno di essi indipendentemente l'uno dall'altro, con un rischio molto più basso di interrompere il programma quando cambierai qualcosa.

    
risposta data 31.03.2015 - 19:47
fonte
2

Penso che sia perfettamente accettabile che una "caratteristica" abbia bisogno di una "applicazione", ma l'uso di globals è nauseante. L'iniezione delle dipendenze del costruttore può risolvere questo problema e consentire un refactoring graduale del codice.

(Non sono uno sviluppatore C ++, quindi eventuali correzioni / chiarimenti sono ben accetti)

Innanzitutto, la classe Application:

class Application
{
public:
    void run_blog_feature();
};

Application::run_blog_feature() {
    BlogFeature blog(this);

    blog.run();
}

La classe BlogFeature ha un costruttore che consente all'applicazione di passarsi in:

class BlogFeature
{
private:
    int some_setting = 0;
public:
    BlogFeature(Application app);
    void run();
};

BlogFeature::BlogFeature(Application app) {
    this->some_setting = app.get_some_setting();

    // more initialization
};

BlogFeature::run() {
    // do stuff
};

In questo modo le "funzionalità" possono configurarsi in base all'applicazione. Inoltre, è possibile eseguire il refactoring della funzione di applicazione in base alle funzionalità separatamente. A poco a poco si refactoring il codice facendo affidamento su globali in codice basandosi su iniezione del costruttore.

Potresti quindi ridefinirlo ulteriormente e dividere la configurazione dell'applicazione nella sua classe, quindi passare semplicemente l'oggetto config nelle funzionalità per fornire ulteriore disaccoppiamento.

Come passaggio finale, puoi creare un "oggetto fabbrica" e fornirlo con qualche configurazione. L'oggetto factory avrebbe un metodo che restituisce gli oggetti "feature". Tutta la configurazione delle funzionalità avviene all'interno dell'oggetto factory, lasciando le "caratteristiche" e l'applicazione ignoranti del livello di configurazione.

    
risposta data 31.03.2015 - 19:53
fonte
1

I can not include Application header (since that would defeat the purpose of the separation). But since there are these types of globals everywhere in the old code this task is rather complicated.

Devi solo dividere l'attività nei passaggi corretti, e quindi è semplice (anche se potrebbe non essere facile :))

But sometimes there may be a lot of parameters to be passed and since "Feature" is created by "Application" anyway I see why my situation might arise.

Regola: le cose che vengono elaborate insieme devono essere raggruppate insieme.

Ecco un esempio, per il tuo codice funzione:

Codice originale

// RunSomeFeature function def in class Application
void Application::RunSomeFeature()
{
    // create object of type feature
     Feature feature = new Feature(this); // added this as a parameter
                                          // assuming Feature reads lots of data
                                          // from Application, to construct itself

    feature.SomeFunction(void)
}

Passaggio 1

Crea un costruttore che riceve valori già costruiti per i suoi membri dati. Se Feature è costruito usando Application :: arg1, Application :: arg2, ..., Application :: arg_n_, allora avrai bisogno di un costruttore di feature che riceve n argomenti.

Passaggio 2

Estrai la funzione in una classe separata e cambia il codice client dal chiamare Feature::Feature(const Application&) a chiamare Feature::Feature(arg1, arg2, ..., argn) .

nota: poiché le cose che vengono elaborate insieme devono essere raggruppate , prendi in considerazione la scrittura di una funzionalità come questa:

struct FeatureData // or class
{
    // feature args are passed together, so we group them together, here:

    Arg1 arg1;
    Arg2 arg2;
    ...
    Argn argn;
};

class Feature
{
    Feature(FeatureData&& data); // call std::move on data.arg1, ... data.argn to populate feature

};

(invece di avere una caratteristica che riceve n argomenti).

Passaggio 3

Aggiorna Application per essere una specializzazione di FeatureData o costruire un'istanza di FeatureData quando necessario.

Passaggio 4

Sposta feature e featureData in una libreria separata.

I see that this may be a bit open ended but any rules of thumb about at what level would we "completely encapsulate" in order to have pieces of code we can reshuffle easily? Or any design philosophy anyone would share about this type of situation?

Principi SOLID : in questo caso, "I" sta per Interface Segregation (ovvero, non utilizzare tutta l'interfaccia pubblica dell'applicazione per passare argomenti ai costruttori, separare l'interfaccia - in FeatureData e resto dell'applicazione, quindi puoi usarli separatamente).

Legge di Demeter : una classe non dovrebbe sapere / avere accesso a più del necessario per completare il suo lavoro (in questo caso, una caratteristica deve conoscere "un insieme di n valori ", non un oggetto Application e" un insieme di valori n ").

    
risposta data 02.04.2015 - 13:59
fonte
0

La fonte della confusione qui potrebbe essere il fatto che stai visualizzando l'oggetto dell'applicazione come una cosa identica al modello di dominio della tua applicazione. Non sono. L'applicazione contiene il modello di dominio e crea un'istanza delle funzioni che operano su di esso.

Quindi, la mia raccomandazione sarebbe quella di togliere tutte le variabili globali dall'oggetto dell'applicazione e spostarle in un oggetto modello di dominio che viene istanziato e contenuto all'interno dell'oggetto dell'applicazione. Quindi, passare un riferimento a questo oggetto del modello di dominio come parametro costruttore per ciascuna caratteristica che deve operare su di esso.

Affinché funzioni correttamente, l'oggetto modello di dominio dovrebbe offrire eventi di notifica delle modifiche, in modo che quando una funzione lo modifica, altre funzionalità possono prendere atto di questa modifica se necessario.

Va perfettamente bene se una funzione deve fare uso solo di un piccolo sottoinsieme del modello di dominio; non è necessario rompere il modello di dominio in blocchi più piccoli per nutrirli su singole funzionalità. Una tale riduzione delle informazioni disponibili sarebbe troppo artificiale, troppi problemi da mantenere, troppo fragile e non produrrebbe alcun vantaggio considerevole. D'altra parte, se un intero gruppo di funzionalità richiede solo un piccolo sottoinsieme comune del modello di dominio, è possibile che si desideri prendere in considerazione un modello di sottodominio da utilizzare solo per quel gruppo di funzionalità.

    
risposta data 02.04.2015 - 17:09
fonte

Leggi altre domande sui tag