java - pattern - El propósito del patrón de visitante con ejemplos
visitor pattern (6)
Creo que el objetivo principal del patrón de visitantes es su gran extensibilidad. La intuición es que has comprado un robot. El robot ya ha implementado funcionalidades básicas como ir adelante, girar a la izquierda, girar a la derecha, retroceder, elegir algo, hablar una fase, ...
Un día, quieres que tu robot pueda ir a la oficina de correos por ti. Con todas estas funciones elementales, puede funcionar, pero debe llevar su robot a la tienda y "actualizar" su robot. El vendedor de la tienda no necesita modificar el robot, sino simplemente ponerle un nuevo chip de actualización a su robot y puede hacer lo que quiera.
Un día más, quieres que tu robot vaya al supermercado. El mismo proceso, debe llevar su robot a la tienda y actualizar esta funcionalidad "avanzada". No es necesario modificar el robot en sí.
y así …
De modo que la idea del patrón Visitor es que, dadas todas las funcionalidades elementales implementadas, puede usar el patrón de visitante para agregar un número infinito de funcionalidades sofisticadas. En el ejemplo, el robot es su clase de trabajadores, y el "chip de actualización" son los visitantes. Cada vez que necesita una nueva "actualización" de funcionalidad, no modifica su clase de trabajador, pero agrega un visitante.
Esta pregunta ya tiene una respuesta aquí:
- ¿Cuándo debería usar el patrón de diseño de visitante? 20 respuestas
Estoy realmente confundido sobre el patrón de visitante y sus usos. Realmente no puedo visualizar los beneficios de usar este patrón o su propósito. Si alguien pudiera explicar con ejemplos si es posible, sería genial.
Ejemplo de patrón de visitante. Libro, Fruta y Vegetales son elementos básicos de tipo "Visitable" y hay dos "Visitantes" , BillingVisitor y OfferVisitor cada uno de los visitantes tiene su propio propósito. Al calcular el proyecto de ley y algo para calcular las ofertas en estos elementos se encapsula en el visitante respectivo y los visitables (elementos) siguen siendo los mismos.
import java.util.ArrayList;
import java.util.List;
public class VisitorPattern {
public static void main(String[] args) {
List<Visitable> visitableElements = new ArrayList<Visitable>();
visitableElements.add(new Book("I123",10,2.0));
visitableElements.add(new Fruit(5,7.0));
visitableElements.add(new Vegetable(25,8.0));
BillingVisitor billingVisitor = new BillingVisitor();
for(Visitable visitableElement : visitableElements){
visitableElement.accept(billingVisitor);
}
OfferVisitor offerVisitor = new OfferVisitor();
for(Visitable visitableElement : visitableElements){
visitableElement.accept(offerVisitor);
}
System.out.println("Total bill " + billingVisitor.totalPrice);
System.out.println("Offer " + offerVisitor.offer);
}
interface Visitor {
void visit(Book book);
void visit(Vegetable vegetable);
void visit(Fruit fruit);
}
//Element
interface Visitable{
public void accept(Visitor visitor);
}
static class OfferVisitor implements Visitor{
StringBuilder offer = new StringBuilder();
@Override
public void visit(Book book) {
offer.append("Book " + book.isbn + " discount 10 %" + " /n");
}
@Override
public void visit(Vegetable vegetable) {
offer.append("Vegetable No discount /n");
}
@Override
public void visit(Fruit fruit) {
offer.append("Fruits No discount /n");
}
}
static class BillingVisitor implements Visitor{
double totalPrice = 0.0;
@Override
public void visit(Book book) {
totalPrice += (book.quantity * book.price);
}
@Override
public void visit(Vegetable vegetable) {
totalPrice += (vegetable.weight * vegetable.price);
}
@Override
public void visit(Fruit fruit) {
totalPrice += (fruit.quantity * fruit.price);
}
}
static class Book implements Visitable{
private String isbn;
private double quantity;
private double price;
public Book(String isbn, double quantity, double price) {
this.isbn = isbn;
this.quantity = quantity;
this.price = price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
static class Fruit implements Visitable{
private double quantity;
private double price;
public Fruit(double quantity, double price) {
this.quantity = quantity;
this.price = price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
static class Vegetable implements Visitable{
private double weight;
private double price;
public Vegetable(double weight, double price) {
this.weight = weight;
this.price = price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
}
Entonces probablemente hayas leído un montón de explicaciones diferentes sobre el patrón de visitante, y probablemente aún estés diciendo "¡pero cuándo lo usarías!"
Tradicionalmente, los visitantes se utilizan para implementar pruebas de tipo sin sacrificar la seguridad de tipo, siempre y cuando sus tipos estén bien definidos por adelantado y conocidos con anticipación. Digamos que tenemos algunas clases de la siguiente manera:
abstract class Fruit { }
class Orange : Fruit { }
class Apple : Fruit { }
class Banana : Fruit { }
Y digamos que creamos una Fruit[]
:
var fruits = new Fruit[]
{ new Orange(), new Apple(), new Banana(),
new Banana(), new Banana(), new Orange() };
Quiero dividir la lista en tres listas, cada una contiene naranjas, manzanas o plátanos. ¿Como lo harias? Bueno, la solución fácil sería una prueba de tipo:
List<Orange> oranges = new List<Orange>();
List<Apple> apples = new List<Apple>();
List<Banana> bananas = new List<Banana>();
foreach (Fruit fruit in fruits)
{
if (fruit is Orange)
oranges.Add((Orange)fruit);
else if (fruit is Apple)
apples.Add((Apple)fruit);
else if (fruit is Banana)
bananas.Add((Banana)fruit);
}
Funciona, pero hay muchos problemas con este código:
- Para empezar, es feo.
- No es seguro para tipos, no captaremos errores de tipo hasta el tiempo de ejecución.
- No es mantenible. Si agregamos una nueva instancia derivada de Fruit, debemos realizar una búsqueda global para cada lugar que realice una prueba de tipo de fruta, de lo contrario, podríamos omitir los tipos.
El patrón de visitante resuelve el problema elegantemente. Comience por modificar nuestra clase base de frutas:
interface IFruitVisitor
{
void Visit(Orange fruit);
void Visit(Apple fruit);
void Visit(Banana fruit);
}
abstract class Fruit { public abstract void Accept(IFruitVisitor visitor); }
class Orange : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Apple : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Banana : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
Parece que estamos copiando el código, pero tenga en cuenta que todas las clases derivadas invocan diferentes sobrecargas ( Apple
llama a Visit(Apple)
, Banana
a Visit(Banana)
, y así sucesivamente).
Implementar el visitante:
class FruitPartitioner : IFruitVisitor
{
public List<Orange> Oranges { get; private set; }
public List<Apple> Apples { get; private set; }
public List<Banana> Bananas { get; private set; }
public FruitPartitioner()
{
Oranges = new List<Orange>();
Apples = new List<Apple>();
Bananas = new List<Banana>();
}
public void Visit(Orange fruit) { Oranges.Add(fruit); }
public void Visit(Apple fruit) { Apples.Add(fruit); }
public void Visit(Banana fruit) { Bananas.Add(fruit); }
}
Ahora puedes dividir tus frutas sin una prueba de tipo:
FruitPartitioner partitioner = new FruitPartitioner();
foreach (Fruit fruit in fruits)
{
fruit.Accept(partitioner);
}
Console.WriteLine("Oranges.Count: {0}", partitioner.Oranges.Count);
Console.WriteLine("Apples.Count: {0}", partitioner.Apples.Count);
Console.WriteLine("Bananas.Count: {0}", partitioner.Bananas.Count);
Esto tiene las ventajas de:
- Al ser un código relativamente limpio y fácil de leer.
- Los errores tipográficos de seguridad se detectan en tiempo de compilación.
- Mantenibilidad Si agrego o elimino una clase concreta de Fruit, podría modificar mi interfaz IFruitVisitor para manejar el tipo en consecuencia, y el compilador encontrará inmediatamente todos los lugares donde implementamos la interfaz para que podamos hacer las modificaciones apropiadas.
Dicho esto, los visitantes suelen ser exagerados y tienen una tendencia a complicar enormemente las API, y puede ser muy engorroso definir a un nuevo visitante para cada nuevo tipo de comportamiento.
Por lo general, los patrones más simples como la herencia se deben utilizar en lugar de los visitantes. Por ejemplo, en principio, podría escribir una clase como:
class FruitPricer : IFruitVisitor
{
public double Price { get; private set; }
public void Visit(Orange fruit) { Price = 0.69; }
public void Visit(Apple fruit) { Price = 0.89; }
public void Visit(Banana fruit) { Price = 1.11; }
}
Funciona, pero ¿cuál es la ventaja sobre esta modificación trivial?
abstract class Fruit
{
public abstract void Accept(IFruitVisitor visitor);
public abstract double Price { get; }
}
Por lo tanto, debe usar visitas cuando se cumplan las siguientes condiciones:
Tienes un conjunto de clases bien definido y conocido que será visitado.
Las operaciones en dichas clases no están bien definidas o conocidas de antemano. Por ejemplo, si alguien está consumiendo su API y desea brindar a los consumidores una forma de agregar nuevas funciones ad-hoc a los objetos. También son una forma conveniente de extender clases selladas con funciones ad-hoc.
Realiza operaciones de una clase de objetos y desea evitar las pruebas de tipo de tiempo de ejecución. Este suele ser el caso cuando recorre una jerarquía de objetos dispares que tienen propiedades diferentes.
No use visitantes cuando:
Usted admite operaciones en una clase de objetos cuyos tipos derivados no se conocen de antemano.
Las operaciones en objetos están bien definidas de antemano, particularmente si pueden heredarse de una clase base o definirse en una interfaz.
Es más fácil para los clientes agregar nuevas funcionalidades a las clases que usan herencia.
Está atravesando una jerarquía de objetos que tienen las mismas propiedades o interfaz.
Quieres una API relativamente simple.
Es separar la manipulación de datos de los datos reales. Como beneficio adicional, puede reutilizar la misma clase de visitante para toda la jerarquía de sus clases, lo que de nuevo le evita transportar los algoritmos de manipulación de datos que son irrelevantes para sus objetos reales.
Había una vez...
class MusicLibrary {
private Set<Music> collection ...
public Set<Music> getPopMusic() { ... }
public Set<Music> getRockMusic() { ... }
public Set<Music> getElectronicaMusic() { ... }
}
Entonces te das cuenta de que te gustaría poder filtrar la colección de la biblioteca por otros géneros. Podrías seguir agregando nuevos métodos getter. O podrías usar Visitantes.
interface Visitor<T> {
visit(Set<T> items);
}
interface MusicVisitor extends Visitor<Music>;
class MusicLibrary {
private Set<Music> collection ...
public void accept(MusicVisitor visitor) {
visitor.visit( this.collection );
}
}
class RockMusicVisitor implements MusicVisitor {
private final Set<Music> picks = ...
public visit(Set<Music> items) { ... }
public Set<Music> getRockMusic() { return this.picks; }
}
class AmbientMusicVisitor implements MusicVisitor {
private final Set<Music> picks = ...
public visit(Set<Music> items) { ... }
public Set<Music> getAmbientMusic() { return this.picks; }
}
Separa los datos del algoritmo. Descargue el algoritmo a implementaciones de visitante. Agregue funcionalidad al crear más visitantes, en lugar de modificar constantemente (e hinchar) la clase que contiene los datos.
Proporciona otra capa de abstracción. Reduce la complejidad de un objeto y lo hace más modular. A Sorta le gusta usar una interfaz (la implementación es completamente independiente y a nadie le importa cómo se hace solo para que se haga).
Ahora nunca lo he usado, pero sería útil para: Implementar una función particular que necesita hacerse en diferentes subclases, ya que cada una de las subclases necesita implementarla de diferentes maneras en que otra clase implementaría todas las funciones. Un poco como un módulo, pero solo para una colección de clases. Wikipedia tiene una explicación bastante buena: en.wikipedia.org/wiki/Visitor_pattern Y su ejemplo ayuda a explicar lo que trato de decir.
Espero que ayude a aclararlo un poco.
EDITAR ** Siento haber vinculado mi wikipedia a su respuesta, pero realmente tienen un ejemplo decente :) No trato de ser ese tipo que dice "encuéntralo tú mismo".