Verstößt die Rückgabe des Zeigers auf zusammengesetzte Objekte gegen die Kapselung?

8

Wenn ich ein Objekt erstellen möchte, das andere Objekte aggregiert, möchte ich Zugriff auf die internen Objekte gewähren, anstatt die Schnittstelle zu den internen Objekten mit Passthrough-Funktionen anzuzeigen.

Angenommen, wir haben zwei Objekte:

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;
};

Nehmen wir an, wir möchten ein Autoobjekt haben, das sowohl aus einer Engine als auch aus einem ModelName besteht (dies ist offensichtlich erfunden). Ein möglicher Weg, dies zu tun, wäre, jedem von ihnen Zugang zu gewähren

/* 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;
};

Die Verwendung dieses Objekts würde folgendermaßen aussehen:

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

Eine andere Möglichkeit wäre, eine öffentliche Funktion für das Autoobjekt für jede der (gewünschten) Funktionen für die internen Objekte wie folgt zu erstellen:

/* 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;
};

Das zweite Beispiel würde folgendermaßen verwendet:

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

Mein Anliegen beim ersten Beispiel ist, dass es die OO-Kapselung verletzt, indem es den privaten Mitgliedern direkten Zugriff gewährt.

Meine Sorge mit dem zweiten Beispiel ist, dass wir, wenn wir zu höheren Ebenen in der Klassenhierarchie gelangen, mit "gottähnlichen" Klassen enden könnten, die sehr große öffentliche Schnittstellen haben (was das "Ich" in SOLID verletzt).

Welches der beiden Beispiele steht für ein besseres OO-Design? Oder zeigen beide Beispiele einen Mangel an OO-Verständnis?

Matthew James Briggs
quelle

Antworten:

5

Ich möchte Zugriff auf die internen Objekte gewähren, anstatt die Schnittstelle zu den internen Objekten mit Passthrough-Funktionen anzuzeigen.

Warum ist es dann intern?

Ziel ist es nicht, "die Schnittstelle zum internen Objekt aufzudecken", sondern eine kohärente, konsistente und ausdrucksstarke Schnittstelle zu schaffen. Wenn die Funktionalität eines internen Objekts verfügbar gemacht werden muss und ein einfacher Durchgang ausreicht, dann Durchgang. Gutes Design ist das Ziel, nicht "triviale Codierung vermeiden".

Zugriff auf ein internes Objekt zu gewähren bedeutet:

  • Der Kunde muss über diese Interna Bescheid wissen, um sie verwenden zu können.
  • Das obige bedeutet, dass die gewünschte Abstraktion aus dem Wasser geblasen wird.
  • Sie machen die anderen öffentlichen Methoden und Eigenschaften des internen Objekts verfügbar, sodass der Client Ihren Status auf unbeabsichtigte Weise bearbeiten kann.
  • Deutlich erhöhte Kopplung. Jetzt besteht die Gefahr, dass der Clientcode beschädigt wird, wenn Sie das interne Objekt ändern, die Methodensignatur ändern oder sogar das gesamte Objekt ersetzen (den Typ ändern).
  • All dies ist der Grund, warum wir das Gesetz von Demeter haben. Demeter sagt nicht: " Nun , wenn es nur durchgeht, ist es in Ordnung, dieses Prinzip zu ignorieren."
Radarbob
quelle
Randnotiz: Der Motor ist für die MaintainableVehicle-Schnittstelle sehr relevant, für DrivableVehicle jedoch nicht. Der Mechaniker muss über den Motor Bescheid wissen (wahrscheinlich in allen Einzelheiten), der Fahrer jedoch nicht. (Und Passagiere müssen nichts über das Lenkrad wissen)
user253751
2

Ich denke nicht, dass es notwendigerweise die Kapselung verletzt, Verweise auf das umschlossene Objekt zurückzugeben, insbesondere wenn sie const sind. Beides std::stringund std::vectormach das. Wenn Sie anfangen können, die Interna des Objekts darunter zu ändern, ohne die Schnittstelle zu durchlaufen, ist dies fragwürdiger. Wenn Sie dies jedoch bereits mit Setzern effektiv tun könnten, wäre die Kapselung ohnehin eine Illusion.

Container sind besonders schwer in dieses Paradigma zu integrieren. Es ist schwer vorstellbar, dass eine nützliche Liste nicht in Kopf und Schwanz zerlegt werden kann. Bis zu einem gewissen Grad können Sie Schnittstellen schreiben std::find(), die orthogonal zum internen Layout der Datenstruktur sind. Haskell geht noch weiter mit Klassen wie Foldable und Traversible. Aber irgendwann haben Sie gesagt, dass sich alles, wofür Sie die Kapselung unterbrechen wollten, jetzt innerhalb der Kapselungsbarriere befindet.

Davislor
quelle
Und insbesondere, wenn sich der Verweis auf eine abstrakte Klasse bezieht, die durch die konkrete Implementierung Ihrer Klasse erweitert wird. Dann enthüllen Sie nicht die Implementierung, sondern stellen lediglich eine Schnittstelle bereit, die die Implementierung unterstützen muss.
Jules