Enkapsulasi bukanlah penyembunyian informasi

Kata-kata itu licin. Seperti Humpty Dumpty yang dinyatakan dalam Lewis Carroll's Through the Looking Glass, "Ketika saya menggunakan sebuah kata, itu berarti apa yang saya pilih - tidak lebih dan tidak kurang." Tentunya penggunaan umum dari kata-kata enkapsulasi dan penyembunyian informasi tampaknya mengikuti logika itu. Penulis jarang membedakan keduanya dan sering secara langsung menyatakan bahwa keduanya sama.

Apakah itu membuatnya begitu? Bukan untuk saya. Seandainya itu hanya masalah kata-kata, saya tidak akan menulis sepatah kata pun tentang masalah itu. Tetapi ada dua konsep berbeda di balik istilah-istilah ini, konsep-konsep yang muncul secara terpisah dan paling baik dipahami secara terpisah.

Enkapsulasi mengacu pada bundling data dengan metode yang beroperasi pada data tersebut. Seringkali definisi tersebut disalahartikan sebagai data tersembunyi. Di Java, Anda dapat memiliki data yang dienkapsulasi yang tidak tersembunyi sama sekali.

Namun, menyembunyikan data tidak sepenuhnya menyembunyikan informasi. David Parnas pertama kali memperkenalkan konsep penyembunyian informasi sekitar tahun 1972. Dia berpendapat bahwa kriteria utama untuk modularisasi sistem harus memperhatikan penyembunyian keputusan desain yang kritis. Dia menekankan menyembunyikan "keputusan desain yang sulit atau keputusan desain yang cenderung berubah." Menyembunyikan informasi dengan cara itu mengisolasi klien dari kebutuhan pengetahuan mendalam tentang desain untuk menggunakan modul, dan dari efek mengubah keputusan tersebut.

Pada artikel ini, saya mengeksplorasi perbedaan antara enkapsulasi dan informasi yang bersembunyi melalui pengembangan kode contoh. Diskusi menunjukkan bagaimana Java memfasilitasi enkapsulasi dan menyelidiki konsekuensi negatif enkapsulasi tanpa menyembunyikan data. Contoh tersebut juga menunjukkan bagaimana meningkatkan desain kelas melalui prinsip menyembunyikan informasi.

Kelas posisi

Dengan meningkatnya kesadaran akan potensi besar Internet nirkabel, banyak pakar mengharapkan layanan berbasis lokasi memberikan peluang bagi aplikasi pembunuh nirkabel pertama. Untuk kode contoh artikel ini, saya telah memilih kelas yang mewakili lokasi geografis suatu titik di permukaan bumi. Sebagai entitas domain, kelas, bernama Position, mewakili informasi Global Position System (GPS). Potongan pertama di kelas terlihat sesederhana:

public class Position {public double latitude; bujur ganda publik; }

Kelas berisi dua item data: GPS latitudedan longitude. Saat ini, Positiontidak lebih dari sekantong kecil data. Meskipun demikian, Positionadalah kelas, dan Positionobjek dapat dibuat instance-nya menggunakan kelas tersebut. Untuk memanfaatkan objek tersebut, kelas PositionUtilityberisi metode untuk menghitung jarak dan heading - yaitu, arah - antara Positionobjek yang ditentukan :

public class PositionUtility {public static double distance (Position position1, Position position2) {// Menghitung dan mengembalikan jarak antara posisi yang ditentukan. } heading ganda public static (Posisi position1, Position position2) {// Hitung dan kembalikan heading dari position1 ke position2. }}

Saya menghilangkan kode implementasi sebenarnya untuk jarak dan perhitungan heading.

Kode berikut mewakili penggunaan umum Positiondan PositionUtility:

// Buat Posisi yang mewakili rumah saya Position myHouse = new Position (); myHouse.latitude = 36.538611; myHouse.longitude = -121.797500; // Buat Posisi yang mewakili kedai kopi lokal. Position coffeeShop = new Position (); coffeeShop.latitude = 36.539722; coffeeShop.longitude = -121.907222; // Gunakan PositionUtility untuk menghitung jarak dan tujuan dari rumah saya // ke kedai kopi lokal. jarak ganda = PositionUtility.distance (myHouse, coffeeShop); judul ganda = PositionUtility.heading (myHouse, coffeeShop); // Hasil cetak System.out.println ("Dari rumah saya di (" + myHouse.latitude + "," + myHouse.longitude + ") ke kedai kopi di (" + coffeeShop.latitude + "," + coffeeShop. longitude + ") adalah jarak" + distance + "pada heading" + heading + "derajat. ");

Kode menghasilkan keluaran di bawah ini, yang menunjukkan bahwa kedai kopi itu berada di sebelah barat (270,8 derajat) dari rumah saya pada jarak 6,09. Diskusi selanjutnya membahas kurangnya unit jarak.

================================================== ================= Dari rumah saya di (36.538611, -121.7975) ke kedai kopi di (36.539722, -121.907222) jarak 6.0873776351893385 di arah 270.7547022304523 derajat. ================================================== =================

Position,, PositionUtilitydan penggunaan kodenya agak mengganggu dan tentunya tidak terlalu berorientasi objek. Tapi bagaimana itu bisa terjadi? Java adalah bahasa berorientasi objek, dan kodenya menggunakan objek!

Meskipun kode dapat menggunakan objek Java, ia melakukannya dengan cara yang mengingatkan pada era yang telah berlalu: fungsi utilitas yang beroperasi pada struktur data. Selamat datang di tahun 1972! Ketika Presiden Nixon berkerumun di atas rekaman pita rahasia, profesional komputer yang mengkode dalam bahasa prosedural Fortran dengan bersemangat menggunakan Perpustakaan Matematika dan Statistik Internasional (IMSL) baru dengan cara ini. Repositori kode seperti IMSL penuh dengan fungsi untuk perhitungan numerik. Pengguna meneruskan data ke fungsi-fungsi ini dalam daftar parameter yang panjang, yang terkadang tidak hanya menyertakan input tetapi juga struktur data output. (IMSL terus berkembang selama bertahun-tahun, dan sebuah versinya sekarang tersedia untuk pengembang Java.)

Dalam desain saat ini, Positionadalah struktur data sederhana dan PositionUtilitymerupakan repositori gaya IMSL dari fungsi perpustakaan yang beroperasi pada Positiondata. Seperti yang ditunjukkan contoh di atas, bahasa berorientasi objek modern tidak serta merta menghalangi penggunaan teknik prosedural kuno.

Membundel data dan metode

Kode dapat dengan mudah ditingkatkan. Sebagai permulaan, mengapa menempatkan data dan fungsi yang beroperasi pada data tersebut dalam modul terpisah? Kelas Java memungkinkan penggabungan data dan metode bersama:

public class Position {public double distance (Position position) {// Hitung dan kembalikan jarak dari objek ini ke // position yang ditentukan. } public double heading (Position position) {// Hitung dan kembalikan heading dari objek ini ke // position yang ditentukan. } lintang ganda publik; bujur ganda publik; }

Menempatkan item data posisi dan kode implementasi untuk menghitung jarak dan arah di kelas yang sama meniadakan kebutuhan akan PositionUtilitykelas yang terpisah . Sekarang Positionmulai menyerupai kelas berorientasi objek yang sebenarnya. Kode berikut menggunakan versi baru ini yang menggabungkan data dan metode menjadi satu:

Posisi myHouse = Posisi baru (); myHouse.latitude = 36.538611; myHouse.longitude = -121.797500; Position coffeeShop = new Position (); coffeeShop.latitude = 36.539722; coffeeShop.longitude = -121.907222; jarak ganda = myHouse.distance (coffeeShop); judul ganda = myHouse.heading (coffeeShop); System.out.println ("Dari rumah saya di (" + myHouse.latitude + "," + myHouse.longitude + ") ke kedai kopi di (" + coffeeShop.latitude + "," + coffeeShop.longitude + ") adalah jarak "+ jarak +" pada heading "+ heading +" derajat. ");

Outputnya identik seperti sebelumnya, dan yang lebih penting, kode di atas tampak lebih natural. Versi sebelumnya meneruskan dua Positionobjek ke suatu fungsi di kelas utilitas terpisah untuk menghitung jarak dan arah. Dalam kode tersebut, menghitung heading dengan pemanggilan metode util.heading( myHouse, coffeeShop )tidak secara jelas menunjukkan arah penghitungan. Pengembang harus ingat bahwa fungsi utilitas menghitung tajuk dari parameter pertama ke parameter kedua.

Sebagai perbandingan, kode di atas menggunakan pernyataan myHouse.heading(coffeeShop)untuk menghitung heading yang sama. Semantik panggilan dengan jelas menunjukkan bahwa arah dari rumah saya ke kedai kopi. Mengonversi fungsi dua argumen menjadi fungsi heading(Position, Position)satu argumen position.heading(Position)dikenal sebagai fungsi kari . Currying secara efektif mengkhususkan fungsi pada argumen pertamanya, menghasilkan semantik yang lebih jelas.

Menempatkan metode yang memanfaatkan Positiondata kelas di Positionkelas itu sendiri membuat fungsi-fungsi distancedan menjadi headingmungkin. Mengubah struktur panggilan fungsi dengan cara ini merupakan keuntungan signifikan atas bahasa prosedural. Kelas Positionsekarang mewakili tipe data abstrak yang merangkum data dan algoritma yang beroperasi pada data itu. Sebagai tipe yang ditentukan pengguna, Positionobjek juga merupakan warga kelas satu yang menikmati semua manfaat dari sistem tipe bahasa Java.

Fasilitas bahasa yang menggabungkan data dengan operasi yang dilakukan pada data tersebut adalah enkapsulasi. Perhatikan bahwa enkapsulasi tidak menjamin perlindungan data maupun penyembunyian informasi. Enkapsulasi juga tidak menjamin desain kelas yang kohesif. Untuk mencapai atribut desain kualitas tersebut membutuhkan teknik di luar enkapsulasi yang disediakan oleh bahasa. Seperti yang saat ini diterapkan, kelas Positiontidak berisi data dan metode yang berlebihan atau tidak terkait, tetapi Positionmengekspos keduanya latitudedan longitudedalam bentuk mentah. Itu memungkinkan setiap klien kelas Positionuntuk secara langsung mengubah salah satu item data internal tanpa intervensi apa pun Position. Jelas, enkapsulasi saja tidak cukup.

Pemrograman defensif

Untuk menyelidiki lebih lanjut konsekuensi dari mengekspos item data internal, misalkan saya memutuskan untuk menambahkan sedikit pemrograman defensif Positiondengan membatasi lintang dan bujur ke rentang yang ditentukan oleh GPS. Lintang jatuh dalam kisaran [-90, 90] dan bujur di kisaran (-180, 180]. Pemaparan dari item data latitudedan longitudedi Positionimplementasi 's saat merender pemrograman defensif ini tidak mungkin.

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.

Misalnya, saya sebelumnya mencatat bahwa penghitungan jarak di kelas Positiontidak menunjukkan satuan. Agar bermanfaat, jarak yang dilaporkan 6,09 dari rumah saya ke kedai kopi jelas membutuhkan satuan ukuran. Saya mungkin tahu arah yang harus diambil, tetapi saya tidak tahu apakah harus berjalan 6,09 meter, berkendara 6,09 mil, atau terbang 6,09 ribu kilometer.