Introducción
En este artículo he decido escribir acerca de un patrón de diseño que
es muy poco conocido y que puede dejarte con sentimientos encontrados en cuanto
a que si conviene implementarlo o no, lo cual puede ser un indicador
para evaluar mejor si la aplicación que le darás es válida.
Trataré de explicarlo paso a paso con la idea que de pueda ser útil
para cualquier desarrollador principiante o no. Solo se requiere estar
familiarizado con el paradigma de la orientación a objetos.
Hace un par de años empecé a escribir una librería para incluirla en
uno de los proyectos que estaba trabajando para convertir expresiones de
consultas con entidades a sql y xml, es el mejor uso que le he dado a este
patrón desde que pude entender su utilidad y la relación que tenía con otros
frameworks que procesan estructuras o listas de objetos para generar un
resultado a partir de diferentes elementos de tipos de datos conocidos como por ejemplo las expressiones booleanas o las expressiones
lambda que escribimos en .Net.
Dicha librería utiliza entidades que contienen metadata de la
aplicación y para el problema que enfrento ahora necesito algo mucho más
sencillo lo cual nos reune aquí.
El código de los ejemplos están con sintaxis del lenguaje C# pero perfectamente
lo pueden aplicar en otros lenguajes como C++ o Java.
La definición
"En programación
orientada a objetos, el patrón visitor es una forma de separar el algoritmo de la estructura de un objeto". Perfecto! Lo dejamos hasta ahí por el momento.
El problema a resolver
El problema a resolver
Si partimos del típico ejemplo de las
figuras que definen su propia implementación del método dibujar heredado de una clase base “Figura” a como está a continuación:
public abstract class Figura
{
// Método para definir el código para dibujar la figura
concreta
public abstract void Dibujar();
}
public class Circulo : Figura
{
public override void Dibujar()
{
// Implementación aquí
}
}
public class Triangulo : Figura
{
public override void Dibujar()
{
// Implementación aquí
}
}
public class Cuadrado : Figura
{
public override void Dibujar()
{
// Implementación aquí
}
}
Aplicando los conceptos de orientación a objetos
como la abstracción, herencia y polimorfismo la tarea de dibujar cada figura se
ve resuelta. Pero que tal si la estructura de estas figuras inicialmente no
contaran con esa implementación y no se pudieran modificar por alguna razón por
ejemplo: porque no contamos con el código fuente o porque son operaciones que
realmente no deben ir dentro de esas clases.
Por lo tanto si quitamos el código del método dibujar de la clase
figura (la definición) y lo ubicamos en otra parte de nuestra aplicación podriamos tener algo
así (la forma menos aceptable por incluir la lógica
dentro del foreach):
static void Main(string[] args)
{
// Lista genérica de figuras
List<Figura> figuras = new List<Figura>()
{
new Circulo(),
new Triangulo(),
new Cuadrado(),
new Circulo()
};
// Iteración de los elementos de la lista
foreach (Figura figura in figuras)
{
if (figura is Circulo)
{
// Multiples líneas de código
// para
para dibujar el círculo
}
else
if (figura is Triangulo)
{
// Multiples líneas de código
// para para dibujar el
triángulo
}
else
if (figura is Cuadrado)
{
//
Multiples líneas de código
// para para dibujar el
cuadrado
}
}
}
Una implementación más decente supondría mover el código que dibuja
cada una de las figuras a métodos diferentes:
//
Iteración de los elementos de la lista
foreach (Figura figura in figuras)
{
if (figura is Circulo)
Dibujar((Circulo)figura);
else
if (figura is Triangulo)
Dibujar((Triangulo)figura);
else
if (figura is Cuadrado)
Dibujar((Cuadrado)figura);
}
Con los respectivos métodos:
private void Dibujar(Circulo figura)
{
// Implementación aquí
}
private void Dibujar(Triangulo figura)
{
// Implementación aquí
}
private void Dibujar(Cuadrado figura)
{
// Implementación aquí
}
Aún así nos quedamos con la verificación de cada uno de los tipos de
dato del objeto en curso “figura” por lo tanto es muy notable la dependencia
que hay hacia esas clases, he aquí donde el patrón visitor puede ser aplicado
para mejorar nuestro código, ojo con el hecho de que este patrón se aplica
mayormente a estructura de datos bien conocidas en donde no se estarán creando
nuevos tipos o por lo menos no como algo frecuente.
Los participantes del patrón
Ilustración de su interacción y relación
Visitor (Visitante): Una interfaz que define una operación de visita para cada uno de los elementos de la estructura.
Concrete Visitor (Visitante
concreto): Una clase con la implementación real de cada una de las operaciones
de visita.
Element: Elemento base de la
estructura que define una operación de aceptación de la visita.
Concrete Element: Elemento
concreto que implementa el código del método de aceptación pasando el flujo
hacia el Concrete Visitor
directamente al método correspondiente del elemento visitado.
La idea simple es invocar un método Accept en los elementos definidos pasandole como parámetro una instancia del Visitor y dentro de este método Accept invocar el método sobrecargado (VisitConcreteElmentX) pasándole como parámetro la referencia del elemento que está siendo visitado.
Si está un poco confuso veamos este escenario aplicado al mundo real:
Un veterinario tiene que ir a una granja a curar animales y por cada animal que visite: caballo, vaca, oveja él tiene que aplicar una cura diferente.
Un veterinario tiene que ir a una granja a curar animales y por cada animal que visite: caballo, vaca, oveja él tiene que aplicar una cura diferente.
Considerando en nuestro ejemplo al veterinario en general como la persona que cura a los animales tenemos:
Visitor: Hace referencia al veterinario con lo cual podemos decir: los veterinarios curan vacas, caballos y ovejas.
Concrete Visitor: Es el médico veterinario (Concrete Visitor 1) o el curandero empírico (Concrete Visitor 2).
Element: La estructura es acerca de animales por lo tanto es "Animal".
Concrete Element: Animal específico osea "Vaca", "Caballo" u "Oveja".
Aplicando el patrón
Retomando el ejemplo de las figuras tendríamos que hacer lo siguiente:
Paso 1: Crear la interfaz del visitor sobrecargando un método para todos
los tipos de datos de la estructura.
Nota: Es más común sobrecargar un método que utilizar nombres diferentes como en el diagrama.
public interface IVisitor
{
public abstract void Visit(Circulo figura);
public abstract void Visit(Cuadrado figura);
public abstract void Visit(Triangulo figura);
}
Paso 2: Definir un método abstracto en la clase base de los elementos e
implementarlo en las clases derivadas
aceptando la visita del visitor.
public abstract class Figura
{
public abstract void Accept(IVisitor visitor);
}
public class Circulo : Figura
{
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
public class Cuadrado : Figura
{
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
public class Triangulo : Figura
{
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
Paso 3: Definir la clase visitor que tendrá la implementación concreta.
public class DibujanteVisitor : IVisitor
{
public virtual void Visit(Circulo figura)
{
// Implementar código aquí
}
public virtual void Visit(Cuadrado figura)
{
// Implementar código aquí
}
public virtual void Visit(Triangulo figura)
{
// Implementar código aquí
}
}
Paso 4: Refactorizar nuestro código para hacer uso de las nuevas clases.
static void Main(string[] args)
static void Main(string[] args)
{
// Lista genérica de figuras
List<Figura> figuras = new List<Figura>()
{
new Circulo(),
new Triangulo(),
new Cuadrado(),
new Circulo()
};
DibujanteVisitor visitor
= new DibujanteVisitor();
// Iteración de los elementos de la lista
foreach (Figura figura in figuras)
{
figura.Accept(visitor);
}
}
Y voilà! Ahora nuestro método ya no necesita conocer las clases Triángulo, Cuadrado y Círculo y así eliminamos esas dependencia y resolvemos el problema de una forma elegante.
Lo que sigue en la “Parte II”
La meta es crear un pequeño Api (Expression Builder) para escribir
expresiones como el código abajo y generar las consultas dinámicamente utilizando
el patrón visitor, la motivación es un escenario real que se justificará con la
siguiente publicación, aquí un ejemplo de lo que buscamos lograr:
EntityExpression expression = new EntityExpression("Entity");
expression.Select("Field1", "Field2", "Field3", "Field4");
expression.Where("Field1", ConditionOperator.Equals,
"Value X");
Llegamos al final
Mientras se desarrolla la siguiente parte te gustaría compartir tus opiniones
acerca de esto:
¿Consideras que este patrón entra en conflicto con otros principios
del paradigma orientado a objetos?
¿Hay otro patrón o método propio que utilizarías para el ejemplo de las
figuras?
Pueden dejar sus comentarios abajo y/o subscribirse al blog para tener
la notificación de la segunda parte y también mientras tanto los invito a
compartirlo.
No hay comentarios:
Publicar un comentario