Chain of Responsibility

Descripción general:

El patrón Chain of Responsibility [Cadena de responsabilidad] es un patrón de diseño de programación orientada a objetos (OOP) común que desacopla al remitente de una solicitud del receptor al permitir que uno o varios objetos de controlador controlen la solicitud. Un controlador puede pasar la solicitud al siguiente hasta que un objeto de la cadena pueda controlar la solicitud.

Chain of Responsibility - Definicion:

El patrón Chain of Responsibility [Cadena de responsabilidad] es un patrón de diseño de programación orientada a objetos (OOP) común que desacopla al remitente de una solicitud del receptor al permitir que uno o varios objetos de controlador controlen la solicitud. Un controlador puede pasar la solicitud al siguiente hasta que un objeto de la cadena pueda controlar la solicitud.

Diagrama de clases UML

texto alternativo-
Diagrama Uml

Participantes

Las clases y objetos que participannen en este patron son:

Ejemplo

Estudia el codigo siguiente

Public Class PruebaConcepto

    ''' <summary>
    ''' Este es el valor que se genera por la cadena que lo atrapa
    ''' </summary>
    Public ValorDeRetorno As New System.Windows.Controls.ValidationResult(True, Nothing)

    Public Function Text(ByVal ClaveTareaARealizar As String)

        Select Case ClaveTareaARealizar
            Case Is = "uno"
                ValorDeRetorno = New System.Windows.Controls.ValidationResult(True, "Uno")
            Case Is = "dos"
                ValorDeRetorno = New System.Windows.Controls.ValidationResult(True, "dos")
            Case Is = "tres"
                ValorDeRetorno = New System.Windows.Controls.ValidationResult(True, "tres")
            Case Else
                ValorDeRetorno = New System.Windows.Controls.ValidationResult(False, "Tarea no realizada")
        End Select

        Return ValorDeRetorno

    End Function

End Class

Este tipo de codigo es un codigo bastante limpio, pero si la tarea a realizar en cada opcion es extensa, o bien hay muchas opciones posibles se enmaraña y deja de ser claro y sencillo

Una forma de resolver este problema es usar el patron chain of responsability. La idea es colocar todas las tareas que se realizen dentro de nuestro hipotetico y muy abultado, [select case] en un objeto

A continuacion defino una estrututa de datos del tipo [Lista simple enlazada], (Wikipedia Lista enlazada) en la que cada objeto mantiene un puntero (una referenacia) a su sucesor (a su hijo

texto alternativo-
Wikipedia Lista enlazada

Cada eslabon de la lista enlazada es, como habras adivinado, nuestro[ConcreteHandler]

Cada eslabon tiene el codigo necesario para resolver un problema, y si no puede hacerlo, sabe quien es su hijo y le pasa el problema para que lo resuelva el, y asi hasta el final. Cuando una clase eslabon, resuleve el problema, devuelve la solucion a su padre, y asi sucesivamente hasta que el resultado llega al primer elemento de la cadena, que lo devuelve al cliente.

Un perfecccionamiento que podemos añadir a nuestro codigo es usar los llamados Nodos centinelas. (segun dice la Wikipedia) A veces las listas enlazadas tienen un nodo centinela (también llamado falso nodo o nodo ficticio) al principio o al final de la lista. Su propósito no es guardar datos, sino simplificar o agilizar algunas operaciones, asegurando que cualquier nodo tiene otro anterior o posterior, y que toda la lista (incluso alguna que no contenga datos) siempre tenga un "primer y un último" nodo.

Hay que resolver otros problemas:

¿Como sabe cada eslabon que es él, el que tiene que resolver el problema? , hay varias soluciones, pero la mas sencilla es imitar el comportamiento del [select case] que nos sirve de ejemplo conceptual inicial, cada opcion de la estructrura [Select Case], se identifica por medio de un valor, algo asi como si vale uno entonces hago la tarea. Podemos habilitar varios metodos, pero el mas sencilla es usar una clave que identifica al eslabon y por lo tanto a la tarea que realiza cada eslabon.

¿Como paso los datos de trabajo?. Evidentemente, para hacer aualquier tarea (por ejemplo una suma), el eslabon tiene que recibir los datos de trabajo, y tambien hay que definir claramente como y en que formato se devuelve la respuesta.

Resumiendo.

''' <summary>
'''  Esta clase Base que define un eslabón. Es la clase [Handler] del diagrama
''' </summary>
Public MustInherit Class EslabonCadenaConcreto
    Implements IDisposable


#Region "Propiedades y variables de la clase"

    ''' <summary>
    ''' Referencia al siguiente eslabón de la cadena
    ''' </summary>
    Public Property SiguienteEslabon As EslabonCadenaConcreto = Nothing

    ''' <summary>
    ''' Los datos de trabajo que circulan entre eslabones.
    ''' </summary>
    Public Property DatosTrabajoMensaje As MensajeVo = Nothing

#End Region


#Region "Constructores"

    ''' <summary>
    '''  Constructor estándar. Inicializa una nueva instancia de la clase 
    ''' </summary>
    Public Sub New()
        '
    End Sub

    ''' <summary>
    '''  Constructor estándar. Inicializa una nueva instancia de la clase 
    '''  y define quien es el nodo hijo
    ''' </summary>
    Public Sub New(ByVal paramSiguienteEslabon As EslabonCadenaConcreto)
        Me.SiguienteEslabon = paramSiguienteEslabon
    End Sub

#End Region

    ''' <summary>
    '''   Permite establecer el siguiente eslabón de la cadena
    ''' </summary>
    ''' <param name=paramSiguienteEslabon">"
    '''   Un objeto EslabonCadenaConcreto que es el siguiente nodo de la cadena, el nodo hijo
    ''' </param>
    Public Sub EstablecerSiguienteEslabon(ByVal paramSiguienteEslabon As EslabonCadenaConcreto)
        Me.SiguienteEslabon = paramSiguienteEslabon
    End Sub

#Region "Miembros [MustOverride] de la clase "

    ''' <summary>
    '''  La clave del nodo.
    '''  Debe ser declarada por cada nodo hijo, por cada eslabón
    ''' </summary>
    Public MustOverride ReadOnly Property Key As String

    ''' <summary>
    '''  La función que hace el trabajo
    '''  Debe ser declarada por cada nodo hijo, por cada eslabón
    ''' </summary>
    ''' <param name=paramObjUnMensaje">"
    '''   El objeto [MensajeVo] que contiene los datos iniciales para hacer el trabajo
    '''   Se pasa [ByRef] porque tiene que visitar varios objetos y así evito que se hagan copias inútiles del mismo  
    ''' </param>
    ''' <returns>Un objeto [RespuestaVo] que contiene la respuesta a la tarea encargada </returns>
    Public MustOverride Function ExecuteTarea(ByRef paramObjUnMensaje As MensajeVo) As RespuestaVo

#End Region



#Region "IDisposable Support.   [Para clases normales y bases ] Implementación de .NET Version: [2020/04/29]"

    ''' <summary>
    '''  Variable local para detectar llamadas redundantes que indica [True] el objeto se ha eliminado, [False], en caso contrario. 
    ''' </summary>
    Private campoDisposedValue As System.Boolean

    ''' <summary>
    '''     Obtiene un valor que indica si el objeto se ha eliminado. 
    ''' </summary>
    ''' <value>
    '''   <para>Tipo: <see cref=System.Boolean>Boolean</see></para>
    '''   <para>Es true si objeto se ha eliminado; en caso contrario, es false.</para>
    ''' </value>
    Public Overloads ReadOnly Property IsDisposed() As System.Boolean
        Get
            Return campoDisposedValue
        End Get
    End Property


    ''' <summary>
    '''  [Dispose] Código de limpieza de la clase
    ''' </summary>
    ''' <param name=paramQuienMeLLama>[True], llama el usuario, [False], llama el sistema</param>
    ''' <remarks>
    '''  <para>
    '''     Dispose (bool quienMeLLama) se ejecuta en dos escenarios distintos.
    '''  </para>
    '''  <para>
    '''     Si [quienMeLLama] es igual a true, el método ha sido llamado 
    '''     directamente, o indirectamente, por el código de un usuario. 
    '''     Los recursos administrados y no administrados se pueden eliminar.
    '''  </para>
    '''  <para>
    '''     Si [quienMeLLama] es igual a false, el método ha sido llamado por el
    '''     [runtime] desde el interior del finalizador y usted no debe hacer 
    '''     referencia a otros objetos, ni siquiera para eliminarlos.
    '''     Solo los recursos no administrados pueden ser eliminados.
    '''  </para>
    ''' </remarks>
    Protected Overridable Sub Dispose(paramQuienMeLLama As System.Boolean)
        If Me.campoDisposedValue = False Then
            Me.campoDisposedValue = True
            If paramQuienMeLLama Then
                '------------------------------------------------------------------------------------------
                ' 1) Desechar objetos SI administrados.
                If Not (SiguienteEslabon Is Nothing) Then
                    SiguienteEslabon = Nothing
                End If
                If Not (DatosTrabajoMensaje Is Nothing) Then
                    DatosTrabajoMensaje = Nothing
                End If

            End If
            '------------------------------------------------------------------------------------------
            ' 2) Liberar objetos NO administrados.
            '------------------------------------------------------------------------------------------
            ' 3) Establecer campos grandes como Null.
            '------------------------------------------------------------------------------------------
        End If
    End Sub


    ''' <summary>
    '''  [Dispose] Código de limpieza de la clase
    ''' </summary>
    ''' <remarks>
    '''  Visual Basic agregó este código para implementar correctamente el patrón descartable.
    '''  No cambie este código. Coloque el código de limpieza en Dispose(disposing As Boolean).
    ''' </remarks>
    Public Sub Dispose() Implements IDisposable.Dispose
        Me.Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

#End Region



End Class

Una clase [Eslabon concreto]

Por ejemplo, la clase eslabón que hace la suma

''' <summary>
''' Realiza la tarea de la suma
''' </summary>
Public Class EslabonSuma
    Inherits EslabonCadenaConcreto

#Region "Miembros [MustOverride] de la clase base"

    ''' <summary>
    '''   La clave que identifica a este eslabón
    ''' </summary>
    Public Overrides ReadOnly Property Key As String
        Get
            Return "Suma"
        End Get
    End Property

    ''' <summary>
    ''' La Función que hace el trabajo
    ''' o bien, llama al siguiente eslabón
    ''' </summary>
    Public Overrides Function ExecuteTarea(ByRef paramObjUnMensaje As MensajeVo) As RespuestaVo

        If String.Equals(paramObjUnMensaje.Key.ToUpper, Me.Key.ToUpper) Then
            ' Este eslabón es el que tiene que hacer el trabajo
            Return HacerLaTareaDelEslabon(paramObjUnMensaje)
        Else
            ' Pasar el problema al siguiente eslabón de la cadena
            Return Me.SiguienteEslabon.ExecuteTarea(paramObjUnMensaje)
        End If
    End Function

#End Region



#Region "HacerLaTareaDelEslabon"

    ''' <summary>
    ''' Función que hace el trabajo
    ''' </summary>
    ''' <param name=paramObjUnMensaje>El objeto [MensajeVo] que contiene los datos iniciales para hacer el trabajo</param>
    ''' <returns>Un objeto [RespuestaVo] que contiene la respuesta a la tarea encargada </returns>
    Private Shared Function HacerLaTareaDelEslabon(ByRef paramObjUnMensaje As MensajeVo) As RespuestaVo

        Dim localObjResultado As New RespuestaVo
        localObjResultado.Resultado = paramObjUnMensaje.DatoUno + paramObjUnMensaje.DatoDos

        Return localObjResultado
    End Function

#End Region

End Class

'*******************************

La clase [MensajeVo]

Esta es la clase que se pasa entre los distintos eslabones y que contiene la informacion de entrada neceasaria poarahacer le trabajo, asi como la clave (Key) que identifica al eslabón que tiene que hacer el trabajo

''' <summary>
''' Esta es la clase que se pasan los eslabones
''' Contiene los datos para hacer el trabajo
''' y la clave que identifica al eslabón que tiene que hacer el trabajo
''' </summary>
Public Class MensajeVo

    Public Sub New()
        ' no hacer nada
    End Sub
    '----------------------------------------------------------------
    ' En este ejemplo supongo que quiero hacer una operación matemática
    ' suma, resta, multiplicación, etc
    ' En este objeto, están los dos datos de trabajo
    ' También esta la clave, el identificador del eslabón que tiene que hacer el trabajo
    '----------------------------------------------------------------

    ''' <summary>
    '''  La clave del nodo.
    ''' </summary>
    Public Property Key As String


    ' Uno de los números con los que se trabaja
    Public Property DatoUno As Integer
    ' El otro número
    Public Property DatoDos As Integer

End Class

'*******************************

La case que encapsula todo el proceso

''' <summary>
''' Este es el objeto que inicia el proceso
''' </summary>
Public Class Chain_Inicial
    Implements System.IDisposable

    ' '---------------------
    ' Este es el Objeto que llama el cliente que quiere hacer uso del patrón Chain
    ' Se encarga de:
    ' a) Definir todos los objetos de la cadena
    ' b) Dirigir la pregunta del cliente al primer eslabón de la misma
    ' c) También es el que devuelve el resultado al cliente

    ' Una observación
    ' Esta clase es la que mantiene la referencia a todos los objetos que componen los eslabones
    ' si va a usarse muchas veces solo debería instanciarse una vez en la aplicación
    ' porque hay un coste (importante) en la declaración y en la eliminación de los objetos.
    '
    ' Este proceso puede realizarse de muchas formas, pero esta es la que me parece mas sensata porque 
    ' todo el proceso de instanciación de objetos, definición de relaciones y montaje de la cadena y la
    ' eliminación de los mismos [Dispose], esta todo junto y es mas fácil de mantener
    ' Por otra parte, funciona a modo de un objeto [Facade], ocultando toda la estructura de clases,
    ' y mostrando únicamente al cliente, la función a la que tiene que llamar [ExecuteTarea]

    ' '---------------------
    ' Forma de uso
    ' '---------------------
    '  ' A) Definición de objetos
    '    Dim localObjChain As New Chain_Inicio
    '    Dim localObjMensajeVo As New MensajeVo
    '    Dim localObjRespuestaVo As New RespuestaVo
    ' '---------------------
    '  ' B) Carga de los valores de trabajo
    '    localObjMensajeVo.DatoUno = 90
    '    localObjMensajeVo.DatoDos = 5

    '  '---------------------
    '  ' C) Operación a realizar
    '    localObjMensajeVo.Key = "suma"
    '  ' D) Iniciar la tarea de la cadena y obtener el resultado
    '    localObjRespuestaVo = localObjChain.ExecuteTarea(localObjMensajeVo)
    '------------------
    ' /Eof forma de uso
    '------------------


    '---------------------------------------------------------
    '  ' A) Definición de objetos
    Private campoEslabonSuma As EslabonSuma
    Private campoEslabonResta As EslabonResta
    Private campoEslabonMultiplicacion As EslabonMultiplicacion
    Private campoEslabonDivision As EslabonDivision
    Private campoNodoCentinela As Eslabon_Centinela


    Public Sub New()
        '----------------------------
        ' Instanciar los eslabones
        campoEslabonSuma = New EslabonSuma
        campoEslabonResta = New EslabonResta
        campoEslabonMultiplicacion = New EslabonMultiplicacion
        campoEslabonDivision = New EslabonDivision
        campoNodoCentinela = New Eslabon_Centinela
        '----------------------------
        ' Decir quien es el siguiente elemento de la cadena
        campoEslabonSuma.EstablecerSiguienteEslabon(campoEslabonResta)
        campoEslabonResta.EstablecerSiguienteEslabon(campoEslabonMultiplicacion)
        campoEslabonMultiplicacion.EstablecerSiguienteEslabon(campoEslabonDivision)
        campoEslabonDivision.EstablecerSiguienteEslabon(campoNodoCentinela)


        ' En el nodo division no hace falta decir quien es el hijo,
        ' porque al crearlo, se coloca automáticamente el nodo centinela

    End Sub


    Public Function ExecuteTarea(ByRef paramObjUnMensaje As MensajeVo) As RespuestaVo

        ' Pasar el problema al PRIMER  eslabón de la cadena
        Return Me.campoEslabonSuma.ExecuteTarea(paramObjUnMensaje)

    End Function


#Region "IDisposable Support.   [Para clases normales y bases ] Implementación de .NET Version: [2020/04/29]"

    ''' <summary>
    '''  Variable local para detectar llamadas redundantes que indica [True] el objeto se ha eliminado, [False], en caso contrario. 
    ''' </summary>
    Private campoDisposedValue As System.Boolean

    ''' <summary>
    '''     Obtiene un valor que indica si el objeto se ha eliminado. 
    ''' </summary>
    ''' <value>
    '''   <para>Tipo: <see cref=System.Boolean>Boolean</see></para>
    '''   <para>Es true si objeto se ha eliminado; en caso contrario, es false.</para>
    ''' </value>
    Public Overloads ReadOnly Property IsDisposed() As System.Boolean
        Get
            Return campoDisposedValue
        End Get
    End Property


    ''' <summary>
    '''  [Dispose] Código de limpieza de la clase
    ''' </summary>
    ''' <param name=paramQuienMeLLama">[True], llama el usuario, [False], llama el sistema</param>"
    ''' <remarks>
    '''  <para>
    '''     Dispose (bool quienMeLLama) se ejecuta en dos escenarios distintos.
    '''  </para>
    '''  <para>
    '''     Si [quienMeLLama] es igual a true, el método ha sido llamado 
    '''     directamente, o indirectamente, por el código de un usuario. 
    '''     Los recursos administrados y no administrados se pueden eliminar.
    '''  </para>
    '''  <para>
    '''     Si [quienMeLLama] es igual a false, el método ha sido llamado por el
    '''     [runtime] desde el interior del finalizador y usted no debe hacer 
    '''     referencia a otros objetos, ni siquiera para eliminarlos.
    '''     Solo los recursos no administrados pueden ser eliminados.
    '''  </para>
    ''' </remarks>
    Protected Overridable Sub Dispose(paramQuienMeLLama As System.Boolean)
        If Me.campoDisposedValue = False Then
            Me.campoDisposedValue = True
            If paramQuienMeLLama Then
                '------------------------------------------------------------------------------------------
                ' 1) Desechar objetos SI administrados.
                If Not (campoEslabonSuma Is Nothing) Then
                    campoEslabonSuma.Dispose()
                    campoEslabonSuma = Nothing
                End If
                If Not (campoEslabonResta Is Nothing) Then
                    campoEslabonResta.Dispose()
                    campoEslabonResta = Nothing
                End If
                If Not (campoEslabonMultiplicacion Is Nothing) Then
                    campoEslabonMultiplicacion.Dispose()
                    campoEslabonMultiplicacion = Nothing
                End If
                If Not (campoEslabonDivision Is Nothing) Then
                    campoEslabonDivision.Dispose()
                    campoEslabonDivision = Nothing
                End If
                If Not (campoNodoCentinela Is Nothing) Then
                    campoNodoCentinela.Dispose()
                    campoNodoCentinela = Nothing
                End If
            End If
            '------------------------------------------------------------------------------------------
            ' 2) Liberar objetos NO administrados.
            ' 3) Establecer campos grandes como Null.
            '------------------------------------------------------------------------------------------
        End If
    End Sub


    ''' <summary>
    '''  [Dispose] Código de limpieza de la clase
    ''' </summary>
    ''' <remarks>
    '''  Visual Basic agregó este código para implementar correctamente el patrón descartable.
    '''  No cambie este código. Coloque el código de limpieza en Dispose(disposing As Boolean).
    ''' </remarks>
    Public Sub Dispose() Implements IDisposable.Dispose
        Me.Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub
#End Region


End Class

'******************

Ejemplo

Tienes un ejemplo (En formato Zip) en el siguiente [Enlace] con el codigo Md5Hash [2E3C890EDA01047B83B51F34B480EE7C]