Haruskah saya selalu merangkum struktur data internal sepenuhnya?

11

Silakan pertimbangkan kelas ini:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Kelas ini memperlihatkan array yang digunakannya untuk menyimpan data, ke kode klien yang tertarik.

Saya melakukan ini di aplikasi yang sedang saya kerjakan. Saya memiliki ChordProgressionkelas yang menyimpan urutan Chords (dan melakukan beberapa hal lain). Itu Chord[] getChords()metode yang mengembalikan array akord. Ketika struktur data harus berubah (dari array ke ArrayList), semua kode klien rusak.

Ini membuat saya berpikir - mungkin pendekatan berikut ini lebih baik:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

Alih-alih mengekspos struktur data itu sendiri, semua operasi yang ditawarkan oleh struktur data sekarang ditawarkan langsung oleh kelas yang melampirkannya, menggunakan metode publik yang mendelegasikan ke struktur data.

Ketika struktur data berubah, hanya metode ini yang harus berubah - tetapi setelah dilakukan, semua kode klien masih berfungsi.

Perhatikan bahwa koleksi yang lebih kompleks daripada array mungkin memerlukan kelas penutup untuk mengimplementasikan lebih dari tiga metode hanya untuk mengakses struktur data internal.


Apakah pendekatan ini biasa? Apa pendapatmu tentang ini? Kelemahan apa yang dimilikinya? Apakah masuk akal untuk memiliki kelas terlampir menerapkan setidaknya tiga metode publik hanya untuk mendelegasikan ke struktur data dalam?

Aviv Cohn
sumber

Jawaban:

14

Kode seperti:

   public Thing[] getThings(){
        return things;
    }

Tidak masuk akal, karena metode akses Anda tidak melakukan apa-apa selain secara langsung mengembalikan struktur data internal. Anda mungkin juga menyatakan Thing[] thingsdemikian public. Gagasan di balik metode akses adalah untuk membuat antarmuka yang mengisolasi klien dari perubahan internal dan mencegah mereka dari memanipulasi struktur data aktual kecuali dengan cara diam-diam yang diizinkan oleh antarmuka. Ketika Anda mengetahui ketika semua kode klien Anda rusak, metode akses Anda tidak melakukan itu - itu hanya kode yang terbuang. Saya pikir banyak programmer cenderung menulis kode seperti itu karena mereka belajar di suatu tempat bahwa semuanya perlu dienkapsulasi dengan metode akses - tapi itu untuk alasan yang saya jelaskan. Melakukannya hanya untuk "mengikuti formulir" ketika metode akses tidak melayani tujuan apa pun hanyalah kebisingan.

Saya pasti akan merekomendasikan solusi yang Anda usulkan, yang mencapai beberapa tujuan enkapsulasi yang paling penting: Memberi klien antarmuka yang kuat dan bijaksana yang mengisolasi mereka dari detail implementasi internal kelas Anda, dan tidak memungkinkan mereka menyentuh struktur data internal berharap dengan cara yang Anda memutuskan sesuai - "hukum hak istimewa yang paling tidak diperlukan". Jika Anda melihat kerangka kerja OOP besar yang populer, seperti CLR, STL, VCL, pola yang Anda usulkan tersebar luas, untuk alasan itulah.

Haruskah kamu selalu melakukan itu? Belum tentu. Misalnya, jika Anda memiliki kelas pembantu atau teman yang pada dasarnya merupakan komponen dari kelas pekerja utama Anda dan tidak "menghadap ke depan", itu tidak perlu - ini adalah kerja keras yang akan menambahkan banyak kode yang tidak perlu. Dan dalam hal ini, saya tidak akan menggunakan metode akses sama sekali - tidak masuk akal, seperti yang dijelaskan. Cukup nyatakan struktur data dengan cara yang hanya mencakup kelas utama yang menggunakannya - sebagian besar bahasa mendukung cara melakukan itu - friend, atau mendeklarasikannya dalam file yang sama dengan kelas pekerja utama, dll.

Satu-satunya downside yang bisa saya lihat dalam proposal Anda adalah bahwa itu lebih sulit untuk dikodekan (dan sekarang Anda harus melakukan pengkodean ulang kelas konsumen Anda - tetapi Anda tetap harus melakukan itu.) Tapi itu bukan kelemahan sebenarnya. - Anda perlu melakukannya dengan benar, dan terkadang itu membutuhkan lebih banyak pekerjaan.

Salah satu hal yang membuat programmer baik adalah mereka tahu kapan kerja ekstra itu layak, dan kapan tidak. Dalam jangka panjang menempatkan ekstra sekarang akan membuahkan hasil dengan dividen besar di masa depan - jika tidak pada proyek ini, maka pada yang lain. Belajarlah kode dengan cara yang benar dan gunakan kepala Anda tentang hal itu, bukan hanya secara robotik mengikuti formulir yang ditentukan.

Perhatikan bahwa koleksi yang lebih kompleks daripada array mungkin memerlukan kelas penutup untuk mengimplementasikan lebih dari tiga metode hanya untuk mengakses struktur data internal.

Jika Anda mengekspos seluruh struktur data melalui kelas yang berisi, IMO Anda harus memikirkan mengapa kelas itu dienkapsulasi sama sekali, jika itu bukan hanya untuk menyediakan antarmuka yang lebih aman - "kelas pembungkus". Anda mengatakan kelas yang mengandung tidak ada untuk tujuan itu - jadi mungkin ada sesuatu yang tidak beres tentang desain Anda. Pertimbangkan untuk memecah kelas-kelas Anda menjadi modul-modul yang lebih bijaksana dan meletakannya.

Kelas harus memiliki satu tujuan yang jelas dan rahasia, dan menyediakan antarmuka untuk mendukung fungsi itu - tidak lebih. Anda mungkin mencoba untuk menggabungkan hal-hal yang bukan milik bersama. Ketika Anda melakukan itu, segala sesuatu akan rusak setiap kali Anda harus menerapkan perubahan. Semakin kecil dan bijaksana kelas Anda, semakin mudah untuk mengubah keadaan: Pikirkan LEGO.

Vektor
sumber
1
Terimakasih telah menjawab. Sebuah pertanyaan: Bagaimana dengan jika struktur data internal memiliki, mungkin, 5 metode publik - bahwa semua harus ditampilkan oleh antarmuka publik kelas saya? Sebagai contoh, Jawa ArrayList memiliki metode berikut: get(index), add(), size(), remove(index), dan remove(Object). Menggunakan teknik yang diusulkan, kelas yang berisi ArrayList ini harus memiliki lima metode publik hanya untuk mendelegasikan ke koleksi batin. Dan tujuan kelas ini dalam program ini kemungkinan besar tidak merangkum ArrayList ini, tetapi melakukan sesuatu yang lain. ArrayList hanyalah detail. [...]
Aviv Cohn
Struktur data dalam hanyalah anggota biasa, yang menggunakan teknik di atas - mengharuskannya berisi kelas untuk menampilkan lima metode publik tambahan. Menurut pendapat Anda - apakah ini masuk akal? Dan juga - apakah ini biasa?
Aviv Cohn
@Prog - Bagaimana dengan jika struktur data internal memiliki, mungkin, 5 metode publik ... IMO jika Anda merasa perlu untuk membungkus seluruh kelas pembantu di dalam kelas utama Anda dan mengeksposnya seperti itu, Anda perlu berpikir ulang bahwa desain - kelas publik Anda melakukan terlalu banyak dan / atau tidak menghadirkan antarmuka yang sesuai. Kelas harus memiliki peran yang sangat rahasia dan jelas, dan antarmuka harus mendukung peran itu dan hanya peran itu. Pikirkan tentang pemecahan dan peletakan kelas Anda. Kelas seharusnya tidak menjadi "kitchen sink" yang berisi semua jenis objek atas nama enkapsulasi.
Vektor
Jika Anda mengekspos seluruh struktur data melalui kelas wrapper, IMO Anda harus memikirkan mengapa kelas itu dienkapsulasi sama sekali jika itu bukan hanya untuk menyediakan antarmuka yang lebih aman. Anda mengatakan kelas yang mengandung tidak ada untuk tujuan itu - jadi ada sesuatu yang tidak beres tentang desain ini.
Vektor
1
@ Phoshi - Read-only adalah kata kunci - Saya setuju dengan itu. Tetapi OP tidak berbicara tentang read-only. misalnya removetidak baca saja. Pemahaman saya adalah OP ingin mempublikasikan semuanya - seperti dalam kode asli sebelum perubahan yang diusulkan. public Thing[] getThings(){return things;}Itulah yang tidak saya sukai.
Vektor
2

Anda bertanya: Haruskah saya selalu merangkum struktur data internal sepenuhnya?

Jawaban Singkat: Ya, sebagian besar waktu tetapi tidak selalu .

Jawaban Panjang: Saya pikir kelas mengikuti kategori berikut:

  1. Kelas yang merangkum data sederhana. Contoh: titik 2D. Sangat mudah untuk membuat fungsi publik yang menyediakan kemampuan untuk mendapatkan / mengatur koordinat X dan Y tetapi Anda dapat menyembunyikan data internal dengan mudah tanpa terlalu banyak kesulitan. Untuk kelas semacam itu, mengekspos detail struktur data internal tidak perlu dilakukan.

  2. Kelas kontainer yang merangkum koleksi. STL memiliki kelas wadah klasik. Saya mempertimbangkan std::stringdan di std::wstringantara mereka juga. Mereka menyediakan antarmuka yang kaya untuk berurusan dengan abstraksi tetapi std::vector, std::string, dan std::wstringjuga menyediakan kemampuan untuk mendapatkan akses ke data mentah. Saya tidak akan tergesa-gesa memanggil mereka kelas yang dirancang dengan buruk. Saya tidak tahu pembenaran untuk kelas-kelas ini yang mengekspos data mentah mereka. Namun, saya telah, dalam pekerjaan saya, merasa perlu untuk mengekspos data mentah ketika berhadapan dengan jutaan node mesh dan data pada node mesh tersebut untuk alasan kinerja.

    Hal penting tentang mengekspos struktur internal kelas adalah bahwa Anda harus berpikir panjang dan keras sebelum memberikannya sinyal hijau. Jika antarmuka internal untuk suatu proyek, itu akan mahal untuk mengubahnya di masa depan tetapi bukan tidak mungkin. Jika antarmuka adalah eksternal untuk proyek (seperti ketika Anda sedang mengembangkan perpustakaan yang akan digunakan oleh pengembang aplikasi lain), mungkin tidak mungkin untuk mengubah antarmuka tanpa kehilangan klien Anda.

  3. Kelas yang sebagian besar fungsional di alam. Contoh: std::istream,, std::ostreamiterator dari wadah STL. Benar-benar bodoh untuk mengekspos detail internal kelas-kelas ini.

  4. Kelas hibrid. Ini adalah kelas yang merangkum beberapa struktur data tetapi juga menyediakan fungsionalitas algoritmik. Secara pribadi, saya pikir ini adalah hasil dari desain yang dipikirkan dengan buruk. Namun, jika Anda menemukannya, Anda harus memutuskan apakah akan masuk akal untuk mengekspos data internal mereka berdasarkan kasus per kasus.

Kesimpulannya: Satu-satunya waktu saya merasa perlu untuk mengekspos struktur data internal kelas adalah ketika itu menjadi hambatan kinerja.

R Sahu
sumber
Saya pikir alasan paling penting bahwa STL memperlihatkan data internal mereka adalah kompatibilitas dengan semua fungsi yang mengharapkan pointer, yang banyak.
Siyuan Ren
0

Alih-alih mengembalikan data mentah secara langsung, coba sesuatu seperti ini

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

Jadi, Anda pada dasarnya menyediakan koleksi khusus yang menghadirkan wajah apa pun yang diinginkan dunia. Maka dalam implementasi baru Anda,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

Sekarang Anda memiliki enkapsulasi yang tepat, menyembunyikan detail implementasi, dan memberikan kompatibilitas mundur (dengan biaya).

BobDalgleish
sumber
Ide cerdas untuk kompatibilitas mundur. Tapi: Sekarang Anda memiliki enkapsulasi yang tepat, menyembunyikan rincian implementasi - tidak benar-benar. Klien masih harus berurusan dengan nuansa List. Metode akses yang hanya mengembalikan anggota data, bahkan dengan para pemain untuk membuat hal-hal yang lebih kuat, tidak benar-benar baik enkapsulasi IMO. Kelas pekerja harus menangani semua itu, bukan klien. "Dumber" klien harus, semakin kuat itu. Selain itu, saya tidak yakin Anda menjawab pertanyaan ...
Vektor
1
@Vektor - Anda benar. Struktur data yang dikembalikan masih bisa berubah dan efek samping akan membunuh informasi yang disembunyikan.
BobDalgleish
Struktur data yang dikembalikan masih bisa berubah dan efek samping akan membunuh informasi yang disembunyikan - ya, itu juga - itu berbahaya. Saya hanya berpikir dalam hal apa yang diperlukan dari klien, yang merupakan fokus dari pertanyaan.
Vektor
@ BobDalgleish: mengapa tidak mengembalikan salinan koleksi asli?
Giorgio
1
@ BobDalgleish: Kecuali ada alasan kinerja yang baik, saya akan mempertimbangkan untuk mengembalikan referensi ke struktur data internal untuk memungkinkan penggunanya mengubahnya sebagai keputusan desain yang sangat buruk. Keadaan internal suatu objek hanya boleh diubah melalui metode publik yang sesuai.
Giorgio
0

Anda harus menggunakan antarmuka untuk hal-hal itu. Tidak akan membantu dalam kasus Anda, karena array Java tidak mengimplementasikan antarmuka itu, tetapi Anda harus melakukannya mulai sekarang:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

Dengan cara itu Anda dapat mengubah ArrayListke LinkedListatau apa pun, dan Anda tidak akan merusak kode apapun karena semua koleksi Java (samping array) yang memiliki (semu?) Akses acak mungkin akan mengimplementasikan List.

Anda juga dapat menggunakan Collection, yang menawarkan lebih sedikit metode daripada Listtetapi dapat mendukung koleksi tanpa akses acak, atau Iterableyang bahkan dapat mendukung stream tetapi tidak menawarkan banyak hal dalam hal metode akses ...

Idan Arye
sumber
-1 - kompromi yang buruk dan IMO yang tidak terlalu aman: Anda mengekspos struktur data internal kepada klien, hanya menutupinya dan berharap yang terbaik karena "koleksi Java ... mungkin akan mengimplementasikan Daftar." Jika solusi Anda benar-benar berdasarkan polimorfik / pewarisan - bahwa semua koleksi selalu diimplementasikan Listsebagai kelas turunan, itu akan lebih masuk akal, tetapi hanya "berharap yang terbaik" bukanlah ide yang baik. "Seorang programmer yang baik terlihat jalan dua arah di jalan satu arah".
Vektor
@Vektor Ya, saya mengasumsikan koleksi Java di masa depan akan menerapkan List(atau Collection, atau setidaknya Iterable). Itulah inti dari antarmuka ini, dan itu memalukan bahwa Java Array tidak mengimplementasikannya, tetapi mereka adalah antarmuka resmi untuk koleksi di Jawa sehingga tidak terlalu jauh untuk menganggap koleksi Java akan mengimplementasikannya - kecuali jika koleksi itu lebih tua daripada List, dan dalam hal itu sangat mudah untuk membungkusnya dengan AbstractList .
Idan Arye
Anda mengatakan bahwa asumsi Anda hampir dijamin benar, jadi OK - saya akan menghapus suara turun (ketika saya diizinkan) karena Anda cukup layak untuk menjelaskan, dan saya bukan orang Jawa kecuali dengan osmosis. Namun, saya tidak mendukung gagasan ini untuk mengekspos struktur data internal, terlepas dari bagaimana hal itu dilakukan, dan Anda belum langsung menjawab pertanyaan OP, yang sebenarnya tentang enkapsulasi. yaitu membatasi akses ke struktur data internal.
Vektor
1
@Vektor Ya, pengguna dapat memasukkannya Listke dalam ArrayList, tetapi ini tidak seperti implementasinya yang 100% terlindungi - Anda selalu dapat menggunakan refleksi untuk mengakses bidang pribadi. Melakukan hal itu disukai, tetapi casting juga disukai (tidak sebanyak itu). Inti enkapsulasi bukanlah mencegah peretasan berbahaya - melainkan untuk mencegah pengguna bergantung pada detail implementasi yang mungkin ingin Anda ubah. Menggunakan Listantarmuka tidak persis seperti itu - pengguna kelas dapat bergantung pada Listantarmuka, bukan ArrayListkelas konkret yang mungkin berubah.
Idan Arye
Anda selalu dapat menggunakan refleksi untuk mengakses bidang pribadi tentu saja - jika seseorang ingin menulis kode yang buruk dan menumbangkan desain, mereka dapat melakukannya. alih-alih, itu untuk mencegah pengguna ... - itulah salah satu alasan untuk enkapsulasi. Cara lainnya adalah memastikan integritas dan konsistensi kondisi internal kelas Anda. Masalahnya bukan "peretas berbahaya", tetapi organisasi yang buruk yang mengarah ke bug jahat. "Hukum hak istimewa yang paling tidak perlu" - beri konsumen kelas Anda hanya apa yang wajib - tidak lebih. Jika Anda wajib membuat seluruh struktur data internal publik, Anda punya masalah desain.
Vektor
-2

Ini cukup umum untuk menyembunyikan struktur data internal Anda dari dunia luar. Kadang-kadang itu berlebihan khususnya di DTO. Saya merekomendasikan ini untuk model domain. Jika diperlukan untuk mengekspos, kembalikan salinan yang tidak dapat diubah. Bersamaan dengan ini saya sarankan membuat antarmuka yang memiliki metode ini seperti mendapatkan, mengatur, menghapus dll.

VGaur
sumber
1
ini sepertinya tidak menawarkan sesuatu yang substansial lebih dari 3 jawaban sebelumnya
agas