Tip Java 75: Gunakan kelas bersarang untuk organisasi yang lebih baik

Sebuah subsistem khas dalam aplikasi Java terdiri dari sekumpulan kelas dan antarmuka yang berkolaborasi, masing-masing melakukan peran tertentu. Beberapa dari kelas dan antarmuka ini hanya bermakna dalam konteks kelas atau antarmuka lain.

Mendesain kelas yang bergantung pada konteks sebagai kelas bertingkat tingkat atas (kelas bertingkat, singkatnya) yang diapit oleh kelas penyajian konteks membuat ketergantungan ini lebih jelas. Selain itu, penggunaan kelas bersarang membuat kolaborasi lebih mudah dikenali, menghindari polusi namespace, dan mengurangi jumlah file sumber.

(Kode sumber lengkap untuk tip ini dapat diunduh dalam format zip dari bagian Resources.)

Kelas bersarang vs. kelas dalam

Kelas bersarang hanyalah kelas dalam statis. Perbedaan antara kelas bersarang dan kelas dalam adalah sama dengan perbedaan antara anggota kelas statis dan nonstatis: kelas bertingkat dikaitkan dengan kelas penutup itu sendiri, sedangkan kelas dalam dikaitkan dengan objek kelas penutup.

Karena itu, objek kelas dalam memerlukan objek dari kelas yang melingkupi, sedangkan objek kelas bersarang tidak. Oleh karena itu, kelas bertingkat berperilaku seperti kelas tingkat atas, menggunakan kelas yang melingkupi untuk menyediakan organisasi seperti paket. Selain itu, kelas bertingkat memiliki akses ke semua anggota kelas yang melingkupinya.

Motivasi

Pertimbangkan subsistem Java yang khas, misalnya komponen Swing, menggunakan pola desain Model-View-Controller (MVC). Objek acara merangkum pemberitahuan perubahan dari model. Tampilan menunjukkan minat dalam berbagai peristiwa dengan menambahkan listener ke model komponen yang mendasarinya. Model tersebut memberi tahu pengaksesnya tentang perubahan dalam statusnya sendiri dengan mengirimkan objek peristiwa ini ke pemroses terdaftarnya. Seringkali, pendengar dan jenis peristiwa ini dikhususkan untuk jenis model, dan oleh karena itu masuk akal hanya dalam konteks jenis model. Karena setiap pemroses dan jenis peristiwa ini harus dapat diakses publik, masing-masing harus berada dalam file sumbernya sendiri. Dalam situasi ini, kecuali beberapa konvensi pengkodean digunakan, kopling antara tipe ini sulit dikenali. Tentu saja, seseorang dapat menggunakan paket terpisah untuk setiap grup untuk menunjukkan kopling,tetapi ini menghasilkan sejumlah besar paket.

Jika kita mengimplementasikan tipe listener dan event sebagai tipe bersarang dari antarmuka model, kita membuat penggandengannya jelas. Kita dapat menggunakan pengubah akses apa pun yang diinginkan dengan tipe bersarang ini, termasuk publik. Selain itu, karena tipe bersarang menggunakan antarmuka yang melingkupi sebagai namespace, sisa sistem merujuknya sebagai ., menghindari polusi namespace di dalam paket itu. File sumber untuk antarmuka model memiliki semua tipe pendukung, yang membuat pengembangan dan pemeliharaan lebih mudah.

Sebelum: Contoh tanpa kelas bertingkat

Sebagai contoh, kami mengembangkan komponen sederhana Slate, yang tugasnya menggambar bentuk. Sama seperti komponen Swing, kami menggunakan pola desain MVC. Model,, SlateModelberfungsi sebagai tempat penyimpanan bentuk. SlateModelListeners berlangganan perubahan model. Model tersebut memberi tahu pendengarnya dengan mengirimkan peristiwa jenis SlateModelEvent. Dalam contoh ini, kita membutuhkan tiga file sumber, satu untuk setiap kelas:

// SlateModel.java import java.awt.Shape; antarmuka publik SlateModel {// Manajemen pendengar public void addSlateModelListener (SlateModelListener l); public void removeSlateModelListener (SlateModelListener l); // Bentuk manajemen repositori, tampilan perlu pemberitahuan public void addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Bentuk operasi repositori read-only public int getShapeCount (); public Shape getShapeAtIndex (int index); }
// SlateModelListener.java import java.util.EventListener; antarmuka publik SlateModelListener meluas EventListener {public void slateChanged (acara SlateModelEvent); }
// SlateModelEvent.java import java.util.EventObject; kelas publik SlateModelEvent meluas EventObject {publik SlateModelEvent (model SlateModel) {super (model); }}

(Kode sumber untuk DefaultSlateModel, implementasi default untuk model ini, ada di file sebelum / DefaultSlateModel.java.)

Berikutnya, kita mengalihkan perhatian kita untuk Slate, pandangan untuk model ini, yang ke depan tugas lukisan dengan delegasi UI, SlateUI:

// Slate.java import javax.swing.JComponent; kelas publik Slate meluas JComponent mengimplementasikan SlateModelListener {private SlateModel _model; publik Slate (model SlateModel) {_model = model; _model.addSlateModelListener (ini); setOpaque (benar); setUI (baru SlateUI ()); } Slate publik () {ini (baru DefaultSlateModel ()); } publik SlateModel getModel () {return _model; } // Implementasi pendengar public void slateChanged (event SlateModelEvent) {repaint (); }}

Terakhir, SlateUIkomponen GUI visual:

// SlateUI.java import java.awt. *; import javax.swing.JComponent; impor javax.swing.plaf.ComponentUI; kelas publik SlateUI meluas ComponentUI {cat ruang kosong publik (Grafik g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Grafik2D g2D = (Grafik2D) g; untuk (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}

Sesudah: Contoh yang dimodifikasi menggunakan kelas bersarang

Struktur kelas pada contoh di atas tidak menunjukkan hubungan antar kelas. Untuk mengurangi ini, kami telah menggunakan konvensi penamaan yang mengharuskan semua kelas terkait memiliki awalan yang sama, tetapi akan lebih jelas untuk menunjukkan hubungan dalam kode. Selanjutnya, pengembang dan pengelola kelas ini harus mengelola tiga file: untuk SlateModel, untuk SlateEvent, dan untuk SlateListener, untuk mengimplementasikan satu konsep. Hal yang sama berlaku untuk mengelola dua file untuk Slatedan SlateUI.

Kita dapat meningkatkan banyak hal dengan membuat SlateModelListenerdan SlateModelEventtipe SlateModelantarmuka bersarang . Karena tipe bertingkat ini berada di dalam antarmuka, mereka secara implisit bersifat statis. Meskipun demikian, kami telah menggunakan deklarasi statis eksplisit untuk membantu programmer pemeliharaan.

Kode klien akan merujuk mereka sebagai SlateModel.SlateModelListenerdan SlateModel.SlateModelEvent, tetapi ini berlebihan dan tidak perlu panjang. Kami menghapus awalan SlateModeldari kelas bersarang. Dengan perubahan ini, kode klien akan merujuk mereka sebagai SlateModel.Listenerdan SlateModel.Event. Ini singkat dan jelas dan tidak bergantung pada standar pengkodean.

Karena SlateUI, kami melakukan hal yang sama - kami menjadikannya kelas bersarang Slatedan mengubah namanya menjadi UI. Karena ini adalah kelas bersarang di dalam kelas (dan bukan di dalam antarmuka), kita harus menggunakan pengubah statis eksplisit.

Dengan perubahan ini, kita hanya membutuhkan satu file untuk kelas terkait model dan satu lagi untuk kelas terkait tampilan. The SlateModelkode sekarang menjadi:

// SlateModel.java import java.awt.Shape; import java.util.EventListener; import java.util.EventObject; antarmuka publik SlateModel {// Manajemen pendengar public void addSlateModelListener (SlateModel.Listener l); public void removeSlateModelListener (SlateModel.Listener l); // Bentuk manajemen repositori, tampilan perlu pemberitahuan public void addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Bentuk operasi repositori read-only public int getShapeCount (); public Shape getShapeAtIndex (int index); // Kelas bersarang tingkat atas yang terkait dan antarmuka Pemroses antarmuka publik extends EventListener {public void slateChanged (event SlateModel.Event); } kelas publik Acara meluas EventObject {Acara publik (model SlateModel) {super (model); }}}

Dan kode untuk Slatediubah menjadi:

// Slate.java import java.awt. *; import javax.swing.JComponent; impor javax.swing.plaf.ComponentUI; kelas publik Slate meluas JComponent mengimplementasikan SlateModel.Listener {Slate publik (model SlateModel) {_model = model; _model.addSlateModelListener (ini); setOpaque (benar); setUI (baru Slate.UI ()); } Slate publik () {ini (baru DefaultSlateModel ()); } publik SlateModel getModel () {return _model; } // Implementasi pendengar public void slateChanged (event SlateModel.Event) {repaint (); } UI kelas statis publik meluas ComponentUI {cat void publik (Grafik g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Grafik2D g2D = (Grafik2D) g; untuk (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}}

(Kode sumber untuk implementasi default untuk model yang diubah DefaultSlateModel,, ada di file setelah / DefaultSlateModel.java.)

Di dalam SlateModelkelas, tidak perlu menggunakan nama yang sepenuhnya memenuhi syarat untuk kelas dan antarmuka bertingkat. Misalnya, Listenercukup di tempat SlateModel.Listener. Namun, menggunakan nama yang sepenuhnya memenuhi syarat membantu pengembang yang menyalin tanda tangan metode dari antarmuka dan menempelkannya ke dalam kelas yang mengimplementasikan.

JFC dan penggunaan kelas bersarang

Pustaka JFC menggunakan kelas bertingkat dalam kasus tertentu. Misalnya, kelas BasicBordersdalam paket javax.swing.plaf.basicmendefinisikan beberapa kelas bersarang seperti BasicBorders.ButtonBorder. Dalam hal ini, kelas BasicBorderstidak memiliki anggota lain dan hanya bertindak sebagai sebuah paket. Menggunakan paket terpisah justru akan sama efektifnya, jika tidak lebih tepat. Ini adalah penggunaan yang berbeda dari yang disajikan dalam artikel ini.

Menggunakan pendekatan tip ini dalam desain JFC akan memengaruhi organisasi pendengar dan jenis peristiwa yang terkait dengan jenis model. Misalnya, javax.swing.event.TableModelListenerdan javax.swing.event.TableModelEventakan diimplementasikan masing-masing sebagai antarmuka bersarang dan kelas bersarang di dalamnya javax.swing.table.TableModel.

This change, together with shortening the names, would result in a listener interface named javax.swing.table.TableModel.Listener and an event class named javax.swing.table.TableModel.Event. TableModel would then be fully self-contained with all the necessary support classes and interfaces rather than having need of support classes and interface spread out over three files and two packages.

Guidelines for using nested classes

As with any other pattern, judicious use of nested classes results in design that is simpler and more easily understood than traditional package organization. However, incorrect usage leads to unnecessary coupling, which makes the role of nested classes unclear.

Note that in the nested example above, we make use of nested types only for types that cannot stand without context of enclosing type. We do not, for example, make SlateModel a nested interface of Slate because there may be other view types using the same model.

Given any two classes, apply the following guidelines to decide if you should use nested classes. Use nested classes to organize your classes only if the answer to both questions below is yes:

  1. Is it possible to clearly classify one of the classes as the primary class and the other as a supporting class?

  2. Is the supporting class meaningless if the primary class is removed from the subsystem?

Conclusion

The pattern of using nested classes couples the related types tightly. It avoids namespace pollution by using the enclosing type as namespace. It results in fewer source files, without losing the ability to publicly expose supporting types.

As with any other pattern, use this pattern judiciously. In particular, ensure that nested types are truly related and have no meaning without the context of the enclosing type. Correct usage of the pattern doesn't increase coupling, but merely clarifies the existent coupling.

Ramnivas Laddad adalah Arsitek Bersertifikat Sun untuk Teknologi Java (Java 2). Ia memiliki gelar Master di bidang teknik elektro dengan spesialisasi di bidang teknik komunikasi. Dia memiliki enam tahun pengalaman merancang dan mengembangkan beberapa proyek perangkat lunak yang melibatkan GUI, jaringan, dan sistem terdistribusi. Dia telah mengembangkan sistem perangkat lunak berorientasi objek di Java selama dua tahun terakhir dan di C ++ selama lima tahun terakhir. Ramnivas saat ini bekerja di Real-Time Innovations Inc. sebagai insinyur perangkat lunak. Di RTI, dia saat ini bekerja untuk merancang dan mengembangkan ControlShell, kerangka kerja pemrograman berbasis komponen untuk membangun sistem waktu nyata yang kompleks.