Il puntatore di ritorno agli oggetti composti viola l'incapsulamento

8

Quando voglio creare un oggetto che aggrega altri oggetti, mi trovo a voler dare accesso agli oggetti interni invece di rivelare l'interfaccia agli oggetti interni con le funzioni passthrough.

Ad esempio, diciamo che abbiamo due oggetti:

class Engine;
using EnginePtr = unique_ptr<Engine>;
class Engine
{
public:
    Engine( int size ) : mySize( 1 ) { setSize( size ); }
    int getSize() const { return mySize; }
    void setSize( const int size ) { mySize = size; }
    void doStuff() const { /* do stuff */ }
private:
    int mySize;
};

class ModelName;
using ModelNamePtr = unique_ptr<ModelName>;
class ModelName
{
public:
    ModelName( const string& name ) : myName( name ) { setName( name ); }
    string getName() const { return myName; }
    void setName( const string& name ) { myName = name; }
    void doSomething() const { /* do something */ }
private:
    string myName;
};

E diciamo che vogliamo avere un oggetto Car che sia composto sia da un Motore che da un Nome Modello (questo è ovviamente concepito). Un modo possibile per farlo sarebbe quello di dare accesso a ciascuno di questi

/* give access */
class Car1
{
public:
    Car1() : myModelName{ new ModelName{ "default" } }, myEngine{ new Engine{ 2 } } {}
    const ModelNamePtr& getModelName() const { return myModelName; }
    const EnginePtr& getEngine() const { return myEngine; }
private:
    ModelNamePtr myModelName;
    EnginePtr myEngine;
};

L'uso di questo oggetto sarebbe simile a questo:

Car1 car1;
car1.getModelName()->setName( "Accord" );
car1.getEngine()->setSize( 2 );
car1.getEngine()->doStuff();

Un'altra possibilità sarebbe quella di creare una funzione pubblica sull'oggetto Car per ognuna delle funzioni (desiderate) sugli oggetti interni, in questo modo:

/* passthrough functions */
class Car2
{
public:
    Car2() : myModelName{ new ModelName{ "default" } }, myEngine{ new Engine{ 2 } } {}
    string getModelName() const { return myModelName->getName(); }
    void setModelName( const string& name ) { myModelName->setName( name ); }
    void doModelnameSomething() const { myModelName->doSomething(); }
    int getEngineSize() const { return myEngine->getSize(); }
    void setEngineSize( const int size ) { myEngine->setSize( size ); }
    void doEngineStuff() const { myEngine->doStuff(); }
private:
    ModelNamePtr myModelName;
    EnginePtr myEngine;
};

Il secondo esempio dovrebbe essere usato in questo modo:

Car2 car2;
car2.setModelName( "Accord" );
car2.setEngineSize( 2 );
car2.doEngineStuff();

La mia preoccupazione per il primo esempio è che viola l'incapsulamento OO dando accesso diretto ai membri privati.

La mia preoccupazione per il secondo esempio è che, quando arriviamo a livelli più alti nella gerarchia delle classi, potremmo finire con classi "divine" che hanno interfacce pubbliche molto grandi (viola la "I" in SOLID).

Quale dei due esempi rappresenta un design OO migliore? Oppure entrambi gli esempi dimostrano una mancanza di comprensione OO?

    
posta Matthew James Briggs 27.09.2015 - 01:09
fonte

2 risposte

5

I find myself wanting to give access to the internal objects instead of revealing the interface to the internal objects with passthrough functions.

Quindi, perché allora è interno?

L'obiettivo non è "rivelare l'interfaccia all'oggetto interno" ma creare un'interfaccia coerente, coerente ed espressiva. Se una funzionalità di un oggetto interno deve essere esposta e un semplice pass-through lo farà, quindi pass-through. Il buon design è l'obiettivo, non "evitare la codifica banale".

Dare accesso a un oggetto interno significa:

  • Il cliente deve sapere di questi interni per usarli.
  • Quanto sopra significa che l'astrazione desiderata è saltata fuori dall'acqua.
  • Esponi gli altri metodi e proprietà pubbliche dell'oggetto interno, consentendo al client di manipolare il tuo stato in modi involontari.
  • Accoppiamento significativamente aumentato. Ora sei a rischio di violare il codice client se dovessi modificare l'oggetto interno, cambiarne la firma del metodo o addirittura sostituire l'intero oggetto (cambia il tipo).
  • Tutto questo è il motivo per cui abbiamo la legge di Demeter. Demeter non dice "bene, se è solo che passa, allora va bene ignorare questo principio."
risposta data 27.09.2015 - 07:51
fonte
2

Non penso che violi necessariamente l'incapsulamento per restituire riferimenti all'oggetto avvolto, in particolare se sono const. Sia std::string che std::vector fanno questo. Se è possibile iniziare a modificare gli interni dell'oggetto da sotto senza passare attraverso la sua interfaccia, è più discutibile, ma se si potesse già farlo efficacemente con i setter, l'incapsulamento era comunque un'illusione.

I contenitori sono particolarmente difficili da inserire in questo paradigma; è difficile immaginare una lista utile che non possa essere scomposta in testa e coda. In una certa misura, puoi scrivere interfacce come std::find() che sono ortogonali al layout interno della struttura dati. Haskell va oltre con classi come Pieghevole e Traversibile. Ma ad un certo punto hai finito per dire che tutto ciò che volevi interrompere l'incapsulamento è ora all'interno della barriera di incapsulamento.

    
risposta data 27.09.2015 - 04:55
fonte

Leggi altre domande sui tag