Apakah mengembalikan pointer ke objek yang dibuat melanggar enkapsulasi

8

Ketika saya ingin membuat objek yang menggabungkan objek lain, saya mendapati diri saya ingin memberikan akses ke objek internal alih-alih mengungkapkan antarmuka ke objek internal dengan fungsi passthrough.

Misalnya, kita memiliki dua objek:

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

Dan katakanlah kita ingin memiliki objek Mobil yang terdiri dari Engine dan ModelName (ini jelas dibuat-buat). Salah satu cara yang mungkin untuk melakukannya adalah memberikan akses ke masing-masing

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

Menggunakan objek ini akan terlihat seperti ini:

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

Kemungkinan lain adalah membuat fungsi publik pada objek Mobil untuk setiap fungsi (yang diinginkan) pada objek internal, seperti ini:

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

Contoh kedua akan digunakan seperti ini:

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

Kekhawatiran saya dengan contoh pertama adalah bahwa hal itu melanggar enkapsulasi OO dengan memberikan akses langsung ke anggota pribadi.

Kekhawatiran saya dengan contoh kedua adalah bahwa, ketika kita mencapai level yang lebih tinggi dalam hirarki kelas, kita dapat berakhir dengan kelas "seperti dewa" yang memiliki antarmuka publik yang sangat besar (melanggar "I" dalam SOLID).

Manakah dari dua contoh yang mewakili desain OO yang lebih baik? Atau apakah kedua contoh menunjukkan kurangnya pemahaman OO?

Matthew James Briggs
sumber

Jawaban:

5

Saya menemukan diri saya ingin memberikan akses ke objek internal daripada mengungkapkan antarmuka ke objek internal dengan fungsi passthrough.

Jadi, mengapa itu internal?

Tujuannya bukan untuk "mengungkapkan antarmuka ke objek internal" tetapi untuk membuat antarmuka yang koheren, konsisten, dan ekspresif. Jika fungsionalitas objek internal perlu diekspos dan pass-through sederhana akan dilakukan, maka pass-through. Desain yang baik adalah tujuannya, bukan "hindari pengkodean sepele."

Memberi akses ke objek internal berarti:

  • Klien harus tahu tentang orang-orang internal untuk menggunakannya.
  • Di atas berarti abstraksi yang diinginkan dihembuskan dari air.
  • Anda mengekspos metode dan properti publik objek internal lainnya, memungkinkan klien untuk memanipulasi keadaan Anda dengan cara yang tidak disengaja.
  • Kopling meningkat secara signifikan. Sekarang Anda berisiko melanggar kode klien jika Anda memodifikasi objek internal, mengubah tanda tangan metode itu, atau bahkan mengganti seluruh objek (ubah tipenya).
  • Semua ini adalah alasan mengapa kita memiliki Hukum Demeter. Demeter tidak mengatakan "baik, jika itu hanya lewat, maka tidak masalah untuk mengabaikan prinsip ini."
radarbob
sumber
Catatan: mesin ini sangat relevan dengan antarmuka MaintainableVehicle tetapi tidak relevan dengan DrivableVehicle. Mekanik perlu tahu tentang mesin (dengan semua detailnya, mungkin), tetapi pengemudi tidak. (Dan penumpang tidak perlu tahu tentang setir)
user253751
2

Saya tidak berpikir itu pasti melanggar enkapsulasi untuk mengembalikan referensi ke objek yang dibungkus, terutama jika mereka const. Keduanya std::stringdan std::vectorlakukan ini. Jika Anda dapat mulai mengubah internal objek dari bawah tanpa melalui antarmuka, itu lebih dipertanyakan, tetapi jika Anda sudah bisa melakukannya secara efektif dengan setter, enkapsulasi adalah ilusi pula.

Wadah sangat sulit untuk masuk ke dalam paradigma ini; sulit membayangkan daftar berguna yang tidak dapat diuraikan menjadi kepala dan ekor. Sampai batas tertentu, Anda dapat menulis antarmuka seperti std::find()yang ortogonal dengan tata letak internal struktur data. Haskell melangkah lebih jauh dengan kelas-kelas seperti Foldable dan Traversible. Tetapi pada titik tertentu, Anda akhirnya mengatakan bahwa semua yang Anda ingin hentikan untuk melakukan enkapsulasi sekarang berada di dalam penghalang enkapsulasi.

Davislor
sumber
Dan khususnya jika rujukannya adalah kelas abstrak yang diperluas dengan implementasi konkret yang digunakan kelas Anda; maka Anda tidak mengungkapkan implementasinya, tetapi hanya menyediakan antarmuka yang harus didukung oleh implementasinya.
Jules