Perangkap dan penambahbaikan corak Rantai Tanggungjawab

Baru-baru ini saya menulis dua program Java (untuk Microsoft Windows OS) yang mesti menangkap peristiwa papan kekunci global yang dihasilkan oleh aplikasi lain yang berjalan secara bersamaan di desktop yang sama. Microsoft menyediakan cara untuk melakukannya dengan mendaftarkan program sebagai pendengar cangkuk papan kekunci global. Pengekodan tidak memerlukan masa yang lama, tetapi penyahpepijatan dilakukan. Kedua-dua program itu kelihatan berjalan lancar apabila diuji secara berasingan, tetapi gagal ketika diuji bersama. Ujian lebih lanjut menunjukkan bahawa ketika kedua program berjalan bersama, program yang diluncurkan pertama kali tidak dapat menangkap acara utama global, tetapi aplikasi yang dilancarkan kemudian berjalan lancar.

Saya menyelesaikan misteri itu setelah membaca dokumentasi Microsoft. Kod yang mendaftarkan program itu sendiri sebagai pendengar mata kail tidak mendapat CallNextHookEx()panggilan yang diperlukan oleh rangka cangkuk. Dokumentasi berbunyi bahawa setiap pendengar cangkuk ditambahkan ke rantai cangkuk mengikut urutan permulaan; pendengar terakhir yang dimulakan akan berada di puncak. Acara dihantar kepada pendengar pertama dalam rangkaian. Untuk membolehkan semua pendengar menerima acara, setiap pendengar mesti membuat CallNextHookEx()panggilan untuk menyampaikan acara kepada pendengar di sebelahnya. Sekiranya ada pendengar yang terlupa melakukannya, pendengar seterusnya tidak akan mendapat acara; akibatnya, fungsi mereka yang dirancang tidak akan berfungsi. Itulah sebab tepat mengapa program kedua saya berjaya tetapi yang pertama tidak!

Misteri itu diselesaikan, tetapi saya tidak berpuas hati dengan rangka cangkuk. Pertama, saya memerlukan "ingat" untuk memasukkan CallNextHookEx()kaedah panggilan ke dalam kod saya. Kedua, program saya boleh mematikan program lain dan sebaliknya. Mengapa perkara itu berlaku? Kerana Microsoft melaksanakan rangka kerja cantuman global mengikut corak Rantai Tanggungjawab (CoR) klasik yang ditentukan oleh Gang of Four (GoF).

Dalam artikel ini, saya membincangkan kelemahan pelaksanaan CoR yang disarankan oleh GoF dan mencadangkan jalan keluarnya. Itu dapat membantu anda mengelakkan masalah yang sama semasa anda membuat kerangka kerja CoR anda sendiri.

CoR Klasik

Corak CoR klasik yang ditentukan oleh GoF dalam Corak Reka Bentuk :

"Elakkan menghubungkan pengirim permintaan ke penerima dengan memberikan lebih dari satu objek kesempatan untuk menangani permintaan tersebut. Rantai objek yang diterima dan lulus permintaan di sepanjang rantai hingga suatu objek menanganinya."

Rajah 1 menggambarkan rajah kelas.

Struktur objek khas mungkin kelihatan seperti Gambar 2.

Dari ilustrasi di atas, kita dapat merumuskan bahawa:

  • Pelbagai pengendali mungkin dapat menangani permintaan
  • Hanya satu pengendali yang benar-benar menangani permintaan tersebut
  • Pemohon hanya tahu merujuk kepada satu pengendali
  • Pemohon tidak tahu berapa banyak pengendali yang dapat menangani permintaannya
  • Pemohon tidak tahu pengendali mana yang menangani permintaannya
  • Pemohon tidak mempunyai kawalan ke atas pengendali
  • Pengendali dapat ditentukan secara dinamik
  • Mengubah senarai pengendali tidak akan mempengaruhi kod pemohon

Segmen kod di bawah menunjukkan perbezaan antara kod pemohon yang menggunakan CoR dan kod pemohon yang tidak.

Kod pemohon yang tidak menggunakan CoR:

pengendali = getHandlers (); untuk (int i = 0; i <handlers.length; i ++) {pengendali [i] .handle (permintaan); jika (pengendali [i]. ditangani ()) pecah; }

Kod pemohon yang menggunakan CoR:

 getChain (). menangani (permintaan); 

Setakat ini, semuanya kelihatan sempurna. Tetapi mari kita lihat pelaksanaan yang dicadangkan oleh GoF untuk CoR klasik:

Pengendali kelas awam {pengganti Pengendali swasta; Pengendali awam (HelpHandler s) {pengganti = s; } pegangan awam (permintaan ARequest) {if (penerus! = null) pengganti.handle (permintaan); }} kelas awam AHandler memanjangkan Handler {public handle (permintaan ARequest) {if (someCondition) // Pengendalian: buat perkara lain super.handle (request); }}

Kelas asas mempunyai kaedah, handle()yang memanggil penggantinya, simpul seterusnya dalam rantai, untuk menangani permintaan tersebut. Subkelas mengatasi kaedah ini dan memutuskan sama ada membiarkan rantaian bergerak maju. Sekiranya node menangani permintaan, subkelas tidak akan memanggil super.handle()yang memanggil pengganti, dan rantaian berjaya dan berhenti. Sekiranya node tidak menangani permintaan, subkelas mesti memanggil super.handle()agar rantai terus bergulir, atau rantai berhenti dan gagal. Kerana peraturan ini tidak ditegakkan di kelas dasar, kepatuhannya tidak terjamin. Apabila pembangun lupa membuat panggilan dalam subkelas, rangkaian gagal. Kelemahan asas di sini adalah bahawa pembuatan keputusan pelaksanaan rantai, yang bukan urusan subkelas, digabungkan dengan penanganan permintaan di subkelas. Itu melanggar prinsip reka bentuk berorientasikan objek: objek hanya perlu memikirkan perniagaannya sendiri. Dengan membiarkan subkelas membuat keputusan, anda akan memberi beban tambahan dan kemungkinan kesalahan.

Celah kerangka cangkuk global Microsoft Windows dan kerangka penapis servlet Java

Pelaksanaan rangka kerja cangkuk global Microsoft Windows adalah sama dengan pelaksanaan CoR klasik yang disarankan oleh GoF. Rangka kerja bergantung pada pendengar cangkuk individu untuk membuat CallNextHookEx()panggilan dan menyampaikan acara melalui rantai. Ia mengandaikan bahawa pembangun akan selalu mengingati peraturan dan tidak pernah lupa membuat panggilan. Secara semula jadi, rantai kait acara global bukan CoR klasik. Acara mesti disampaikan kepada semua pendengar dalam rangkaian, tanpa mengira sama ada pendengar sudah mengatasinya. Jadi CallNextHookEx()panggilan itu sepertinya tugas kelas asas, bukan pendengar individu. Membiarkan setiap pendengar membuat panggilan tidak ada gunanya dan memperkenalkan kemungkinan untuk menghentikan rangkaian secara tidak sengaja.

Kerangka penapis servlet Java membuat kesilapan yang serupa dengan cangkuk global Microsoft Windows. Ia mengikuti dengan tepat pelaksanaan yang disarankan oleh GoF. Setiap penapis memutuskan sama ada menggulung atau menghentikan rangkaian dengan memanggil atau tidak memanggil doFilter()penapis seterusnya. Peraturan ini dikuatkuasakan melalui javax.servlet.Filter#doFilter()dokumentasi:

"4. a) Entah memanggil entiti seterusnya dalam rantai menggunakan FilterChainobjek ( chain.doFilter()), 4. b) atau tidak meneruskan pasangan permintaan / respons ke entiti berikutnya dalam rantai penapis untuk menyekat pemprosesan permintaan."

Sekiranya satu penapis lupa membuat chain.doFilter()panggilan ketika seharusnya, ia akan mematikan penapis lain dalam rantai. Sekiranya satu penapis membuat chain.doFilter()panggilan ketika tidak seharusnya dilakukan , ia akan menggunakan penapis lain dalam rantai.

Penyelesaian

The rules of a pattern or a framework should be enforced through interfaces, not documentation. Counting on developers to remember the rule doesn't always work. The solution is to decouple the chain execution decision-making and the request-handling by moving the next() call to the base class. Let the base class make the decision, and let subclasses handle the request only. By steering clear of decision-making, subclasses can completely focus on their own business, thus avoiding the mistake described above.

Classic CoR: Send request through the chain until one node handles the request

This is the implementation I suggest for the classic CoR:

 /** * Classic CoR, i.e., the request is handled by only one of the handlers in the chain. */ public abstract class ClassicChain { /** * The next node in the chain. */ private ClassicChain next; public ClassicChain(ClassicChain nextNode) { next = nextNode; } /** * Start point of the chain, called by client or pre-node. * Call handle() on this node, and decide whether to continue the chain. If the next node is not null and * this node did not handle the request, call start() on next node to handle request. * @param request the request parameter */ public final void start(ARequest request) { boolean handledByThisNode = this.handle(request); if (next != null && !handledByThisNode) next.start(request); } /** * Called by start(). * @param request the request parameter * @return a boolean indicates whether this node handled the request */ protected abstract boolean handle(ARequest request); } public class AClassicChain extends ClassicChain { /** * Called by start(). * @param request the request parameter * @return a boolean indicates whether this node handled the request */ protected boolean handle(ARequest request) { boolean handledByThisNode = false; if(someCondition) { //Do handling handledByThisNode = true; } return handledByThisNode; } } 

The implementation decouples the chain execution decision-making logic and request-handling by dividing them into two separate methods. Method start() makes the chain execution decision and handle() handles the request. Method start() is the chain execution's starting point. It calls handle() on this node and decides whether to advance the chain to the next node based on whether this node handles the request and whether a node is next to it. If the current node doesn't handle the request and the next node is not null, the current node's start() method advances the chain by calling start() on the next node or stops the chain by not calling start() on the next node. Method handle() in the base class is declared abstract, providing no default handling logic, which is subclass-specific and has nothing to do with chain execution decision-making. Subclasses override this method and return a Boolean value indicating whether the subclasses handle the request themselves. Note that the Boolean returned by a subclass informs start() in the base class whether the subclass has handled the request, not whether to continue the chain. The decision of whether to continue the chain is completely up to the base class's start() method. The subclasses can't change the logic defined in start() because start() is declared final.

In this implementation, a window of opportunity remains, allowing the subclasses to mess up the chain by returning an unintended Boolean value. However, this design is much better than the old version, because the method signature enforces the value returned by a method; the mistake is caught at compile time. Developers are no longer required to remember to either make the next() call or return a Boolean value in their code.

Non-classic CoR 1: Send request through the chain until one node wants to stop

This type of CoR implementation is a slight variation of the classic CoR pattern. The chain stops not because one node has handled the request, but because one node wants to stop. In that case, the classic CoR implementation also applies here, with a slight conceptual change: the Boolean flag returned by the handle() method doesn't indicate whether the request has been handled. Rather, it tells the base class whether the chain should be stopped. The servlet filter framework fits in this category. Instead of forcing individual filters to call chain.doFilter(), the new implementation forces the individual filter to return a Boolean, which is contracted by the interface, something the developer never forgets or misses.

Non-classic CoR 2: Regardless of request handling, send request to all handlers

For this type of CoR implementation, handle() doesn't need to return the Boolean indicator, because the request is sent to all handlers regardless. This implementation is easier. Because the Microsoft Windows global hook framework by nature belongs to this type of CoR, the following implementation should fix its loophole:

 /** * Non-Classic CoR 2, i.e., the request is sent to all handlers regardless of the handling. */ public abstract class NonClassicChain2 { /** * The next node in the chain. */ private NonClassicChain2 next; public NonClassicChain2(NonClassicChain2 nextNode) { next = nextNode; } /** * Start point of the chain, called by client or pre-node. * Call handle() on this node, then call start() on next node if next node exists. * @param request the request parameter */ public final void start(ARequest request) { this.handle(request); if (next != null) next.start(request); } /** * Called by start(). * @param request the request parameter */ protected abstract void handle(ARequest request); } public class ANonClassicChain2 extends NonClassicChain2 { /** * Called by start(). * @param request the request parameter */ protected void handle(ARequest request) { //Do handling. } } 

Contoh

Di bahagian ini, saya akan menunjukkan kepada anda dua contoh rantai yang menggunakan pelaksanaan untuk CoR 2 bukan klasik yang dijelaskan di atas.

Contoh 1