Design Patterns - Visitor

Propósito

El patrón Visitor (Visitante) permite agregar nuevas operaciones sobre una estructura de objetos, sin cambiar las clases de los elementos sobre los que opera.

Problema

El patrón Visitor resuelve la dificultad de añadir nuevas operaciones o comportamientos a estructuras de objetos complejas, sin modificarlas.

Solución

La solución viene mediante un nuevo objeto, el “Visitor“, que contiene las operaciones a realizar.

Las clases de objetos a visitar aceptan al Visitor y le delegan la ejecución de la operación.

Esto permite añadir funcionalidades específicas sin modificar las clases de los objetos.

Estructura

visitor

Participantes

  • Visitor (Interface): Define una operación para cada clase de ConcreteElement en la estructura de objetos.
  • ConcreteVisitor: Implementa cada operación declarada por Visitor. Cada operación implementa el fragmento del algoritmo correspondiente al elemento visitado.
  • Element (Interface): Define un método accept que recibe al visitor.
  • ConcreteElement: Implementa el método accept.

Cuándo Usarlo

Este patrón es recomendable cuando:

  • hay estructuras de objetos complejas y debemos realizar operaciones sobre ellos que no son necesariamente pertinentes a sus clases.
  • es probable que se añadan nuevas operaciones en el futuro y no se desea modificar las clases de los elementos.

Ventajas

Separación de Responsabilidades: Separa las operaciones, de los objetos sobre los que operan.

Extensibilidad: Facilita la adición de nuevas operaciones sin cambiar las clases de los elementos.

Agrupación de Operaciones: Permite agrupar operaciones relacionadas.

Desventajas

Rompe el Encapsulamiento: Los Visitors necesitan acceso a los elementos internos de las clases que visitan, lo que puede romper el encapsulamiento.

Complejidad: Añade una capa de complejidad al diseño, especialmente en la interacción entre los Visitors y los elementos.

Dificultades con Estructuras Cambiantes: Si la estructura de los elementos cambia seguido, es costoso mantener los Visitors actualizados.

Ejemplo: Sistema de Contabilidad con Reportes Financieros Diversificados

Debemos desarrollar un sistema de contabilidad para una firma que maneja una gran variedad de cuentas y transacciones financieras.

Este sistema necesita ser capaz de generar distintos tipos de reportes financieros, como balances generales, estados de flujo de efectivo, y análisis de costos para diferentes tipos de cuentas y productos financieros.

Problema

El principal desafío es estructurar el sistema para que pueda interactuar con una variedad de cuentas y productos financieros para generar diferentes tipos de reportes, sin que el código se vuelva monolítico y difícil de mantener.

A medida que la firma crece y las regulaciones cambian, también deberemos agregar rápidamente nuevos tipos de reportes y manejar nuevos tipos de productos financieros, sin tener que alterar la base de código existente.

Solución planteada

Elegimos el patrón Visitor porque nos permite separar las operaciones (los reportes) de las estructuras de objetos (las cuentas y productos financieros) sobre las que operan.

Con Visitor, podemos añadir nuevas operaciones sin cambiar las clases de los elementos que visitamos. Esto hace que nuestro sistema sea más fácil de mantener y expandir, permitiendo a los desarrolladores agregar nuevos tipos de reportes y manejar nuevos productos financieros, sin modificar las clases existentes.

Definimos la interfaz Element, Account, y el Visitor, AccountVisitor. Los elementos concretos, los tipos de cuenta. El visitor concreto, FinancialReportVisitor, para generar reportes. Y finalmente el Cliente, que instancia los objetos para generar el reporte.

visitor

Código Java

Codificamos en Java lo que preparamos en el diagrama.

Definimos la Interfaz Element:


              
  interface Account {
      void accept(AccountVisitor visitor);
  }
  
              

Definimos la Interfaz Visitor:


  
  interface AccountVisitor {
      void visit(SavingsAccount account);
      void visit(LoanAccount account);
  }
              
              

Creamos las clases ConcreteElement:


              
  class SavingsAccount implements Account {
      double balance;
      
      SavingsAccount(double balance) {
          this.balance = balance;
      }
  
      public void accept(AccountVisitor visitor) {
          visitor.visit(this);
      }
  }
  
  class LoanAccount implements Account {
      double owedAmount;
      
      LoanAccount(double owedAmount) {
          this.owedAmount = owedAmount;
      }
  
      public void accept(AccountVisitor visitor) {
          visitor.visit(this);
      }
  }
              
              

Creamos las clases ConcreteVisitor:


              
  class FinancialReportVisitor implements AccountVisitor {
      // Acumuladores para datos del reporte
      double totalSavings;
      double totalLoans;
  
      public void visit(SavingsAccount account) {
          totalSavings += account.balance;
      }
  
      public void visit(LoanAccount account) {
          totalLoans += account.owedAmount;
      }
  
      void displayReport() {
          System.out.println("Total Savings: " + totalSavings);
          System.out.println("Total Loans: " + totalLoans);
      }
  }
  
              

El código cliente:


              
  public class AccountingSystem {
      public static void main(String[] args) {
          Account[] accounts = new Account[]{
              new SavingsAccount(1000),
              new LoanAccount(500)
              // Más cuentas
          };
  
          FinancialReportVisitor reportVisitor = new FinancialReportVisitor();
  
          for(Account account : accounts) {
              account.accept(reportVisitor);
          }
  
          reportVisitor.displayReport();
      }
  }
  
              

Mapeo (del ejemplo a Participantes)

Los participantes que vimos antes son: Visitor, ConcreteVisitor, Element, ConcreteElement, Client:

  • Account (Element): Define el método accept que los elementos concretos implementarán para aceptar visitantes.
  • AccountVisitor (Visitor): Define las operaciones que se realizan en los elementos concretos SavingsAccount y LoanAccount.
  • SavingsAccount, LoanAccount (ConcreteElement): Implementan la interfaz Account. Definen la forma en que un visitante es aceptado y cómo interactúa con ellos.
  • FinancialReportVisitor (ConcreteVisitor): Visitante concreto que realiza cálculos financieros en cuentas de ahorro y préstamos, y acumula los datos para el reporte.
  • AccountingSystem (Client): Usa los elementos concretos (SavingsAccount y LoanAccount) y el visitante concreto (FinancialReportVisitor) para realizar y mostrar un informe financiero. Es responsable de crear los elementos y visitantes, y de iniciar la interacción entre ellos.

Conclusiones

La solución que conseguimos con el patrón Visitor fue eficaz y flexible. Al separar los reportes de las estructuras de las cuentas, hemos logrado que el sistema pueda adaptarse y expandirse fácilmente para incluir nuevos tipos de reportes y productos financieros.

Introduce cierta complejidad al requerir una serie de clases e interfaces adicionales, pero los beneficios al lograr que el sistema sea más fácil de mantener y expandir, lo hacen ideal para sistemas de este estilo, que requieren una gran variedad de operaciones sobre estructuras de datos complejas y en constante evolución.

Patrones relacionados

  • Composite

El patrón Visitor puede ser usado para aplicar una operación sobre un objeto Composite.

  • Interpreter

Visitor puede ser usado para interpretar una estructura de objetos, como un árbol de sintaxis.