En el diseño orientado a objetos , el patrón de cadena de responsabilidad es un patrón de diseño de comportamiento que consta de una fuente de objetos de comando y una serie de objetos de procesamiento . [1] Cada objeto de procesamiento contiene lógica que define los tipos de objetos de comando que puede manejar; el resto se pasa al siguiente objeto de procesamiento de la cadena. También existe un mecanismo para agregar nuevos objetos de procesamiento al final de esta cadena.
En una variación del modelo estándar de cadena de responsabilidad, algunos manejadores pueden actuar como despachadores , capaces de enviar comandos en una variedad de direcciones, formando un árbol de responsabilidad . En algunos casos, esto puede ocurrir de forma recursiva, con objetos de procesamiento que llaman a objetos de procesamiento superiores con comandos que intentan resolver una parte más pequeña del problema; en este caso, la recursividad continúa hasta que se procesa el comando o se explora todo el árbol. Un intérprete de XML podría funcionar de esta manera.
Este patrón promueve la idea de un acoplamiento flojo .
El patrón de la cadena de responsabilidad es estructuralmente casi idéntico al patrón del decorador , la diferencia es que para el decorador, todas las clases manejan la solicitud, mientras que para la cadena de responsabilidad, exactamente una de las clases en la cadena maneja la solicitud. Esta es una definición estricta del concepto de Responsabilidad en el libro GoF . Sin embargo, muchas implementaciones (como los registradores a continuación, el manejo de eventos de la interfaz de usuario o los filtros de servlets en Java, etc.) permiten que varios elementos de la cadena asuman la responsabilidad.
Descripción general
El patrón de diseño Chain of Responsibility [2] es uno de los veintitrés patrones de diseño GoF bien conocidos que describen soluciones comunes a problemas de diseño recurrentes al diseñar software orientado a objetos flexible y reutilizable, es decir, objetos que son más fáciles de implementar, cambiar, probar y reutilizar.
¿Qué problemas puede resolver el patrón de diseño de la Cadena de responsabilidad? [3]
- Debe evitarse vincular el remitente de una solicitud a su receptor.
- Debería ser posible que más de un receptor pueda manejar una solicitud.
Implementar una solicitud directamente dentro de la clase que envía la solicitud es inflexible porque acopla la clase a un receptor en particular y hace que sea imposible admitir varios receptores.
¿Qué solución describe el patrón de diseño de la Cadena de responsabilidad?
- Defina una cadena de objetos receptores que tengan la responsabilidad, según las condiciones de tiempo de ejecución, de manejar una solicitud o reenviarla al siguiente receptor de la cadena (si corresponde).
Esto nos permite enviar una solicitud a una cadena de receptores sin tener que saber cuál maneja la solicitud. La solicitud se pasa a lo largo de la cadena hasta que un receptor la maneja. El remitente de una solicitud ya no está vinculado a un receptor en particular.
Consulte también el diagrama de secuencia y clase UML a continuación.
Estructura
Diagrama de secuencia y clase UML
En el diagrama de clases de UML anterior , la Sender
clase no se refiere directamente a una clase de receptor en particular. En cambio, se Sender
refiere a la Handler
interfaz para manejar una solicitud ( handler.handleRequest()
), lo que hace Sender
que el receptor sea independiente de qué receptor maneja la solicitud. Los Receiver1
, Receiver2
y Receiver3
clases implementan la Handler
interfaz ya sea por manipulación o la transmisión de una petición (dependiendo de las condiciones de tiempo de ejecución).
El diagrama de secuencia UML muestra las interacciones en tiempo de ejecución: En este ejemplo, el Sender
objeto llama handleRequest()
al receiver1
objeto (de tipo Handler
). El receiver1
reenvía la solicitud a receiver2
, que a su vez reenvía la solicitud a receiver3
, que maneja (realiza) la solicitud.
Ejemplo
Ejemplo de Java
A continuación se muestra un ejemplo de este patrón en Java. Un registrador se crea utilizando una cadena de registradores, cada uno configurado con diferentes niveles de registro.
import java.util.Arrays ;import java.util.EnumSet ;import java.util.function.Consumer ;@FuncionalInterfaz Logger de interfaz pública { public enum LogLevel { INFO , DEPURACIÓN , ADVERTENCIA , ERROR , MENSAJE_FUNCIONAL , ERROR_FUNCIONAL ; LogLevel estático público [] all () { valores de retorno (); } } mensaje vacío abstracto ( String msg , LogLevel severity ); Default Logger appendNext ( Logger nextLogger ) { return ( mensaje , gravedad ) -> { mensaje ( msg , gravedad ); nextLogger . mensaje ( msg , gravedad ); }; } static Logger writeLogger ( LogLevel [] niveles , Consumer < String > stringConsumer ) { EnumSet < LogLevel > set = EnumSet . copyOf ( Arrays . asList ( niveles )); return ( mensaje , gravedad ) -> { si ( conjunto . contiene ( gravedad )) { stringConsumer . aceptar ( mensaje ); } }; } static Logger consoleLogger ( LogLevel ... niveles ) { return writeLogger ( niveles , msg -> System . err . println ( "Escribiendo en la consola:" + msg )); } static Logger emailLogger ( LogLevel ... niveles ) { return writeLogger ( niveles , msg -> System . err . println ( "Envío por correo electrónico:" + msg )); } static Logger fileLogger ( LogLevel ... niveles ) { return writeLogger ( niveles , msg -> System . err . println ( "Escribiendo en el archivo de registro:" + msg )); }}class Runner { public static void main ( String [] args ) { // Construye una cadena de responsabilidad inmutable Logger logger = consoleLogger ( LogLevel . Todo ()) . appendNext ( emailLogger ( LogLevel . FUNCTIONAL_MESSAGE , LogLevel . FUNCTIONAL_ERROR )) . appendNext ( fileLogger ( LogLevel . WARNING , LogLevel . ERROR )); // Manejado por consoleLogger ya que la consola tiene un LogLevel de todos registrador . message ( "Ingresando a la función ProcessOrder ()." , LogLevel . DEBUG ); registrador . mensaje ( "Registro de pedido recuperado." , LogLevel . INFO ); // Manejado por consoleLogger y emailLogger ya que emailLogger implementa Functional_Error & Functional_Message registrador . message ( "No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1." , LogLevel . FUNCTIONAL_ERROR ); registrador . message ( "Pedido enviado ." , LogLevel . FUNCTIONAL_MESSAGE ); // Manejado por consoleLogger y fileLogger ya que fileLogger implementa Warning & Error registrador . message ( "Falta la información de la dirección del cliente en la base de datos de la sucursal" , Nivel de registro . ADVERTENCIA ); registrador . message ( "Faltan los detalles de la dirección del cliente en la base de datos de la organización" , LogLevel . ERROR ); }}
Ejemplo de C #
Estos ejemplos de C # utilizan la aplicación de registro para seleccionar diferentes fuentes según el nivel de registro;
namespace ChainOfResponsibility { [Flags] public enum LogLevel { None = 0 , // 0 Info = 1 , // 1 Debug = 2 , // 10 Warning = 4 , // 100 Error = 8 , // 1000 FunctionalMessage = 16 , / / 10000 FunctionalError = 32 , // 100000 All = 63 // 111111 } /// /// Abstract Handler en patrón de cadena de responsabilidad. /// Logger de clase abstracta pública { protegido LogLevel logMask ; // El siguiente Handler en la cadena protegió a Logger next ; pública Logger ( Loglevel máscara ) { esto . logMask = máscara ; } /// /// Establece el Next logger para hacer una lista / cadena de Handlers. /// public Logger SetNext ( Logger nextlogger ) { Logger lastLogger = this ; while ( lastLogger . next ! = null ) { lastLogger = lastLogger . siguiente ; } lastLogger . next = nextlogger ; devuelve esto ; } public void Message ( string msg , LogLevel severity ) { if (( severity & logMask ) ! = 0 ) // True solo si alguno de los bits de logMask está configurado en severity { WriteMessage ( msg ); } si ( siguiente ! = nulo ) { siguiente . Mensaje ( msg , gravedad ); } } abstracta protegida vacío WriteMessage ( string msg ); } público de clase ConsoleLogger : Logger { público ConsoleLogger ( Loglevel máscara ) : la base ( máscara ) { } protected override void WriteMessage ( string msg ) { Console . WriteLine ( "Escribiendo en la consola:" + msg ); } } público de clase EmailLogger : Logger { público EmailLogger ( Loglevel máscara ) : la base ( máscara ) { } protected override void WriteMessage ( string msg ) { // Marcador de posición para la lógica de envío de correo, generalmente las configuraciones de correo electrónico se guardan en el archivo de configuración. Consola . WriteLine ( "Envío por correo electrónico:" + msg ); } } clase FileLogger : Logger { público FileLogger ( Loglevel máscara ) : la base ( máscara ) { } protected override void WriteMessage ( string msg ) { // Marcador de posición para la consola lógica de escritura de archivos . WriteLine ( "Escritura en archivo de registro:" + msg ); } } Programa de clase pública { public static void Main ( string [] args ) { // Construye la cadena de responsabilidad Logger Logger ; logger = nuevo ConsoleLogger ( LogLevel . Todo ) . SetNext ( nuevo EmailLogger ( LogLevel . FunctionalMessage | LogLevel . FunctionalError )) . SetNext ( nuevo FileLogger ( LogLevel . Warning | LogLevel . Error )); // Manejado por ConsoleLogger ya que la consola tiene un nivel de registro de todos los registradores . Mensaje ( "Ingresando la función ProcessOrder ()." , LogLevel . Debug ); registrador . Mensaje ( "Registro de pedido recuperado" , LogLevel . Info ); // Manejado por ConsoleLogger y FileLogger ya que filelogger implementa el registrador de advertencias y errores . Mensaje ( "Falta la información de la dirección del cliente en la base de datos de la sucursal" , Nivel de registro . Advertencia ); registrador . Mensaje ( "Falta la información de la dirección del cliente en la base de datos de la organización" , LogLevel . Error ); // Manejado por ConsoleLogger y EmailLogger ya que implementa el registrador de errores funcional . Mensaje ( "No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1." , LogLevel . FunctionalError ); // Manejado por ConsoleLogger y EmailLogger logger . Message ( "Pedido enviado." , LogLevel . FunctionalMessage ); } } } / * Escritura de salida a la consola: Ingresando la función ProcessOrder (). Escribiendo en la consola: Registro de pedido recuperado. Escribiendo en la consola: faltan los detalles de la dirección del cliente en la base de datos de la sucursal. Escribiendo en el archivo de registro: faltan los detalles de la dirección del cliente en la base de datos de la sucursal. Escribiendo en la consola: faltan los detalles de la dirección del cliente en la base de datos de la organización. Escribiendo en el archivo de registro: faltan los detalles de la dirección del cliente en la base de datos de la organización. Escribiendo en la consola: No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1. Envío por correo electrónico: No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1. Escribiendo en la consola: pedido enviado. Envío por correo electrónico: pedido enviado. * /
Ejemplo de cristal
enum LogLevel Ninguno Info Debug Warning Error FunctionalMessage FunctionalError All endabstracta clase Logger propiedad log_levels propiedad siguiente : Logger | Nulo def initialize ( * niveles ) @log_levels = [] de LogLevel niveles . cada uno hace | nivel | @log_levels << fin de nivel fin def message ( msg : String , severity : LogLevel ) if @log_levels . incluye? ( LogLevel :: All ) || @log_levels . incluye? ( gravedad ) write_message ( msg ) end @next . try ( &. message ( msg , severity )) end resumen def write_message ( msg : String ) endclass ConsoleLogger < Logger def write_message ( msg : String ) pone "Escribiendo en la consola: # { msg } " end endclass EmailLogger < Logger def write_message ( msg : String ) pone "Sending via email: # { msg } " end endclass FileLogger < Logger def write_message ( msg : String ) coloca "Escribiendo en el archivo de registro: # { msg } " end end# Programa # Construye la cadena de responsabilidad logger = ConsoleLogger . nuevo ( LogLevel :: All ) logger1 = logger . siguiente = EmailLogger . nuevo ( LogLevel :: FunctionalMessage , LogLevel :: FunctionalError ) logger2 = logger1 . siguiente = FileLogger . nuevo ( LogLevel :: Warning , LogLevel :: Error )# Manejado por ConsoleLogger ya que la consola tiene un nivel de registro de todos los registradores . message ( "Ingresando a la función ProcessOrder ()." , LogLevel :: Debug ) logger . mensaje ( "Registro de pedido recuperado" , LogLevel :: Info )# Manejado por ConsoleLogger y FileLogger ya que filelogger implementa el registrador de advertencias y errores . message ( "Faltan los detalles de la dirección del cliente en la base de datos de la sucursal" , LogLevel :: Warning ) logger . message ( "Falta la información de la dirección del cliente en la base de datos de la organización" , LogLevel :: Error )# Manejado por ConsoleLogger y EmailLogger ya que implementa un registrador de errores funcional . mensaje ( "No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1" , LogLevel :: FunctionalError )# Manejado por ConsoleLogger y EmailLogger logger . message ( "Pedido enviado " , LogLevel :: FunctionalMessage )
Producción
Escribiendo en la consola: Ingresando a la función ProcessOrder ().Escribiendo en la consola: Registro de pedido recuperado.Escribiendo en la consola: faltan los detalles de la dirección del cliente en Branch DataBase.Escritura en archivo de registro: faltan los detalles de la dirección del cliente en la base de datos de la sucursal.Escribiendo en la consola: faltan los detalles de la dirección del cliente en la base de datos de la organización.Escribiendo en el archivo de registro: faltan los detalles de la dirección del cliente en la base de datos de la organización.Escribiendo en la consola: No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1.Envío por correo electrónico: No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1.Escribiendo en la consola: pedido enviado.Envío por correo electrónico: pedido enviado.
Ejemplo de Python
"" " Ejemplo de patrón de cadena de responsabilidad. " "" De abc import ABCMeta , método abstracto de enum import Enum , autoclass LogLevel ( Enum ): "" "Log Levels Enum." "" NINGUNO = auto () INFO = auto () DEBUG = auto () ADVERTENCIA = auto () ERROR = auto () FUNCTIONAL_MESSAGE = auto () FUNCTIONAL_ERROR = auto ( ) TODO = auto ()class Logger : "" "Manejador abstracto en cadena de patrón de responsabilidad." "" __metaclass__ = ABCMeta siguiente = Ninguno def __init__ ( self , niveles ) -> Ninguno : "" "Inicializar nuevo registrador. Argumentos: niveles (lista [str]): Lista de niveles de registro. "" " self . log_levels = [] para nivel en niveles : uno mismo . log_levels . añadir ( nivel ) def set_next ( self , next_logger : Logger ): "" "Establecer el siguiente registrador responsable en la cadena. Argumentos: next_logger (Logger): Siguiente registrador responsable. Devoluciones: Logger: Siguiente registrador responsable. "" " self . next = next_logger return self . next def message ( self , msg : str , severity : LogLevel ) -> None : "" "Controlador de escritura de mensajes. Argumentos: msg (str): Cadena de mensaje. severity (LogLevel): severidad del mensaje como enumeración de nivel de registro. "" " si LogLevel . ALL en self . log_levels o gravedad en self . log_levels : self . write_message ( msg ) si yo . el siguiente no es Ninguno : yo . siguiente . mensaje ( msg , gravedad ) @abstractmethod def write_message ( self , msg : str ) -> None : "" "Método abstracto para escribir un mensaje. Argumentos: msg (str): Cadena de mensaje. Raises: NotImplementedError "" " raise NotImplementedError ( " Debería implementar este método " ).class ConsoleLogger ( Logger ): def write_message ( self , msg : str ) -> None : "" "Anula el método abstracto de los padres para escribir en la consola. Argumentos: msg (str): Cadena de mensaje. "" " print ( " Escribiendo en la consola: " , msg )class EmailLogger ( Logger ): "" "Anula el método abstracto de los padres para enviar un correo electrónico. Argumentos: msg (str): Cadena de mensaje. "" " def write_message ( self , msg : str ) -> None : print ( f " Envío por correo electrónico: { msg } " )class FileLogger ( Logger ): "" "Anula el método abstracto de los padres para escribir un archivo. Argumentos: msg (str): Cadena de mensaje. "" " def write_message ( self , msg : str ) -> None : print ( f " Escribiendo en el archivo de registro: { msg } " )def main (): "" "Construyendo la cadena de responsabilidad." "" logger = ConsoleLogger ([ LogLevel . ALL ]) email_logger = logger . set_next ( EmailLogger ([ LogLevel . FUNCTIONAL_MESSAGE , LogLevel . FUNCTIONAL_ERROR ]) ) # Como no necesitamos usar la instancia del registrador de archivos en ningún otro lugar posterior # No estableceremos ningún valor para ello. email_logger . set_next ( FileLogger ([ LogLevel . WARNING , LogLevel . ERROR ]) ) # ConsoleLogger manejará esta parte del código ya que el mensaje # tiene un nivel de registro de todos los registradores . message ( "Ingresando a la función ProcessOrder ()." , LogLevel . DEBUG ) logger . mensaje ( "Registro de pedido recuperado" , LogLevel . INFO ) # ConsoleLogger y FileLogger manejarán esta parte ya que el archivo logger # implementos de advertencia y error registrador . message ( "Falta la información de la dirección del cliente en la base de datos de la sucursal" , LogLevel . ADVERTENCIA ) registrador . message ( "Falta la información de la dirección del cliente en la base de datos de la organización" , LogLevel . ERROR ) # ConsoleLogger y EmailLogger manejarán esta parte mientras implementan # registrador de errores funcionales . mensaje ( "No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1." , LogLevel . FUNCTIONAL_ERROR ) registrador . mensaje ( "Pedido enviado." , LogLevel . FUNCTIONAL_MESSAGE )if __name__ == "__main__" : main ()
Ejemplo de PHP
php Logger de clase abstracta { / ** * Indicadores de máscara de bits para la gravedad. * / public const NINGUNO = 0 ; INFO const pública = 0 b000001 ; public const DEBUG = 0 b000010 ; ADVERTENCIA const público = 0 b000100 ; public const ERROR = 0 b001000 ; public const FUNCTIONAL_MESSAGE = 0 b010000 ; public const FUNCTIONAL_ERROR = 0 b100000 ; const público TODOS = 0 b111111 ; / ** @var int Un indicador de máscara de bits de esta clase. * / protected int $ logMask ; / ** @var \ Logger | null Un siguiente registrador opcional para manejar el mensaje * / protected ? Registrador $ siguiente = nulo ; / ** * Constructor del registrador. * * @param int $ mask * Un indicador de máscara de bits de esta clase. * / función pública __construct ( int $ máscara ) { $ esto -> logMask = $ máscara ; } / ** * Establecer el siguiente registrador responsable en la cadena. * * @param \ Logger $ nextLogger * Siguiente registrador responsable. * * @return \ Logger * Logger: Siguiente registrador responsable. * / función pública setNext ( Logger $ nextLogger ) : Logger { $ this -> next = $ nextLogger ; return $ nextLogger ; } / ** * Manejador del escritor de mensajes. * * @param string $ msg * Cadena de mensaje. * @param int $ severity * Severidad del mensaje como una bandera de máscara de bits de esta clase. * * @return $ this * / mensaje de función pública ( string $ msg , int $ severity ) : Logger { if ( $ severity & $ this -> logMask ) { $ this -> writeMessage ( $ msg ); } if ( $ this -> next ! == null ) { $ this -> next -> message ( $ msg , $ severity ); } return $ this ; } / ** * Método abstracto para escribir un mensaje * * @param string $ msg * Cadena de mensaje. * / función protegida abstracta writeMessage ( string $ msg ) : void ; }class ConsoleLogger extiende Logger { función protegida writeMessage ( string $ msg ) : void { echo "Escribiendo en la consola: $ msg \ n " ; }}class EmailLogger extiende Logger { función protegida writeMessage ( string $ msg ) : void { echo "Envío por correo electrónico: $ msg \ n " ; }}class FileLogger extiende Logger { función protegida writeMessage ( string $ msg ) : void { echo "Escribiendo en un archivo de registro: $ msg \ n " ; }}$ logger = new ConsoleLogger ( Logger :: ALL ); $ logger -> setNext ( nuevo EmailLogger ( Logger :: FUNCTIONAL_MESSAGE | Logger :: FUNCTIONAL_ERROR )) -> setNext ( nuevo FileLogger ( Logger :: ADVERTENCIA | Logger :: ERROR ));$ logger // Manejado por ConsoleLogger ya que la consola tiene un nivel de registro de todos -> mensaje ( "Ingresando la función ProcessOrder ()." , Logger :: DEBUG ) -> message ( "Registro de pedido recuperado." , Logger :: INFO ) / / Manejado por ConsoleLogger y FileLogger ya que filelogger implementa Advertencia y Error -> mensaje ( "Falta la dirección del cliente en la base de datos de la sucursal" , Logger :: ADVERTENCIA ) -> mensaje ( "Falta la información de la dirección del cliente en la base de datos de la organización" , Logger :: ERROR ) // Manejado por ConsoleLogger y EmailLogger mientras implementa el error funcional -> mensaje ( "No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1." , Logger :: FUNCTIONAL_ERROR ) // Manejado por ConsoleLogger y EmailLogger -> mensaje ( "Pedido Enviado. " , Logger :: FUNCTIONAL_MESSAGE );/ * Escritura de salida a la consola: Ingresando la función ProcessOrder (). Escribiendo en la consola: Registro de pedido recuperado. Escribiendo en la consola: faltan los detalles de la dirección del cliente en la base de datos de la sucursal. Escribiendo en un archivo de registro: faltan los detalles de la dirección del cliente en la base de datos de la sucursal. Escribiendo en la consola: faltan los detalles de la dirección del cliente en la base de datos de la organización. Escribir en un archivo de registro: faltan los detalles de la dirección del cliente en la base de datos de la organización. Escribiendo en la consola: No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1. Envío por correo electrónico: No se puede procesar el pedido ORD1 con fecha D1 para el cliente C1. Escribiendo en la consola: pedido enviado. Envío por correo electrónico: pedido enviado. * /
Implementaciones
Toque de cacao y cacao
Los frameworks Cocoa y Cocoa Touch , usados para aplicaciones OS X e iOS respectivamente, usan activamente el patrón de cadena de responsabilidad para manejar eventos. Los objetos que participan en la cadena se denominan objetos respondedores , heredados de la clase NSResponder
(OS X) / UIResponder
(iOS). Todos los objetos de vista ( NSView
/ UIView
), los objetos de controlador de vista ( NSViewController
/ UIViewController
), los objetos de ventana ( NSWindow
/ UIWindow
) y el objeto de aplicación ( NSApplication
/ UIApplication
) son objetos de respuesta.
Normalmente, cuando una vista recibe un evento que no puede manejar, lo envía a su supervista hasta que alcanza el controlador de vista o el objeto de ventana. Si la ventana no puede manejar el evento, el evento se envía al objeto de la aplicación, que es el último objeto de la cadena. Por ejemplo:
- En OS X, se puede mover una ventana texturizada con el mouse desde cualquier ubicación (no solo la barra de título), a menos que en esa ubicación haya una vista que maneje eventos de arrastre, como controles deslizantes. Si no existe tal vista (o supervista), los eventos de arrastre se envían hacia arriba en la cadena a la ventana que maneja el evento de arrastre.
- En iOS, es típico manejar eventos de vista en el controlador de vista que administra la jerarquía de vista, en lugar de subclasificar la vista en sí. Dado que un controlador de vista se encuentra en la cadena de respuesta después de todas sus subvistas administradas, puede interceptar cualquier evento de vista y manejarlo.
Ver también
Referencias
- ^ "Copia archivada" . Archivado desde el original el 27 de febrero de 2018 . Consultado el 8 de noviembre de 2013 .CS1 maint: copia archivada como título ( enlace )
- ^ Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994). Patrones de diseño: elementos de software orientado a objetos reutilizable . Addison Wesley. págs. 223 y siguientes . ISBN 0-201-63361-2.CS1 maint: varios nombres: lista de autores ( enlace )
- ^ "El patrón de diseño de la Cadena de Responsabilidad - Problema, Solución y Aplicabilidad" . w3sDesign.com . Consultado el 12 de agosto de 2017 .
- ^ "El patrón de diseño de la Cadena de Responsabilidad - Estructura y Colaboración" . w3sDesign.com . Consultado el 12 de agosto de 2017 .