【设计模式-4.10】行为型——访问者模式

25 阅读6分钟

说明:本文介绍行为型设计模式之一的访问者模式

定义

访问者模式(Visitor Pattern)是一种将数据结构与数据操作分离的设计模式,指封装一些作用于某种数据结构中的各元素的操作,可以在不改变数据结构的前提下定义作用于这些元素的新的操作,属于行为型设计模式。

(引自《设计模式就该这样学》P415)

购物打折

以超市购物打折为例,超市搞促销活动,购买不同类型的商品有不同的折扣,规则如下:

  • 酒水类:没有折扣;

  • 水果类:距生产日期3天内,不打折;超出3天,但不超过7天,打五折;超出7天,已过保质期,禁止出售;

  • 糖果类:距生产日期180内,打九折;超出180,已过保质期,禁止出售;


首先,定义一个商品类,如下:

(商品类,Product)

import java.time.LocalDate;

/**
 * 商品类
 */
public class Product {

    /**
     * 商品名称
     */
    private String name;

    /**
     * 生产日期
     */
    private LocalDate producedDate;

    /**
     * 价格
     * 单位,元
     */
    private Integer price;

    /**
     * 全参构造
     */
    public Product(String name, LocalDate producedDate, Integer 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 Integer getPrice() {
        return price;
    }

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

不同类型的商品,继承商品类,如下:

(糖果类,Candy)

import java.time.LocalDate;

/**
 * 糖果类
 */
public class Candy extends Product {

    /**
     * 全参构造
     */
    public Candy(String name, LocalDate producedDate, Integer price) {
        super(name, producedDate, price);
    }
}

(水果类,Fruit,水果是按斤称计算价格的,需增加重量属性)

import java.time.LocalDate;

/**
 * 水果类
 */
public class Fruit extends Product {

    /**
     * 重量
     * 单位,斤
     */
    private Integer weight;


    /**
     * 全参构造
     */
    public Fruit(String name, LocalDate producedDate, Integer price, Integer weight) {
        super(name, producedDate, price);
        this.weight = weight;
    }

    public Integer getWeight() {
        return weight;
    }

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

(酒水类,Wine)

import java.time.LocalDate;

/**
 * 酒水类
 */
public class Wine extends Product {

    /**
     * 全参构造
     */
    public Wine(String name, LocalDate producedDate, Integer price) {
        super(name, producedDate, price);
    }
}

(访问者接口,Visitor,定义不同商品的访问行为,也就是后面的计价行为)

/**
 * 访问者接口
 * 定义三种商品的访问行为
 */
public interface Visitor {

    void visit(Fruit fruit);

    void visit(Wine wine);

    void visit(Candy candy);
}

(促销计算价格行为实现,DiscountVisitor,针对不同类型的商品写不同的实现代码)

import java.text.NumberFormat;
import java.time.LocalDate;

/**
 * 促销计算价格访问者
 */
public class DiscountVisitor implements Visitor {

    /**
     * 计价日期
     */
    private LocalDate discountDate;

    public DiscountVisitor(LocalDate discountDate) {
        this.discountDate = discountDate;
        System.out.println("计价日期:" + discountDate);
    }

    /**
     * 水果促销价格计算
     * 折扣后价格 = 价格 * 重量 * 折扣率
     * 折扣率计算:
     *  在生产日期3天内,不打折;
     *  超出生产日期3天,打5折;
     *  超出7天,禁止出售
     */
    @Override
    public void visit(Fruit fruit) {
        System.out.println("=================【水果类】" + fruit.getName() + "打折后价格=================");
        double rete = 0;
        // 统计商品距离当前已多少天
        long days = discountDate.toEpochDay() - fruit.getProducedDate().toEpochDay();
        // 折扣计算
        if (days > 7) {
            System.out.println("水果保质期7天,已超出保质期,禁止出售!");
        } else if (days > 3) {
            rete = 0.5;
        } else {
            rete = 1;
        }
        // 计算促销价格
        double discountPrice = fruit.getPrice() * fruit.getWeight() * rete;
        System.out.println(NumberFormat.getCurrencyInstance().format(discountPrice));
    }

    /**
     * 糖果促销价格计算
     * 折扣后价格 = 价格 * 折扣率
     * 折扣率计算:
     *  在生产日期180天内,打9折;
     *  超出生产日期180天,禁止出售
     */
    @Override
    public void visit(Candy candy) {
        System.out.println("=================【水果类】" + candy.getName() + "打折后价格=================");
        double rete = 0;
        // 统计商品距离当前已多少天
        long days = discountDate.toEpochDay() - candy.getProducedDate().toEpochDay();
        // 折扣计算
        if (days > 180) {
            System.out.println("糖果保质期180天,已超出保质期,请勿食用!");
        } else {
            rete = 0.9;
        }
        // 计算促销价格
        double discountPrice = candy.getPrice() * rete;
        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()));
    }
}

(客户端使用,Client)

import java.time.LocalDate;

public class Client {
    public static void main(String[] args) {
        DiscountVisitor discountVisitor = new DiscountVisitor(LocalDate.of(2025, 5, 5));
        // 椰子奶糖,30块
        Candy candy = new Candy("椰子奶糖", LocalDate.of(2025, 1, 1), 30);
        // 西瓜,2块钱一斤,15斤
        Fruit watermelon = new Fruit("西瓜", LocalDate.of(2025, 5, 2), 2, 15);
        // 酒水
        Wine coke = new Wine("可乐", LocalDate.of(2025, 1, 3), 3);

        // 计算促销价格
        discountVisitor.visit(candy);
        discountVisitor.visit(watermelon);
        discountVisitor.visit(coke);
    }
}

执行结果,糖果类打了九折,水果类生产日期未超出3天,未打折,酒水类没有折扣

在这里插入图片描述

看似很完美,但是不切实际,因为在实际的购物场景中,不太可能一次只买一样商品,会一次购买多种商品,如下:

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class Client {
    public static void main(String[] args) {
        DiscountVisitor discountVisitor = new DiscountVisitor(LocalDate.of(2025, 5, 5));
        // 椰子奶糖,30块
        Candy candy = new Candy("椰子奶糖", LocalDate.of(2025, 1, 1), 30);
        // 西瓜,2块钱一斤,15斤
        Fruit watermelon = new Fruit("西瓜", LocalDate.of(2025, 5, 2), 2, 15);
        // 酒水
        Wine coke = new Wine("可乐", LocalDate.of(2025, 1, 3), 3);
        
        // 购物车
        List<Product> productList = new ArrayList<>();
        productList.add(candy);
        productList.add(watermelon);
        productList.add(coke);

        // 计算促销价格
        for (Product product : productList) {
            discountVisitor.visit(product);
        }
    }
}

因为购物车集合,productList存的是商品的父类Product,而促销计算价格访问者的visit()方法,只能是Product的其中一个子类,所以计算促销价格这里,会编译报错。

在这里插入图片描述

访问者模式

针对上述困境,使用访问者模式进行改造,如下,定义一个接口,表示具有被访问的能力,并定义一个访问方法,传入一个访问对象

(可被访问的接口,Acceptable)

/**
 * 可被访问的接口
 */
public interface Acceptable {

    /**
     * 接受访问者
     */
    void accept(Visitor visitor);
}

每个商品的子类,都实现该接口,如下:

(糖果类,Candy)

/**
 * 糖果类
 */
public class Candy extends Product implements Acceptable {

    /**
     * 全参构造
     */
    public Candy(String name, LocalDate producedDate, Integer price) {
        super(name, producedDate, price);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

(水果类,Fruit)

import java.time.LocalDate;

/**
 * 水果类
 */
public class Fruit extends Product implements Acceptable {

    /**
     * 重量
     * 单位,斤
     */
    private Integer weight;


    /**
     * 全参构造
     */
    public Fruit(String name, LocalDate producedDate, Integer price, Integer weight) {
        super(name, producedDate, price);
        this.weight = weight;
    }

    public Integer getWeight() {
        return weight;
    }

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

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

(酒水类,Wine)

import java.time.LocalDate;

/**
 * 酒水类
 */
public class Wine extends Product implements Acceptable {

    /**
     * 全参构造
     */
    public Wine(String name, LocalDate producedDate, Integer price) {
        super(name, producedDate, price);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

客户端使用。购物车集合内,泛型声明为Acceptable,计算促销价格时,不再是计价访问者逐个去计算价格,而是商品调用自己的被访问方法,让计价访问者计算自己的价格。

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class Client {
    public static void main(String[] args) {
        DiscountVisitor discountVisitor = new DiscountVisitor(LocalDate.of(2025, 5, 5));
        // 椰子奶糖,30块
        Candy candy = new Candy("椰子奶糖", LocalDate.of(2025, 1, 1), 30);
        // 西瓜,2块钱一斤,15斤
        Fruit watermelon = new Fruit("西瓜", LocalDate.of(2025, 5, 2), 2, 15);
        // 酒水
        Wine coke = new Wine("可乐", LocalDate.of(2025, 1, 3), 3);

        // 购物车
        List<Acceptable> productList = new ArrayList<>();
        productList.add(candy);
        productList.add(watermelon);
        productList.add(coke);

        // 计算促销价格
        for (Acceptable product : productList) {
            product.accept(discountVisitor);
        }
    }
}

改造完成

在这里插入图片描述

使用场景

在《设计模式就该这样学》(P416)这本书中,提到访问者模式适用于以下场景:

(1)数据结构稳定,作用于数据结构的操作经常变化的场景。

(2)需要数据结构于数据操作分离的场景。

(3)需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。

可以对照上述购物打折的例子,支付场景(多种支付渠道)也可以考虑使用访问者模式。

总结

本文介绍了行为型设计模式中的访问者模式,参考《设计模式就该这样学》、《秒懂设计模式》两书,购物打折是《秒懂设计模式》中的举例。

首次发布

hezhongying.blog.csdn.net/article/det…