Merancang bidang dan metode

Angsuran Teknik Desain bulan ini adalah yang kedua dalam seri mini kolom tentang mendesain objek. Di kolom bulan lalu, yang membahas mendesain objek untuk inisialisasi yang tepat, saya berbicara tentang cara mendesain konstruktor dan penginisialisasi. Bulan ini dan bulan depan saya akan membahas prinsip desain untuk bidang aktual dan metode kelas. Setelah itu, saya akan menulis tentang finalizer dan menunjukkan cara mendesain objek untuk pembersihan yang tepat di akhir hidup mereka.

Materi artikel ini (menghindari nilai data khusus, menggunakan konstanta, meminimalkan kopling) dan artikel selanjutnya (memaksimalkan kohesi) mungkin sudah tidak asing lagi bagi banyak pembaca, karena materi tersebut didasarkan pada prinsip desain umum yang cukup independen dari bahasa pemrograman Java . Namun demikian, karena saya telah menemukan begitu banyak kode selama bertahun-tahun yang tidak memanfaatkan prinsip-prinsip ini, saya pikir kode tersebut pantas untuk diulang dari waktu ke waktu. Selain itu, dalam artikel ini saya mencoba untuk menunjukkan bagaimana prinsip-prinsip umum ini diterapkan pada bahasa Java secara khusus.

Merancang bidang

Dalam mendesain bidang, prinsip utamanya adalah menghindari penggunaan satu variabel untuk mewakili beberapa atribut kelas. Anda dapat melanggar aturan ini dengan menunjukkan nilai khusus dalam variabel, masing-masing dengan arti khususnya sendiri.

Seperti yang digunakan di sini, atribut adalah karakteristik pembeda dari suatu objek atau kelas. Dua atribut suatu CoffeeCupobjek, misalnya, dapat berupa:

  • Jumlah kopi yang dikandung cangkir
  • Apakah cangkir itu bersih atau kotor

Untuk melihat lebih dekat aturan ini, bayangkan Anda sedang mendesain CoffeeCupkelas untuk kafe virtual yang dijelaskan di kolom Teknik Desain bulan lalu . Asumsikan Anda ingin membuat model apakah cangkir kopi di kafe virtual Anda telah dicuci dan siap digunakan oleh pelanggan berikutnya. Dengan informasi ini, Anda dapat memastikan bahwa Anda tidak menggunakan kembali cangkir kopi sebelum dicuci.

Jika Anda memutuskan Anda hanya peduli apakah cangkir telah dicuci atau tidak jika kosong, Anda dapat menggunakan nilai khusus dari innerCoffeebidang tersebut, yang biasanya digunakan untuk mencatat jumlah kopi dalam cangkir, untuk mewakili cangkir yang tidak dicuci . Jika 473 mililiter (16 ons cairan) adalah jumlah maksimum kopi di cangkir terbesar Anda, nilai maksimum innerCoffeebiasanya adalah 473. Jadi, Anda dapat menggunakan innerCoffeenilai, katakanlah, 500 (nilai khusus) untuk menunjukkan kosong cangkir yang belum dicuci:

// Dalam paket sumber di bidang file / ex1 / CoffeeCup.java kelas CoffeeCup {private int innerCoffee; public boolean isReadyForNextUse () {// Jika cangkir kopi belum dicuci, maka // tidak siap untuk digunakan selanjutnya if (innerCoffee == 500) {return false; } kembali benar; } public void setCustomerDone () {innerCoffee = 500; // ...} pencucian ruang kosong publik () {innerCoffee = 0; // ...} // ...}

Kode ini akan memberi CoffeeCupobjek perilaku yang diinginkan. Masalah dengan pendekatan ini adalah bahwa nilai khusus tidak langsung dipahami, dan membuat kode lebih sulit untuk diubah. Meskipun Anda mendeskripsikan nilai-nilai khusus dalam komentar, pemrogram lain mungkin membutuhkan waktu lebih lama untuk memahami apa yang dilakukan kode Anda. Selain itu, mereka mungkin tidak pernah memahami kode Anda. Mereka mungkin menggunakan kelas Anda secara tidak benar atau mengubahnya sehingga mereka memperkenalkan bug.

Misalnya, jika nanti seseorang menambahkan cangkir 20 ons ke persembahan kafe virtual, maka dimungkinkan untuk menampung hingga 592 mililiter (ml) kopi dalam cangkir. Jika seorang programmer menambahkan ukuran cangkir baru tanpa menyadari bahwa Anda menggunakan 500 ml untuk menunjukkan bahwa cangkir perlu dicuci, kemungkinan besar bug akan muncul. Jika seorang pelanggan di kafe virtual Anda membeli cangkir 20 ons, kemudian meneguk 92 ml, dia akan memiliki tepat 500 ml tersisa di cangkir. Pelanggan akan terkejut dan tidak puas ketika, setelah minum hanya 92 ml, cangkir tersebut menghilang dari tangannya dan muncul di wastafel, siap untuk dicuci. Dan, bahkan jika pemrogram yang membuat perubahan menyadari bahwa Anda menggunakan nilai khusus, nilai khusus lain untuk atribut tidak dicuci harus dipilih.

Pendekatan yang lebih baik untuk situasi ini adalah memiliki bidang terpisah untuk memodelkan atribut terpisah:

// Dalam paket sumber di bidang file / ex2 / CoffeeCup.java kelas CoffeeCup {private int innerCoffee; kebutuhan boolean pribadi public boolean isReadyForNextUse () {// Jika cangkir kopi belum dicuci, maka // belum siap untuk penggunaan selanjutnya return! needsWashing; } public void setCustomerDone () {needsWashing = true; // ...} public void wash () {needsWashing = false; // ...} // ...}

Di sini innerCoffeebidang hanya digunakan untuk memodelkan jumlah kopi di atribut cangkir. Atribut cup-need-washing dimodelkan oleh needsWashinglapangan. Skema ini lebih mudah dipahami daripada skema sebelumnya, yang menggunakan nilai khusus innerCoffeedan tidak akan menghalangi seseorang untuk memperluas nilai maksimumnya innerCoffee.

Menggunakan konstanta

Aturan praktis lain yang harus diikuti saat membuat bidang adalah menggunakan konstanta (variabel akhir statis) untuk nilai konstan yang diteruskan ke, dikembalikan dari, atau digunakan dalam metode. Jika suatu metode mengharapkan salah satu dari sekumpulan nilai konstan yang terbatas di salah satu parameternya, mendefinisikan konstanta membantu membuatnya lebih jelas bagi programmer klien apa yang perlu diteruskan dalam parameter itu. Demikian juga, jika sebuah metode mengembalikan salah satu dari sekumpulan nilai yang terbatas, mendeklarasikan konstanta membuatnya lebih jelas bagi pemrogram klien apa yang diharapkan sebagai keluaran. Misalnya, lebih mudah untuk memahami ini:

if (cup.getSize () == CoffeeCup.TALL) {} 

daripada memahami ini:

jika (cup.getSize () == 1) {} 

Anda juga harus mendefinisikan konstanta untuk penggunaan internal dengan metode kelas - meskipun konstanta tersebut tidak digunakan di luar kelas - sehingga lebih mudah dipahami dan diubah. Menggunakan konstanta membuat kode lebih fleksibel. Jika Anda menyadari bahwa Anda salah menghitung nilai dan Anda tidak menggunakan konstanta, Anda harus memeriksa kode Anda dan mengubah setiap kemunculan nilai hard-code. Namun, jika Anda menggunakan konstanta, Anda hanya perlu mengubahnya di tempat yang didefinisikan sebagai konstanta.

Konstanta dan compiler Java

Hal berguna yang perlu diketahui tentang compiler Java adalah ia memperlakukan bidang final statis (konstanta) secara berbeda dari jenis bidang lainnya. Referensi ke variabel final statis yang diinisialisasi ke konstanta waktu kompilasi diselesaikan pada waktu kompilasi ke salinan lokal dari nilai konstan. Hal ini berlaku untuk konstanta semua tipe dan tipe primitif java.lang.String.

Biasanya, ketika kelas Anda merujuk ke kelas lain - katakanlah, kelas java.lang.Math- kompilator Java menempatkan referensi simbolik ke kelas Mathke dalam file kelas untuk kelas Anda. Misalnya, jika metode kelas Anda dipanggil Math.sin(), file kelas Anda akan berisi dua referensi simbolis ke Math:

  • Satu referensi simbolis ke kelas Math
  • Salah satu referensi simbolis untuk Math's sin()metode

Untuk menjalankan kode yang terdapat di kelas Anda yang merujuk Math.sin(), JVM perlu memuat kelas Mathuntuk menyelesaikan referensi simbolik.

Sebaliknya, jika kode Anda hanya merujuk ke variabel kelas final statis yang PIdideklarasikan di kelas Math, compiler Java tidak akan menempatkan referensi simbolis apa pun ke Mathdalam file kelas untuk kelas Anda. Sebagai gantinya, ini hanya akan menempatkan salinan nilai literal Math.PIke dalam file kelas kelas Anda. Untuk menjalankan kode yang terdapat di kelas Anda yang menggunakan Math.PIkonstanta, JVM tidak perlu memuat kelas Math.

Hasil dari fitur compiler Java ini adalah bahwa JVM tidak harus bekerja lebih keras untuk menggunakan konstanta daripada menggunakan literal. Lebih memilih konstanta daripada literal adalah salah satu dari sedikit pedoman desain yang meningkatkan fleksibilitas program tanpa mengambil risiko penurunan kinerja program.

Tiga macam metode

Sisa artikel ini akan membahas teknik desain metode yang berkaitan dengan data yang digunakan atau dimodifikasi metode. Dalam konteks ini, saya ingin mengidentifikasi dan memberi nama tiga tipe dasar metode dalam program Java: metode utilitas metode tampilan status , dan metode perubahan status .

Metode utilitas

A utility method is a class method that doesn't use or modify the state (class variables) of its class. This kind of method simply provides a useful service related to its class of object.

Some examples of utility methods from the Java API are:

  • (In class Integer) public static int toString(int i) -- returns a new String object representing the specified integer in radix 10
  • (In class Math) public static native double cos(double a) -- returns the trigonometric cosine of an angle

The state-view method

A state-view method is a class or instance method that returns some view of the internal state of the class or object, without changing that state. (This kind of method brazenly disregards the Heisenberg Uncertainty Principle -- see Resources if you need a refresher on this principle.) A state-view method may simply return the value of a class or instance variable, or it may return a value calculated from several class or instance variables.

Some examples of state-view methods from the Java API are:

  • (In class Object) public String toString() -- returns a string representation of the object
  • (In class Integer) public byte byteValue() -- returns the value of the Integer object as a byte
  • (In class String) public int indexOf(int ch) -- returns the index within the string of the first occurrence of the specified character

The state-change method

The state-change method is a method that may transform the state of the class in which the method is declared, or, if an instance method, the object upon which it is invoked. When a state-change method is invoked, it represents an "event" to a class or object. The code of the method "handles" the event, potentially changing the state of the class or object.

Some examples of state-change methods from the Java API are:

  • (In class StringBuffer) public StringBuffer append(int i) -- appends the string representation of the int argument to the StringBuffer
  • (In class Hashtable) public synchronized void clear() -- clears the Hashtable so that it contains no keys
  • (In class Vector) public final synchronized void addElement(Object obj) -- adds the specified component to the end of the Vector, increasing its size by one

Minimizing method coupling

Armed with these definitions of utility, state-view, and state-change methods, you are ready for the discussion of method coupling.

As you design methods, one of your goals should be to minimize coupling -- the degree of interdependence between a method and its environment (other methods, objects, and classes). The less coupling there is between a method and its environment, the more independent that method is, and the more flexible the design is.

Methods as data transformers

To understand coupling, it helps to think of methods purely as transformers of data. Methods accept data as input, perform operations on that data, and generate data as output. A method's degree of coupling is determined primarily by where it gets its input data and where it puts its output data.

Figure 1 shows a graphical depiction of the method as data transformer: A data flow diagram from structured (not object-oriented) design.

Input and output

A method in Java can get input data from many sources:

  • It can require that the caller specify its input data as parameters when it is invoked
  • It can grab data from any accessible class variables, such as the class's own class variables or any accessible class variables of another class
  • If it is an instance method, it can grab instance variables from the object upon which it was invoked

Likewise, a method can express its output in many places:

  • It can return a value, either a primitive type or an object reference
  • It can alter objects referred to by references passed in as parameters
  • It can alter any class variables of its own class or any accessible class variables of another class
  • If it is an instance method, it can alter any instance variables of the object upon which it was invoked
  • It can throw an exception

Note that parameters, return values, and thrown exceptions are not the only kinds of method inputs and outputs mentioned in the above lists. Instance and class variables also are treated as input and output. This may seem non-intuitive from an object-oriented perspective, because access to instance and class variables in Java is "automatic" (you don't have to pass anything explicitly to the method). When attempting to gauge a method's coupling, however, you must look at the kind and amount of data used and modified by the code, regardless of whether or not the code's access to that data was "automatic."

Minimally coupled utility methods

Metode paling tidak berpasangan yang dimungkinkan di Java adalah metode utilitas yang:

  1. Mengambil masukan hanya dari parameternya
  2. Mengekspresikan outputnya hanya melalui parameternya atau nilai kembaliannya (atau dengan membuat pengecualian)
  3. Menerima sebagai input hanya data yang benar-benar dibutuhkan oleh metode
  4. Mengembalikan sebagai keluaran hanya data yang sebenarnya diproduksi oleh metode

Metode utilitas yang baik

Misalnya, metode yang convertOzToMl()ditunjukkan di bawah ini menerima an intsebagai satu-satunya masukan dan mengembalikan an intsebagai satu-satunya keluaran: