Java Tip 107: Maksimalkan penggunaan kembali kode Anda

Penggunaan kembali itu adalah mitos yang tampaknya menjadi sentimen yang semakin umum di kalangan programmer. Mungkin, bagaimanapun, penggunaan kembali sulit untuk dicapai karena kekurangan ada dalam pendekatan pemrograman berorientasi objek tradisional untuk digunakan kembali. Tip ini menjelaskan tiga langkah yang membentuk pendekatan berbeda untuk memungkinkan penggunaan kembali.

Langkah 1: Pindahkan fungsionalitas dari metode instance kelas

Pewarisan kelas adalah mekanisme suboptimal untuk penggunaan kembali kode karena kurangnya presisi. Yaitu, Anda tidak dapat menggunakan kembali satu metode kelas tanpa mewarisi metode lain kelas tersebut serta anggota datanya. Bagasi berlebih itu tidak perlu mempersulit kode yang ingin menggunakan kembali metode tersebut. Ketergantungan kelas yang mewarisi pada induknya menimbulkan kompleksitas tambahan: perubahan yang dibuat pada kelas induk dapat merusak subkelas; saat memodifikasi salah satu kelas, akan sulit untuk mengingat metode mana yang diganti atau tidak; dan, tidak jelas apakah metode yang diganti harus memanggil metode induk terkait atau tidak.

Setiap metode yang melakukan satu tugas konseptual harus mampu berdiri sendiri sebagai kandidat kelas satu untuk digunakan kembali. Untuk mencapai itu, kita harus kembali ke pemrograman prosedural dengan memindahkan kode dari metode instance kelas dan ke prosedur yang terlihat secara global. Untuk mempromosikan penggunaan kembali prosedur tersebut, Anda harus mengkodekannya seperti metode utilitas statis: setiap prosedur harus menggunakan hanya parameter input dan / atau panggilan ke prosedur yang terlihat secara global untuk melakukan tugasnya, dan tidak boleh menggunakan variabel nonlokal. Pengurangan ketergantungan eksternal mengurangi kompleksitas penggunaan prosedur, sehingga meningkatkan motivasi untuk menggunakannya kembali di tempat lain. Tentu saja, bahkan kode yang tidak dimaksudkan untuk digunakan kembali mendapatkan keuntungan dari organisasi itu, karena strukturnya selalu menjadi jauh lebih bersih.

Di Java, metode tidak bisa berdiri sendiri di luar kelas. Sebagai gantinya, Anda dapat mengambil prosedur terkait dan membuatnya terlihat secara publik sebagai metode statis dari satu kelas. Sebagai contoh, Anda dapat mengambil kelas yang terlihat seperti ini:

class Polygon {. . public int getPerimeter () {...} public boolean isConvex () {...} public boolean berisiPoint (Titik p) {...}. . }

dan mengubahnya menjadi seperti ini:

class Polygon {. . public int getPerimeter () {return pPolygon.computePerimeter (this);} public boolean isConvex () {return pPolygon.isConvex (this);} public boolean containsPoint (Point p) {return pPolygon.containsPoint (this, p);}. . }

Ini, pPolygonapakah ini:

kelas pPolygon {static public int computePerimeter (Poligon poligon) {...} boolean publik statis isConvex (Poligon poligon) {...} boolean publik statis containsPoint (Poligon poligon, Titik p) {...}} 

Nama kelas pPolygonmerefleksikan bahwa prosedur yang diapit oleh kelas tersebut paling berkaitan dengan objek bertipe Polygon. Di pdepan nama menunjukkan bahwa satu-satunya tujuan kelas adalah untuk mengelompokkan prosedur statis yang terlihat secara publik. Meskipun tidak standar di Java untuk memiliki nama kelas yang diawali dengan huruf kecil, kelas seperti pPolygonitu tidak menjalankan fungsi kelas normal. Artinya, itu tidak mewakili kelas objek; ini lebih merupakan entitas organisasi yang dibutuhkan oleh bahasa.

Efek keseluruhan dari perubahan yang dibuat pada contoh di atas adalah bahwa kode klien tidak lagi harus mewarisi Polygonuntuk menggunakan kembali fungsinya. Fungsionalitas tersebut sekarang tersedia di pPolygonkelas secara prosedur demi prosedur. Kode klien hanya menggunakan fungsionalitas yang dibutuhkannya, tanpa harus memikirkan fungsionalitas yang tidak diperlukannya.

Itu tidak dimaksudkan untuk menyiratkan bahwa kelas tidak memiliki tujuan yang berguna dalam gaya pemrograman neoprosedural tersebut. Sebaliknya, kelas melakukan tugas yang diperlukan untuk mengelompokkan dan merangkum anggota data dari objek yang mereka wakili. Selain itu, kemampuan mereka untuk menjadi polimorfik dengan mengimplementasikan banyak antarmuka adalah enabler penggunaan kembali yang unggul, seperti yang dijelaskan di langkah berikutnya. Namun, Anda harus menurunkan penggunaan kembali dan polimorfisme melalui pewarisan kelas ke status yang kurang disukai di gudang teknik Anda, karena menjaga fungsionalitas yang terjerat dalam metode instance kurang dari optimal untuk mencapai penggunaan kembali.

Sebuah variasi kecil dari teknik tersebut secara singkat disebutkan dalam buku Design Patterns yang banyak dibaca oleh Gang of Four . Pola Strategi mereka menganjurkan merangkum setiap anggota keluarga dari algoritme terkait di balik antarmuka umum sehingga kode klien dapat menggunakan algoritme tersebut secara bergantian. Karena algoritme biasanya dikodekan sebagai salah satu atau beberapa prosedur terisolasi, enkapsulasi tersebut menekankan penggunaan kembali prosedur yang melakukan tugas tunggal (yaitu, algoritme), penggunaan kembali objek yang berisi kode dan data, yang dapat melakukan banyak tugas. Langkah itu mempromosikan ide dasar yang sama.

Namun, mengenkapsulasi algoritma di belakang antarmuka menyiratkan pengkodean algoritma sebagai objek yang mengimplementasikan antarmuka itu. Itu berarti kita masih terikat pada prosedur yang digabungkan dengan data dan metode lain dari objek yang melingkupinya, sehingga memperumit penggunaannya kembali. Ada juga masalah harus membuat instance objek tersebut setiap kali algoritme perlu digunakan, yang dapat memperlambat kinerja program. Untungnya, Design Patterns menawarkan solusi yang mengatasi kedua masalah tersebut. Anda bisa menggunakan Kelas Terbangpola saat mengkodekan objek Strategi sehingga hanya ada satu contoh yang diketahui dan dibagikan dari masing-masing (yang mengatasi masalah kinerja), dan sehingga setiap objek bersama tidak mempertahankan status antara akses (sehingga objek tidak akan memiliki data anggota, yang membahas banyak masalah kopling). Pola Flyweight-Strategy yang dihasilkan sangat mirip dengan teknik langkah ini dalam merangkum fungsionalitas dalam prosedur tanpa kewarganegaraan yang tersedia secara global.

Langkah 2: Ubah jenis parameter masukan nonprimitif menjadi jenis antarmuka

Mengambil keuntungan dari polimorfisme melalui tipe parameter antarmuka, daripada melalui pewarisan kelas, adalah dasar sebenarnya dari penggunaan kembali dalam pemrograman berorientasi objek, seperti yang dinyatakan oleh Allen Holub dalam "Membangun Antarmuka Pengguna untuk Sistem Berorientasi Objek, Bagian 2".

"... Anda bisa digunakan kembali dengan memprogram ke antarmuka daripada ke kelas. Jika semua argumen ke metode adalah referensi ke beberapa antarmuka yang dikenal, diimplementasikan oleh kelas yang belum pernah Anda dengar, maka metode itu dapat beroperasi pada objek yang kelasnya tidak Bahkan tidak ada saat kode ditulis. Secara teknis, ini adalah metode yang dapat digunakan kembali, bukan objek yang diteruskan ke metode. "

Menerapkan pernyataan Holub ke hasil Langkah 1, setelah satu blok fungsionalitas dapat berdiri sendiri sebagai prosedur yang terlihat secara global, Anda selanjutnya dapat meningkatkan potensi penggunaan kembali dengan mengubah setiap parameter input tipe kelasnya menjadi tipe antarmuka. Kemudian, objek dari setiap kelas yang mengimplementasikan tipe antarmuka dapat digunakan untuk memenuhi parameter, bukan hanya kelas asli. Dengan demikian, prosedur menjadi dapat digunakan dengan kumpulan tipe objek yang berpotensi lebih besar.

Misalnya, Anda memiliki metode statis yang terlihat secara global:

boolean publik statis berisi (Persegi panjang persegi, int x, int y) {...} 

Metode itu dimaksudkan untuk menjawab apakah persegi panjang yang diberikan berisi lokasi tertentu. Di sini Anda akan mengubah tipe rectparameter dari tipe kelas Rectanglemenjadi tipe antarmuka, yang ditunjukkan di sini:

boolean publik statis berisi (Persegi panjang persegi, int x, int y) {...} 

Rectangular bisa jadi antarmuka berikut:

antarmuka publik Rectangular {Rectangle getBounds (); }

Sekarang, objek dari kelas yang dapat dideskripsikan sebagai persegi panjang (artinya dapat mengimplementasikan Rectangularantarmuka) dapat diberikan sebagai rectparameter ke pRectangular.contains(). Kami telah membuat metode itu lebih dapat digunakan kembali dengan melonggarkan batasan tentang apa yang mungkin diteruskan kepadanya.

Untuk contoh di atas, bagaimanapun, Anda mungkin bertanya-tanya apakah ada manfaat nyata menggunakan Rectangularantarmuka saat getBoundsmetode mengembalikan Rectangle; Artinya, jika kita tahu objek yang ingin kita kirimkan dapat menghasilkan seperti Rectangleketika ditanya, mengapa tidak meneruskan saja Rectanglealih - alih tipe antarmuka? Alasan paling penting untuk tidak melakukannya berkaitan dengan koleksi. Katakanlah Anda memiliki metode:

areAnyOverlapping boolean publik statis (Koleksi rects) {...} 

that is meant to answer whether any of the rectangular objects in the given collection are overlapping. Then, in the body of that method, as you iterate through each object in the collection, how do you access that object's rectangle if you can't cast the object to an interface type such as Rectangular? The only option would be to cast the object to its specific class type (which we know has a method that can provide the rectangle), meaning the method would have to know ahead of time on which class types it will operate, limiting its reuse to those types. That's just what that step tries to avoid in the first place!

Step 3: Choose less-coupling input parameter interface types

When performing Step 2, which interface type should be chosen to replace a given class type? The answer is whichever interface fully represents what the procedure needs from that parameter with the least amount of excess baggage. The smaller the interface the parameter object has to implement, the better the chances for any particular class to be able to implement that interface -- and therefore, the larger the number of classes whose objects can be used as that parameter. It is easy to see that if you have a method such as:

static public boolean areOverlapping(Window window1, Window window2) {...} 

which is meant to answer whether two (assumed to be rectangular) windows overlap, and if that method only requires from its two parameters their rectangular coordinates, then it would be better to reduce the types of the parameters to reflect that fact:

static public boolean areOverlapping(Rectangular rect1, Rectangular rect2) {...} 

The above code assumes that the objects of the previous Window type can also implement Rectangular. Now you can reuse the functionality contained in the first method for all rectangular objects.

You may experience times when the available interfaces that sufficiently specify what is needed from a parameter have too many unnecessary methods. In that case, you should define a new interface publicly in the global namespace for reuse by other methods that might face the same dilemma.

You may also find times when it is best to create a unique interface to specify what is needed from just one parameter to a single procedure. You would use that interface for that parameter only. That usually occurs in situations where you want to treat the parameter as if it's a function pointer in C. For example, if you have a procedure:

static public void sort(List list, SortComparison comp) {...} 

that sorts the given list by comparing all of its objects, using the provided comparison object comp, then all sort wants from comp is to call a single method on it that makes the comparison. SortComparison should therefore be an interface with just one method:

public interface SortComparison { boolean comesBefore(Object a, Object b); } 

The only purpose of that interface is to provide sort with a hook to the functionality it needs to do its job, so SortComparison should not be reused elsewhere.

Conclusion

Those three steps are meant to be performed on existing code that was written using more traditional object-oriented methodologies. Together, those steps combined with OO programming can constitute a new methodology that you can employ when writing future code, one that increases the reusability and cohesion of methods while reducing their coupling and complexity.

Obviously, you should not perform those steps on code that is inherently ill-suited for reuse. Such code is usually found in a program's presentation layer. The code that creates a program's user interface and the control code that ties input events to the procedures that do the actual work are both examples of functionality that change so much from program to program that their reuse becomes infeasible.

Jeff Mather bekerja untuk Tucson, yang berbasis di Ariz. EBlox.com, di mana dia membuat applet untuk perusahaan-perusahaan di bidang materi promosi dan industri bioteknologi. Dia juga menulis game shareware di waktu luangnya.