Капаните и подобренията на модела „Верига на отговорността“

Наскоро написах две Java програми (за Microsoft Windows OS), които трябва да улавят глобални клавиатурни събития, генерирани от други приложения, изпълнявани едновременно на същия работен плот. Microsoft предоставя начин за това, като регистрира програмите като глобален слушател на куки за клавиатура. Кодирането не отне много време, но отстраняването на грешки. Изглежда, че двете програми работят добре, когато се тестват отделно, но се провалят, когато се тестват заедно. По-нататъшни тестове разкриха, че когато двете програми се изпълняват заедно, програмата, която стартира първа, винаги не може да улови глобалните ключови събития, но стартираното по-късно приложение работи добре.

Разгадах загадката, след като прочетох документацията на Microsoft. Кодът, който регистрира самата програма като слушател на прослушване, пропуска CallNextHookEx()повикването, изисквано от рамката на привързване. Документацията гласи, че всеки слушател на куки се добавя към верига на куки по реда на стартиране; последният стартиран слушател ще бъде отгоре. Събитията се изпращат до първия слушател във веригата. За да позволи на всички слушатели да получават събития, всеки слушател трябва да се CallNextHookEx()обади, за да предаде събитията на слушателя до него. Ако някой слушател забрави да го направи, следващите слушатели няма да получат събитията; в резултат проектираните им функции няма да работят. Това беше точната причина, поради която втората ми програма работи, но първата не!

Мистерията беше разрешена, но бях недоволен от рамката на куката. Първо, това изисква от мен да "запомня" да вмъкна CallNextHookEx()извикването на метода в моя код. Второ, моята програма може да деактивира други програми и обратно. Защо се случва това? Тъй като Microsoft внедри глобалната рамка на куки, следвайки точно класическия модел Chain of Responsibility (CoR), определен от Gang of Four (GoF).

В тази статия обсъждам вратичката в изпълнението на КР, предложена от GoF, и предлагам решение за него. Това може да ви помогне да избегнете същия проблем, когато създавате своя собствена рамка на КР.

Класически КР

Класическият модел на КР, определен от GoF в Design Patterns :

"Избягвайте да свързвате подателя на заявка с нейния приемник, като давате шанс на повече от един обект да обработи заявката. Свържете веригата на приемащите обекти и предайте заявката по веригата, докато обектът я обработи."

Фигура 1 илюстрира диаграмата на класа.

Типична обектна структура може да изглежда като Фигура 2.

От горните илюстрации можем да обобщим, че:

  • Множество манипулатори може да са в състояние да обработят заявка
  • Всъщност само един манипулатор обработва заявката
  • Заявителят знае само препратка към един манипулатор
  • Заявителят не знае колко манипулатори са в състояние да обработят заявката му
  • Заявителят не знае кой манипулатор е обработил заявката му
  • Заявителят няма контрол над манипулаторите
  • Манипулаторите могат да се определят динамично
  • Промяната на списъка с манипулатори няма да повлияе на кода на заявителя

Сегментите на кода по-долу показват разликата между кода на заявителя, който използва CoR, и кода на заявителя, който не използва.

Код на заявителя, който не използва CoR:

манипулатори = getHandlers (); за (int i = 0; i <handlers.length; i ++) {handlers [i] .handle (заявка); if (handlers [i] .handled ()) break; }

Код на заявителя, който използва CoR:

 getChain (). handle (заявка); 

Към момента всичко изглежда перфектно. Но нека разгледаме изпълнението, което GoF предлага за класическия КР:

публичен клас Handler {наследник на частния Handler; публичен манипулатор (HelpHandler s) {наследник = s; } публичен манипулатор (заявка за ARequest) {if (наследник! = нула) successor.handle (заявка); }} публичен клас AHandler разширява Handler {public handle (ARequest request) {if (someCondition) // Handling: направи нещо друго super.handle (заявка); }}

Основният клас има метод, handle()който извиква неговия наследник, следващия възел във веригата, за обработка на заявката. Подкласовете заменят този метод и решават дали да позволят на веригата да продължи. Ако възелът обработва заявката, подкласът няма да извика super.handle()това, което извиква наследника, и веригата успява и спира. Ако възелът не обработва заявката, подкласът трябва да се обади, за super.handle()да поддържа веригата да се търкаля, или веригата спира и се проваля. Тъй като това правило не се прилага в базовия клас, неговото съответствие не е гарантирано. Когато разработчиците забравят да се обадят в подкласове, веригата се проваля. Основният недостатък тук е, че вземането на решения за изпълнение на вериги, което не е предмет на подкласове, е съчетано с обработка на заявки в подкласовете. Това нарушава принципа на обектно-ориентирания дизайн: обектът трябва да гледа само собствения си бизнес. Като оставите даден подклас да вземе решение, вие въвеждате допълнителна тежест върху него и възможност за грешка.

Loophole на Microsoft Windows глобална рамка за закачане и рамка за филтриране на Java сървлети

Внедряването на глобалната рамка за куки на Microsoft Windows е същото като класическото внедряване на КР, предложено от GoF. Рамката зависи от отделните слушатели на закачане, за да се CallNextHookEx()обадят и да предадат събитието през веригата. Предполага се, че разработчиците винаги ще помнят правилото и никога няма да забравят да се обадят. По природа глобалната верига за събития не е класически КР. Събитието трябва да бъде доставено на всички слушатели във веригата, независимо дали слушателят вече се справя с него. Така че CallNextHookEx()разговорът изглежда е работа на базовия клас, а не на отделните слушатели. Позволяването на отделните слушатели да се обадят не носи никаква полза и въвежда възможността за случайно спиране на веригата.

Рамката на филтъра за сървлети на Java прави подобна грешка като глобалната кука на Microsoft Windows. То следва точно изпълнението, предложено от GoF. Всеки филтър решава дали да превърти или спре веригата, като извика или не извика doFilter()следващия филтър. Правилото се прилага чрез javax.servlet.Filter#doFilter()документация:

"4. а) Или извикайте следващия обект във веригата, използвайки FilterChainобекта ( chain.doFilter()), 4. б), или не предайте двойката заявка / отговор на следващия обект във филтърната верига, за да блокирате обработката на заявката."

Ако един филтър забрави да осъществи chain.doFilter()повикването, когато трябва, той ще деактивира други филтри във веригата. Ако един филтър chain.doFilter()извика, когато не трябва, той ще извика други филтри във веригата.

Решение

Правилата за шаблон или рамка трябва да се прилагат чрез интерфейси, а не чрез документация. Разчитането на разработчиците да запомнят правилото не винаги работи. Решението е да отделите вземането на решения за изпълнение на веригата и обработката на заявки чрез преместване на next()повикването в основния клас. Нека базовият клас вземе решение и нека подкласовете обработват само заявката. Избягвайки вземането на решения, подкласовете могат да се фокусират изцяло върху собствения си бизнес, като по този начин се избягва грешката, описана по-горе.

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. } } 

Примери

В този раздел ще ви покажа два примерни вериги, които използват изпълнението за некласически CoR 2, описано по-горе.

Пример 1