合成されたオブジェクトへのポインタを返すことはカプセル化に違反しますか


8

他のオブジェクトを集約するオブジェクトを作成する場合、パススルー関数を使用して内部オブジェクトへのインターフェイスを公開するのではなく、内部オブジェクトへのアクセスを許可したいと思います。

たとえば、2つのオブジェクトがあるとします。

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

EngineとModelNameの両方で構成されるCarオブジェクトが必要だとします(これは明らかに工夫されています)。そうするための1つの可能な方法は、これらのそれぞれにアクセス権を与えることです

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

このオブジェクトを使用すると、次のようになります。

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

別の可能性は、次のように、内部オブジェクトの(必要な)関数ごとにCarオブジェクトにパブリック関数を作成することです。

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

2番目の例は次のように使用されます。

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

最初の例に対する私の懸念は、プライベートメンバーに直接アクセスを与えることにより、OOカプセル化に違反することです。

2番目の例についての私の懸念は、クラス階層の上位レベルに到達すると、非常に大きなパブリックインターフェース(SOLIDの「I」に違反)を持つ「神のような」クラスに巻き込まれる可能性があることです。

2つの例のどちらが優れたOO設計を表していますか?または、両方の例がOOの理解の欠如を示していますか?

回答:


5

パススルー関数を使用して内部オブジェクトへのインターフェイスを公開するのではなく、内部オブジェクトへのアクセスを許可したいと思っています。

では、なぜ内部的なのでしょうか。

目的は、「内部オブジェクトへのインターフェースを明らかにする」ことではなく、首尾一貫した一貫した表現力のあるインターフェースを作成することです。内部オブジェクトの機能を公開する必要があり、単純なパススルーが必要な場合は、パススルーします。優れた設計が目標であり、「些細なコーディングを避ける」ことではありません。

内部オブジェクトへのアクセスを許可するとは、次のことを意味します。

  • クライアントは、それらを使用するためにそれらの内部について知る必要があります。
  • 上記は、望ましい抽象化が水から吹き飛ばされることを意味します。
  • 内部オブジェクトの他のパブリックメソッドとプロパティを公開し、クライアントが意図しない方法で状態を操作できるようにします。
  • カップリングが大幅に増加しました。内部オブジェクトを変更したり、メソッドシグネチャを変更したり、オブジェクト全体を置き換えたり(タイプを変更したり)した場合、クライアントコードが破損する危険があります。
  • これがすべて、デメテルの法則がある理由です。デメテルは「まあ、それが単に通過しているだけなら、この原則を無視しても大丈夫だ」とは言っていません。

補足:エンジンはMaintainableVehicleインターフェースに非常に関連していますが、DrivableVehicleには関連していません。整備士はエンジンについて(おそらくそのすべての詳細について)知る必要がありますが、ドライバーは知りません。(そして、乗客はステアリングホイールについて知る必要はありません)
user253751

2

特にconstの場合、ラップされたオブジェクトへの参照を返すことは、必ずしもカプセル化に違反するとは思いません。両方std::stringstd::vectorこれを行います。インターフェースを経由せずにオブジェクトの下からオブジェクトの内部を変更し始めることができるなら、それはもっと疑わしいですが、セッターですでにそれを効果的に行うことができれば、とにかくカプセル化は幻想でした。

コンテナーは、このパラダイムに合わせるのが特に困難です。頭と尾に分解できない便利なリストを想像するのは困難です。ある程度、std::find()データ構造の内部レイアウトに直交するようなインターフェースを作成できます。Haskellは、FoldableやTraversibleなどのクラスにさらに進んでいます。しかし、ある時点で、カプセル化を解除するために必要なすべてのものがカプセル化バリアの内部にあると言ってきました。


そして特に、参照が、クラスが使用する具体的な実装によって拡張される抽象クラスへの参照である場合。次に、実装を明らかにするのではなく、実装がサポートする必要のあるインターフェースを提供するだけです。
Jules
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.