Encapsulation bukanlah penyembunyian maklumat

Perkataan licin. Seperti Humpty Dumpty yang dinyatakan dalam Lewis Carroll's Through the Looking Glass, "Apabila saya menggunakan kata, itu bermaksud apa yang saya pilih untuk dimaknai - tidak lebih atau kurang." Tentunya penggunaan umum kata-kata enkapsulasi dan penyembunyian maklumat nampaknya mengikuti logik itu. Pengarang jarang membezakan antara keduanya dan sering secara langsung mendakwa mereka sama.

Adakah itu berjaya? Bukan untuk saya. Sekiranya hanya masalah perkataan, saya tidak akan menulis perkataan lain mengenai perkara itu. Tetapi terdapat dua konsep yang berbeza di sebalik istilah ini, konsep yang dihasilkan secara terpisah dan paling baik difahami secara terpisah.

Encapsulation merujuk kepada pengumpulan data dengan kaedah yang beroperasi pada data tersebut. Selalunya definisi itu salah difahami bahawa data entah bagaimana tersembunyi. Di Java, anda dapat memiliki data yang dikemas yang tidak tersembunyi sama sekali.

Walau bagaimanapun, menyembunyikan data bukanlah keseluruhan maklumat yang disembunyikan. David Parnas pertama kali memperkenalkan konsep penyembunyian maklumat sekitar tahun 1972. Dia berpendapat bahawa kriteria utama untuk modularisasi sistem harus menyangkut penyembunyian keputusan reka bentuk kritis. Dia menekankan untuk menyembunyikan "keputusan reka bentuk yang sukar atau keputusan reka bentuk yang mungkin akan berubah." Menyembunyikan maklumat dengan cara itu mengasingkan pelanggan daripada memerlukan pengetahuan mendalam mengenai reka bentuk untuk menggunakan modul, dan dari kesan mengubah keputusan tersebut.

Dalam artikel ini, saya meneroka perbezaan antara enkapsulasi dan penyembunyian maklumat melalui pengembangan kod contoh. Perbincangan menunjukkan bagaimana Java memfasilitasi enkapsulasi dan menyiasat kesan negatif enkapsulasi tanpa menyembunyikan data. Contohnya juga menunjukkan cara meningkatkan reka bentuk kelas melalui prinsip penyembunyian maklumat.

Kelas kedudukan

Dengan kesedaran yang semakin meningkat mengenai potensi Internet tanpa wayar, banyak pakar mengharapkan perkhidmatan berasaskan lokasi untuk memberi peluang untuk aplikasi pembunuh tanpa wayar pertama. Untuk contoh kod artikel ini, saya telah memilih kelas yang mewakili lokasi geografi titik di permukaan bumi. Sebagai entiti domain, kelas, yang dinamakan Position, mewakili maklumat Sistem Kedudukan Global (GPS). Potongan pertama di kelas kelihatan semudah:

kedudukan kelas awam {public latitude ganda; longitud berkembar awam; }

Kelas mengandungi dua item data: GPS latitudedan longitude. Pada masa ini, Positiontidak lebih dari sekumpulan data kecil. Walaupun demikian, Positionadalah kelas, dan Positionobjek boleh dijadikan contoh menggunakan kelas. Untuk menggunakan objek tersebut, kelas PositionUtilitymengandungi kaedah untuk mengira jarak dan arah - iaitu arah - antara Positionobjek yang ditentukan :

kelas awam PositionUtility {public double static jarak (Kedudukan kedudukan1, Kedudukan kedudukan2) {// Hitung dan kembalikan jarak antara kedudukan yang ditentukan. } tajuk berganda statik awam (Posisi kedudukan1, Kedudukan kedudukan2) {// Hitung dan kembalikan tajuk dari kedudukan1 ke kedudukan2. }}

Saya menghilangkan kod pelaksanaan sebenar untuk pengiraan jarak dan arah.

Kod berikut menunjukkan penggunaan biasa Positiondan PositionUtility:

// Buat Kedudukan yang mewakili rumah saya Kedudukan myHouse = Kedudukan baru (); myHouse.latitude = 36.538611; myHouse.longitude = -121.797500; // Buat Kedudukan yang mewakili kedai kopi tempatan Kedudukan coffeeShop = Kedudukan baru (); coffeeShop.latitude = 36.539722; coffeeShop.longitude = -121.907222; // Gunakan PositionUtility untuk mengira jarak dan menuju dari rumah saya // ke kedai kopi tempatan. jarak berganda = PositionUtility.distance (myHouse, coffeeShop); tajuk dua = PositionUtility.heading (myHouse, coffeeShop); // Cetak hasil Sistem.out.println ("Dari rumah saya di (" + myHouse.latitude + "," + myHouse.longitude + ") ke kedai kopi di (" + coffeeShop.latitude + "," + coffeeShop. bujur + ") adalah jarak" + jarak + "pada tajuk" + tajuk + "darjah." );

Kod menghasilkan output di bawah, yang menunjukkan bahawa kedai kopi terletak di sebelah barat (270.8 darjah) rumah saya pada jarak 6.09. Perbincangan kemudian menangani kekurangan unit jarak.

================================================== ================= Dari rumah saya di (36.538611, -121.7975) ke kedai kopi di (36.539722, -121.907222) adalah jarak 6.0873776351893385 pada tajuk 270.7547022304523 darjah. ================================================== =================

Position, PositionUtilitydan penggunaan kod mereka agak menyusahkan dan tentunya tidak sangat berorientasikan objek. Tetapi bagaimana ia boleh berlaku? Java adalah bahasa berorientasi objek, dan kod menggunakan objek!

Walaupun kod tersebut dapat menggunakan objek Java, kode tersebut mengingatkannya pada era yang berlalu: fungsi utiliti yang beroperasi pada struktur data. Selamat datang ke 1972! Ketika Presiden Nixon menyoroti rakaman pita rahsia, profesional komputer yang menggunakan bahasa prosedur Fortran dengan bersemangat menggunakan Perpustakaan Matematik dan Statistik Antarabangsa (IMSL) baru dengan cara ini. Repositori kod seperti IMSL penuh dengan fungsi untuk pengiraan berangka. Pengguna menyampaikan data ke fungsi ini dalam daftar parameter panjang, yang kadang-kadang tidak hanya memasukkan input tetapi juga struktur data output. (IMSL terus berkembang selama bertahun-tahun, dan versi kini tersedia untuk pengembang Java.)

Dalam reka bentuk semasa, Positionadalah struktur data sederhana dan PositionUtilitymerupakan repositori gaya perpustakaan fungsi IMSL yang beroperasi pada Positiondata. Seperti contoh di atas, bahasa berorientasikan objek moden tidak semestinya menghalang penggunaan teknik prosedural kuno.

Pengumpulan data dan kaedah

Kodnya dapat diperbaiki dengan mudah. Sebagai permulaan, mengapa meletakkan data dan fungsi yang beroperasi pada data tersebut dalam modul yang berasingan? Kelas Java membolehkan menggabungkan data dan kaedah bersama:

kelas awam Kedudukan {public double jarak (Posisi kedudukan) {// Hitung dan kembalikan jarak dari objek ini ke kedudukan // yang ditentukan. } tajuk berganda awam (Posisi kedudukan) {// Hitung dan kembalikan tajuk dari objek ini ke kedudukan // yang ditentukan. } latitud berkembar awam; longitud berkembar awam; }

Meletakkan item data kedudukan dan kod pelaksanaan untuk mengira jarak dan menuju ke kelas yang sama menghilangkan keperluan untuk PositionUtilitykelas yang terpisah . Sekarang Positionmula menyerupai kelas berorientasikan objek yang sebenarnya. Kod berikut menggunakan versi baru ini yang menggabungkan data dan kaedah bersama:

Posisikan myHouse = Kedudukan baru (); myHouse.latitude = 36.538611; myHouse.longitude = -121.797500; Kedudukan coffeeShop = Kedudukan baru (); coffeeShop.latitude = 36.539722; coffeeShop.longitude = -121.907222; jarak dua kali ganda = myHouse.distance (kedai kopi); tajuk dua = myHouse.heading (kedai kopi); System.out.println ("Dari rumah saya di (" + myHouse.latitude + "," + myHouse.longitude + ") ke kedai kopi di (" + coffeeShop.latitude + "," + coffeeShop.longitude + ") ialah jarak "+ jarak +" pada tajuk "+ tajuk +" darjah. ");

Keluarannya sama seperti sebelumnya, dan yang lebih penting, kod di atas nampaknya lebih semula jadi. Versi sebelumnya menyampaikan dua Positionobjek ke fungsi dalam kelas utiliti yang terpisah untuk mengira jarak dan arah. Dalam kod tersebut, menghitung tajuk dengan kaedah panggilan util.heading( myHouse, coffeeShop )tidak menunjukkan arah pengiraan dengan jelas. Pembangun mesti ingat bahawa fungsi utiliti mengira tajuk dari parameter pertama ke yang kedua.

In comparison, the above code uses the statement myHouse.heading(coffeeShop) to calculate the same heading. The call's semantics clearly indicate that the direction proceeds from my house to the coffee shop. Converting the two-argument function heading(Position, Position) to a one-argument function position.heading(Position) is known as currying the function. Currying effectively specializes the function on its first argument, resulting in clearer semantics.

Placing the methods utilizing Position class data in the Position class itself makes currying the functions distance and heading possible. Changing the call structure of the functions in this way is a significant advantage over procedural languages. Class Position now represents an abstract data type that encapsulates data and the algorithms that operate on that data. As a user-defined type, Position objects are also first class citizens that enjoy all the benefits of the Java language type system.

The language facility that bundles data with the operations that perform on that data is encapsulation. Note that encapsulation guarantees neither data protection nor information hiding. Nor does encapsulation ensure a cohesive class design. To achieve those quality design attributes requires techniques beyond the encapsulation provided by the language. As currently implemented, class Position doesn't contain superfluous or nonrelated data and methods, but Position does expose both latitude and longitude in raw form. That allows any client of class Position to directly change either internal data item without any intervention by Position. Clearly, encapsulation is not enough.

Defensive programming

To further investigate the ramifications of exposing internal data items, suppose I decide to add a bit of defensive programming to Position by restricting the latitude and longitude to ranges specified by GPS. Latitude falls in the range [-90, 90] and longitude in the range (-180, 180]. The exposure of the data items latitude and longitude in Position's current implementation renders this defensive programming impossible.

Making attributes latitude and longitude private data members of class Position and adding simple accessor and mutator methods, also commonly called getters and setters, provides a simple remedy to exposing raw data items. In the example code below, the setter methods appropriately screen the internal values of latitude and longitude. Rather than throw an exception, I specify performing modulo arithmetic on input values to keep the internal values within specified ranges. For example, attempting to set the latitude to 181.0 results in an internal setting of -179.0 for latitude.

The following code adds getter and setter methods for accessing the private data members latitude and longitude:

public class Position { public Position( double latitude, double longitude ) { setLatitude( latitude ); setLongitude( longitude ); } public void setLatitude( double latitude ) { // Ensure -90 <= latitude <= 90 using modulo arithmetic. // Code not shown. // Then set instance variable. this.latitude = latitude; } public void setLongitude( double longitude ) { // Ensure -180 < longitude <= 180 using modulo arithmetic. // Code not shown. // Then set instance variable. this.longitude = longitude; } public double getLatitude() { return latitude; } public double getLongitude() { return longitude; } public double distance( Position position ) { // Calculate and return the distance from this object to the specified // position. // Code not shown. } public double heading( Position position ) { // Calculate and return the heading from this object to the specified // position. } private double latitude; private double longitude; } 

Using the above version of Position requires only minor changes. As a first change, since the above code specifies a constructor that takes two double arguments, the default constructor is no longer available. The following example uses the new constructor, as well as the new getter methods. The output remains the same as in the first example.

Position myHouse = new Position( 36.538611, -121.797500 ); Position coffeeShop = new Position( 36.539722, -121.907222 ); double distance = myHouse.distance( coffeeShop ); double heading = myHouse.heading( coffeeShop ); System.out.println ( "From my house at (" + myHouse.getLatitude() + ", " + myHouse.getLongitude() + ") to the coffee shop at (" + coffeeShop.getLatitude() + ", " + coffeeShop.getLongitude() + ") is a distance of " + distance + " at a heading of " + heading + " degrees." ); 

Choosing to restrict the acceptable values of latitude and longitude through setter methods is strictly a design decision. Encapsulation does not play a role. That is, encapsulation, as manifested in the Java language, does not guarantee protection of internal data. As a developer, you are free to expose the internals of your class. Nevertheless, you should restrict access and modification of internal data items through the use of getter and setter methods.

Isolating potential change

Protecting internal data is only one of many concerns driving design decisions on top of language encapsulation. Isolation to change is another. Modifying the internal structure of a class should not, if at all possible, affect client classes.

Sebagai contoh, sebelum ini saya perhatikan bahawa pengiraan jarak di kelas Positiontidak menunjukkan unit. Untuk berguna, jarak yang dilaporkan 6.09 dari rumah saya ke kedai kopi jelas memerlukan satuan ukuran. Saya mungkin tahu arah yang harus diambil, tetapi saya tidak tahu mahu berjalan sejauh 6.09 meter, memandu sejauh 6.09 batu, atau terbang sejauh 6.09 ribu kilometer.