第六章 6.7 访问者模式

分类:01_设计模式

标签:

6.7.1 访问者模式介绍

访问者模式在实际开发中使用的非常少,因为它比较难以实现并且应用该模式肯能会导致代码的可读性变差,可维护性变差,在没有特别必要的情况下,不建议使用访问者模式.

访问者模式(Visitor Pattern) 的原始定义是:允许在运行时将一个或多个操作应用于一组对象,将操作与对象结构分离。

这个定义会比较抽象,但是我们依然能看出两个关键点:

访问者模式主要解决的是数据与算法的耦合问题, 尤其是在数据结构比较稳定,而算法多变的情况下.为了不污染数据本身,访问者会将多种算法独立归档,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并确保算法的自由扩展.

6.7.2 访问者模式原理

122.jpg

访问者模式包含以下主要角色:

6.7.3 访问者模式实现

我们以超市购物为例,假设超市中的三类商品: 水果,糖果,酒水进行售卖. 我们可以忽略每种商品的计价方法,因为最终结账时由收银员统一集中处理,在商品类中添加计价方法是不合理的设计.我们先来定义糖果类和酒类、水果类.


/**
* 抽象商品父类
* @author spikeCong
* @date 2022/10/18
**/
public abstract class Product {

   private String name;  //商品名
   private LocalDate producedDate;  // 生产日期
   private double price;  //单品价格

   public Product(String name, LocalDate producedDate, double price) {
       this.name = name;
       this.producedDate = producedDate;
       this.price = price;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public LocalDate getProducedDate() {
       return producedDate;
   }

   public void setProducedDate(LocalDate producedDate) {
       this.producedDate = producedDate;
   }

   public double getPrice() {
       return price;
   }

   public void setPrice(double price) {
       this.price = price;
   }
}

/**
* 糖果类
* @author spikeCong
* @date 2022/10/18
**/
public class Candy extends Product{
   public Candy(String name, LocalDate producedDate, double price) {
       super(name, producedDate, price);
   }
}

/**
* 酒水类
* @author spikeCong
* @date 2022/10/18
**/
public class Wine extends Product{

   public Wine(String name, LocalDate producedDate, double price) {
       super(name, producedDate, price);
   }
}

/**
* 水果类
* @author spikeCong
* @date 2022/10/18
**/
public class Fruit extends Product{
   
   //重量
   private float weight;

   public Fruit(String name, LocalDate producedDate, double price, float weight) {
       super(name, producedDate, price);
       this.weight = weight;
   }

   public float getWeight() {
       return weight;
   }

   public void setWeight(float weight) {
       this.weight = weight;
   }
}

访问者接口


/**
* 访问者接口-根据入参不同调用对应的重载方法
* @author spikeCong
* @date 2022/10/18
**/
public interface Visitor {

   public void visit(Candy candy);  //糖果重载方法
   
   public void visit(Wine wine);  //酒类重载方法
   
   public void visit(Fruit fruit);  //水果重载方法
}

具体访问者


/**
* 折扣计价访问者类
* @author spikeCong
* @date 2022/10/18
**/
public class DiscountVisitor implements Visitor {

   private LocalDate billDate;

   public DiscountVisitor(LocalDate billDate) {
       this.billDate = billDate;
       System.out.println("结算日期: " + billDate);
   }

   @Override
   public void visit(Candy candy) {
       System.out.println("糖果: " + candy.getName());

       //获取产品生产天数
       long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();

       if(days > 180){
           System.out.println("超过半年的糖果,请勿食用!");
       }else{
           double rate = 0.9;
           double discountPrice = candy.getPrice() * rate;
           System.out.println("糖果打折后的价格"+NumberFormat.getCurrencyInstance().format(discountPrice));
       }
   }

   @Override
   public void visit(Wine wine) {
       System.out.println("酒类: " + wine.getName()+",无折扣价格!");
       System.out.println("原价: "+NumberFormat.getCurrencyInstance().format(wine.getPrice()));
   }

   @Override
   public void visit(Fruit fruit) {
       System.out.println("水果: " + fruit.getName());
       //获取产品生产天数
       long days = billDate.toEpochDay() - fruit.getProducedDate().toEpochDay();

       double rate = 0;

       if(days > 7){
           System.out.println("超过七天的水果,请勿食用!");
       }else if(days > 3){
           rate = 0.5;
       }else{
           rate = 1;
       }

       double discountPrice = fruit.getPrice() * fruit.getWeight() * rate;
       System.out.println("水果价格: "+NumberFormat.getCurrencyInstance().format(discountPrice));
   }

   public static void main(String[] args) {

       LocalDate billDate = LocalDate.now();

       Candy candy = new Candy("徐福记",LocalDate.of(2022,10,1),10.0);
       System.out.println("糖果: " + candy.getName());

       double rate = 0.0;

       long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();
       System.out.println(days);

       if(days > 180){
           System.out.println("超过半年的糖果,请勿食用!");
       }else{
           rate = 0.9;
           double discountPrice = candy.getPrice() * rate;
           System.out.println("打折后的价格"+NumberFormat.getCurrencyInstance().format(discountPrice));
       }
   }
}

客户端


public class Client {

   public static void main(String[] args) {

       //德芙巧克力,生产日期2002-5-1 ,原价 10元
       Candy candy = new Candy("德芙巧克力",LocalDate.of(2022,5,1),10.0);

       Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,11));
       visitor.visit(candy);
   }
}

上面的代码虽然可以完成当前的需求,但是设想一下这样一个场景: 由于访问者的重载方法只能对当个的具体商品进行计价,如果顾客选择了多件商品来结账时,就可能会引起重载方法的派发问题(到底该由谁来计算的问题).

首先我们定义一个接待访问者的类 Acceptable,其中定义了一个accept(Visitor visitor)方法, 只要是visitor的子类都可以接收.


/**
* 接待者接口(抽象元素角色)
* @author spikeCong
* @date 2022/10/18
**/
public interface Acceptable {

   //接收所有的Visitor访问者的子类实现类
   public void accept(Visitor visitor);
}

/**
* 糖果类
* @author spikeCong
* @date 2022/10/18
**/
public class Candy extends Product implements Acceptable{
   public Candy(String name, LocalDate producedDate, double price) {
       super(name, producedDate, price);
   }

   @Override
   public void accept(Visitor visitor) {
       //accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型
       visitor.visit(this);
   }
}

//酒水与水果类同样实现Acceptable接口,重写accept方法

测试


public class Client {

   public static void main(String[] args) {

//        //德芙巧克力,生产日期2002-5-1 ,原价 10元
////        Candy candy = new Candy("德芙巧克力",LocalDate.of(2022,5,1),10.0);
////
////        Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,11));
////        visitor.visit(candy);

       //模拟添加多个商品的操作
       List<Acceptable> products = Arrays.asList(
               new Candy("金丝猴奶糖",LocalDate.of(2022,6,10),10.00),
               new Wine("衡水老白干",LocalDate.of(2020,6,10),100.00),
               new Fruit("草莓",LocalDate.of(2022,10,12),50.00,1)
       );

       Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,17));
       for (Acceptable product : products) {
           product.accept(visitor);
       }
   }
}

代码编写到此出,就可以应对计价方式或者业务逻辑的变化了,访问者模式成功地将数据资源(需实现接待者接口)与数据算法 (需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,数据则保持相对固定,最终形成⼀个算法类对应⼀套数据。

6.7.4 访问者模式总结

1) 访问者模式优点:

2) 访问者模式缺点:

3) 使用场景


修改内容