Segui la catena di responsabilità

Di recente sono passato a Mac OS X da Windows e sono entusiasta dei risultati. Ma poi di nuovo, ho trascorso solo un breve periodo di cinque anni su Windows NT e XP; prima di allora sono stato rigorosamente uno sviluppatore Unix per 15 anni, principalmente su macchine Sun Microsystems. Ho anche avuto la fortuna di sviluppare software con Nextstep, il lussureggiante predecessore basato su Unix di Mac OS X, quindi sono un po 'di parte.

A parte la sua bellissima interfaccia utente Aqua, Mac OS X è Unix, probabilmente il miglior sistema operativo esistente. Unix ha molte caratteristiche interessanti; uno dei più noti è il pipe , che consente di creare combinazioni di comandi collegando l'output di un comando all'input di un altro. Ad esempio, si supponga di voler elencare i file di origine dalla distribuzione di origine di Struts che invocano o definiscono un metodo denominato execute(). Ecco un modo per farlo con una pipa:

grep "execute (" `trova $ STRUTS_SRC_DIR -name" * .java "` | awk -F: '{print}'

Il grepcomando cerca nei file espressioni regolari; qui, lo uso per trovare le occorrenze della stringa execute(nei file scoperti dal findcomando. grepL'output di viene reindirizzato awk, che stampa il primo token, delimitato da due punti, in ogni riga grepdell'output di (una barra verticale indica una barra verticale). Quel token è un nome di file, quindi finisco con un elenco di nomi di file che contengono la stringa execute(.

Ora che ho un elenco di nomi di file, posso usare un altro pipe per ordinare l'elenco:

grep "execute (" `trova $ STRUTS_SRC_DIR -name" * .java "` | awk -F: '{print}' | sort

Questa volta, ho reindirizzato l'elenco dei nomi di file a sort. E se volessi sapere quanti file contengono la stringa execute(? È facile con un altro tubo:

 grep "execute (" `trova $ STRUTS_SRC_DIR -name" * .java "` | awk -F: '{print}' | sort -u | wc -l 

Il wccomando conta parole, righe e byte. In questo caso, ho specificato l' -lopzione per contare le righe, una riga per ogni file. Ho anche aggiunto -uun'opzione per sortgarantire l'unicità di ogni nome di file (l' -uopzione filtra i duplicati).

I pipe sono potenti perché ti consentono di comporre dinamicamente una catena di operazioni. I sistemi software spesso utilizzano l'equivalente di pipe (ad esempio, filtri di posta elettronica o una serie di filtri per un servlet). Al centro di tubi e filtri c'è un modello di progettazione: Catena di responsabilità (CoR).

Nota: puoi scaricare il codice sorgente di questo articolo da Risorse.

Introduzione del CdR

Il modello Catena di responsabilità utilizza una catena di oggetti per gestire una richiesta, che in genere è un evento. Gli oggetti nella catena inoltrano la richiesta lungo la catena finché uno degli oggetti non gestisce l'evento. L'elaborazione si interrompe dopo la gestione di un evento.

La figura 1 illustra come il modello del CdR elabora le richieste.

In Design Patterns , gli autori descrivono il modello Chain of Responsibility in questo modo:

Evita di accoppiare il mittente di una richiesta al suo destinatario dando a più di un oggetto la possibilità di gestire la richiesta. Concatena gli oggetti riceventi e passa la richiesta lungo la catena finché un oggetto non la gestisce.

Il modello Catena di responsabilità è applicabile se:

  • Si desidera disaccoppiare il mittente e il destinatario di una richiesta
  • Più oggetti, determinati in fase di esecuzione, sono candidati per gestire una richiesta
  • Non si desidera specificare i gestori in modo esplicito nel codice

Se utilizzi il modello CoR, ricorda:

  • Solo un oggetto nella catena gestisce una richiesta
  • Alcune richieste potrebbero non essere gestite

Queste restrizioni, ovviamente, riguardano una classica attuazione del CdR. In pratica, quelle regole sono piegate; ad esempio, i filtri servlet sono un'implementazione del CdR che consente a più filtri di elaborare una richiesta HTTP.

La Figura 2 mostra un diagramma delle classi del modello CoR.

In genere, i gestori di richieste sono estensioni di una classe base che mantiene un riferimento al gestore successivo nella catena, noto come successor. La classe base potrebbe essere implementata in handleRequest()questo modo:

public abstract class HandlerBase {... public void handleRequest (SomeRequestObject sro) {if (successor! = null) successor.handleRequest (sro); }}

Quindi, per impostazione predefinita, i gestori passano la richiesta al gestore successivo nella catena. Un'estensione concreta di HandlerBasepotrebbe assomigliare a questa:

public class SpamFilter estende HandlerBase {public void handleRequest (SomeRequestObject mailMessage) {if (isSpam (mailMessage)) {// Se il messaggio è spam // intraprendi un'azione correlata allo spam. Non inoltrare il messaggio. } else {// Il messaggio non è spam. super.handleRequest (mailMessage); // Passa il messaggio al filtro successivo nella catena. }}}

Il SpamFiltergestore gestisce la richiesta (presumibilmente la ricezione di una nuova email) se il messaggio è spam e, quindi, la richiesta non va oltre; in caso contrario, i messaggi affidabili vengono passati al gestore successivo, presumibilmente un altro filtro e-mail che cerca di eliminarli. Alla fine, l'ultimo filtro della catena potrebbe memorizzare il messaggio dopo che ha superato il raduno passando attraverso diversi filtri.

Nota che gli ipotetici filtri di posta elettronica discussi sopra si escludono a vicenda: in definitiva, solo un filtro gestisce una richiesta. Potresti scegliere di capovolgerlo lasciando che più filtri gestiscano una singola richiesta, che è una migliore analogia con le pipe Unix. In ogni caso, il motore sottostante è il modello CoR.

In questo articolo, discuto di due implementazioni del pattern Chain of Responsibility: filtri servlet, un'implementazione popolare del CdR che consente a più filtri di gestire una richiesta, e il modello di eventi dell'Abstract Window Toolkit (AWT) originale, un'implementazione classica del CoR impopolare che alla fine è stata deprecata .

Filtri servlet

All'inizio della piattaforma Java 2, Enterprise Edition (J2EE), alcuni contenitori servlet fornivano una comoda funzionalità nota come servlet chaining, per cui si poteva essenzialmente applicare un elenco di filtri a un servlet. I filtri servlet sono popolari perché sono utili per sicurezza, compressione, registrazione e altro. E, naturalmente, puoi comporre una catena di filtri per eseguire alcune o tutte queste cose a seconda delle condizioni di runtime.

Con l'avvento della Java Servlet Specification versione 2.3, i filtri sono diventati componenti standard. A differenza del CoR classico, i filtri servlet consentono a più oggetti (filtri) in una catena di gestire una richiesta.

I filtri servlet sono una potente aggiunta a J2EE. Inoltre, dal punto di vista dei design pattern, forniscono una svolta interessante: se si desidera modificare la richiesta o la risposta, si utilizza il pattern Decorator oltre al CoR. La Figura 3 mostra come funzionano i filtri servlet.

Un semplice filtro servlet

Devi fare tre cose per filtrare un servlet:

  • Implementa un servlet
  • Implementa un filtro
  • Associa il filtro e il servlet

Gli esempi 1-3 eseguono tutti e tre i passaggi in successione:

Esempio 1. Un servlet

import java.io.PrintWriter; import javax.servlet.*; import javax.servlet.http.*; public class FilteredServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException { PrintWriter out = response.getWriter(); out.println("Filtered Servlet invoked"); } } 

Example 2. A filter

import java.io.PrintWriter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; public class AuditFilter implements Filter { private ServletContext app = null; public void init(FilterConfig config) { app = config.getServletContext(); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, javax.servlet.ServletException { app.log(((HttpServletRequest)request).getServletPath()); chain.doFilter(request, response); } public void destroy() { } } 

Example 3. The deployment descriptor

    auditFilter AuditFilter  <filter-mapping>auditFilter/filteredServlet</filter-mapping>   filteredServlet FilteredServlet   filteredServlet /filteredServlet  ...  

If you access the servlet with the URL /filteredServlet, the auditFilter gets a crack at the request before the servlet. AuditFilter.doFilter writes to the servlet container log file and calls chain.doFilter() to forward the request. Servlet filters are not required to call chain.doFilter(); if they don't, the request is not forwarded. I can add more filters, which would be invoked in the order they are declared in the preceding XML file.

Now that you've seen a simple filter, let's look at another filter that modifies the HTTP response.

Filter the response with the Decorator pattern

Unlike the preceding filter, some servlet filters need to modify the HTTP request or response. Interestingly enough, that task involves the Decorator pattern. I discussed the Decorator pattern in two previous Java Design Patterns articles: "Amaze Your Developer Friends with Design Patterns" and "Decorate Your Java Code."

Example 4 lists a filter that performs a simple search and replace in the body of the response. That filter decorates the servlet response and passes the decorator to the servlet. When the servlet finishes writing to the decorated response, the filter performs a search and replace within the response's content.

Example 4. A search and replace filter

import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class SearchAndReplaceFilter implements Filter { private FilterConfig config; public void init(FilterConfig config) { this.config = config; } public FilterConfig getFilterConfig() { return config; } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, javax.servlet.ServletException { StringWrapper wrapper = new StringWrapper((HttpServletResponse)response); chain.doFilter(request, wrapper); String responseString = wrapper.toString(); String search = config.getInitParameter("search"); String replace = config.getInitParameter("replace"); if(search == null || replace == null) return; // Parameters not set properly int index = responseString.indexOf(search); if(index != -1) { String beforeReplace = responseString.substring(0, index); String afterReplace=responseString.substring(index + search.length()); response.getWriter().print(beforeReplace + replace + afterReplace); } } public void destroy() { config = null; } } 

The preceding filter looks for filter init parameters named search and replace; if they are defined, the filter replaces the first occurrence of the search parameter value with the replace parameter value.

SearchAndReplaceFilter.doFilter() wraps (or decorates) the response object with a wrapper (decorator) that stands in for the response. When SearchAndReplaceFilter.doFilter() calls chain.doFilter() to forward the request, it passes the wrapper instead of the original response. The request is forwarded to the servlet, which generates the response.

When chain.doFilter() returns, the servlet is done with the request, so I go to work. First, I check for the search and replace filter parameters; if present, I obtain the string associated with the response wrapper, which is the response content. Then I make the substitution and print it back to the response.

Example 5 lists the StringWrapper class.

Example 5. A decorator

import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class StringWrapper extends HttpServletResponseWrapper { StringWriter writer = new StringWriter(); public StringWrapper(HttpServletResponse response) { super(response); } public PrintWriter getWriter() { return new PrintWriter(writer); } public String toString() { return writer.toString(); } } 

StringWrapper, which decorates the HTTP response in Example 4, is an extension of HttpServletResponseWrapper, which spares us the drudgery of creating a decorator base class for decorating HTTP responses. HttpServletResponseWrapper ultimately implements the ServletResponse interface, so instances of HttpServletResponseWrapper can be passed to any method expecting a ServletResponse object. That's why SearchAndReplaceFilter.doFilter() can call chain.doFilter(request, wrapper) instead of chain.doFilter(request, response).

Ora che abbiamo un filtro e un wrapper di risposta, associamo il filtro a un pattern URL e specificiamo i pattern di ricerca e sostituzione: