初学 Java 设计模式(二十三):实战访问者模式 「今天,你的基金红了吗?」

300 阅读6分钟

一、访问者模式介绍

1. 解决的问题

主要解决稳定的数据结构和易变的操作耦合问题。

2. 定义

访问者模式是一种行为设计模式,它能将算法与其所作用的对象隔离开来。

3. 应用场景

  • 如果需要对一个复杂对象结构(例如对象树)中的所有元素执行某些操作,可使用访问者模式。
  • 可使用访问者模式来清理辅助行为的业务逻辑。
  • 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用访问者模式。

二、访问者模式优缺点

1. 优点

  • 开闭原则:可以引入在不同类对象上执行的新行为,且无需对这些类做出修改。
  • 单一职责原则:可将同一行为的不同版本移到同一个类中。
  • 访问者对象可以在各种对象交互时收集一些有用的信息。当想要遍历一些复杂的对象结构,并在结构中的每个对象应用访问者时,这些信息可能会有帮助。

2. 缺点

  • 每次在元素层次结构中添加或移除一个类时,都要更新所有的访问者。
  • 在访问者同某个元素进行交互时,它们可能没有访问私有成员变量和方法的必要权限。

三、访问者模式应用实例:今天,你的基金红了吗?

1. 实例场景

从2020年开始,越来越多的人开始关注投资这一领域,2020年可以说是基金元年,各大基金公司收益满满,各种关于基金的段子漫天飞。

“ 买股票买基金,本来是想做财富的朋友,慢慢发现只能做时间的朋友了。 ”

“ 春天来了,到处都是绿色。 ”

“ 前几天第一次买基金,不太懂,最近时间比较闲暇,连续几天打开查询了基金,每次打开都少几百,所以想请问一下大家:基金查询是需要手续费吗? ”

不知道,大家投资了什么,反正我还是时间的朋友。

今天就以长短线投资者对基金、股票的不同关注点为例,介绍一下访问者模式。

2. 状态模式实现

2.1 工程结构
visitor-pattern
└─ src
    ├─ main
    │   └─ java
    │    └─ org.design.pattern.visitor
    │       ├─ product
    │       │    ├─ InvestmentProduct.java
    │       │    └─ impl
    │       │       ├─ Fund.java
    │       │       └─ Stock.java
    │       └─ context
    │            ├─ InvestorVisitor.java
    │            └─ impl
    │               ├─ ShortTermInvestor.java
    │               └─ LongTermInvestor.java
    └─ test
        └─ java
            └─ org.design.pattern.visitor.test
                └─ VisitorTest.java
2.2 代码实现
2.2.1 投资产品

投资产品超类

/**
 * 投资产品
 */
@Getter
@Setter
@AllArgsConstructor
public abstract class InvestmentProduct {
​
    /**
     * 代码
     */
    private String code;
​
    /**
     * 名称
     */
    private String name;
​
    /**
     * 接收方法
     *
     * @param investorVisitor 投资访问者
     */
    public abstract void accept(InvestorVisitor investorVisitor);
}

股票

/**
 * 股票
 */
@Slf4j
public class Stock extends InvestmentProduct {
​
    /**
     * 市盈率
     */
    private Double pe;
​
    /**
     * 净利润同比
     */
    private Double profitYoy;
​
    /**
     * 构造方法
     *
     * @param code 个股代码
     * @param name 个股名称
     * @param pe 市盈率
     * @param profitYoy 净利润同比
     */
    public Stock(String code, String name, Double pe, Double profitYoy) {
        super(code, name);
        this.pe = pe;
        this.profitYoy = profitYoy;
    }
​
    /**
     * 获取市盈率情况
     */
    public void getPEPosition() {
        String pePosition = "";
        if (pe > 50) {
            pePosition = "高估值";
        } else if (pe < 0) {
            pePosition = "估值亏损";
        } else {
            pePosition = "低估值";
        }
        log.info("个股{}市盈率为{}, 处于{}", this.getName(), pe, pePosition);
    }
​
    /**
     * 获取净利润情况
     */
    public void getProfitPosition() {
        String profitPosition = "";
        if (profitYoy > 50) {
            profitPosition = "业绩上涨";
        } else if (profitYoy < 0) {
            profitPosition = "业绩下跌";
        } else {
            profitPosition = "业绩一般";
        }
        log.info("个股{}净利润同比增长为{}%, {}", this.getName(), profitYoy, profitPosition);
    }
​
    /**
     * 接收方法
     *
     * @param investorVisitor 投资访问者
     */
    @Override
    public void accept(InvestorVisitor investorVisitor) {
        investorVisitor.visit(this);
    }
}

基金

/**
 * 基金
 */
@Slf4j
public class Fund extends InvestmentProduct {
​
    /**
     * 基金经理
     */
    private String manager;
​
    /**
     * 基金经理战绩
     */
    private String managerRecord;
​
    /**
     * 基金历史最大回撤率
     */
    private Double historyRetreatRate;
​
    /**
     * 构造方法
     *
     * @param code 基金代码
     * @param name 基金名称
     * @param manager 基金经理
     * @param managerRecord 基金经理战绩
     * @param historyRetreatRate 易方达中小盘混合
     */
    public Fund(String code, String name, String manager, String managerRecord, Double historyRetreatRate) {
        super(code, name);
        this.manager = manager;
        this.managerRecord = managerRecord;
        this.historyRetreatRate = historyRetreatRate;
    }
​
    /**
     * 获取基金经理战绩
     */
    public void getManagerRecord() {
        log.info("基金{}经理为{},历史战绩{}", getName(), manager, managerRecord);
    }
​
    /**
     * 获取历史最大回撤率情况
     */
    public void getHistoryRetreatRatePosition() {
        String retreatRatePosition = "";
        if (historyRetreatRate > 30) {
            retreatRatePosition = "风险较大";
        } else if (historyRetreatRate < 0) {
            retreatRatePosition = "风险低";
        } else {
            retreatRatePosition = "风险较低";
        }
        log.info("基金{}历史最大回撤率为{}%, {}", this.getName(), historyRetreatRate, retreatRatePosition);
    }
​
    /**
     * 接收方法
     *
     * @param investorVisitor 投资访问者
     */
    @Override
    public void accept(InvestorVisitor investorVisitor) {
        investorVisitor.visit(this);
    }
}
2.2.2 投资者

投资访问者

/**
 * 投资访问者超类
 */
public interface InvestorVisitor {
​
    /**
     * 访问基金信息
     *
     * @param fund 基金
     */
    void visit(Fund fund);
​
    /**
     * 访问股票信息
     *
     * @param stock 股票
     */
    void visit(Stock stock);
}

短期访问者

/**
 * 短线投资者
 */
public class ShortTermInvestor implements InvestorVisitor {
​
    /**
     * 访问基金信息
     *
     * @param fund 基金
     */
    @Override
    public void visit(Fund fund) {
        fund.getManagerRecord();
    }
​
    /**
     * 访问股票信息
     *
     * @param stock 股票
     */
    @Override
    public void visit(Stock stock) {
        stock.getProfitPosition();
    }
}

长期访问者

/**
 * 长线投资者
 */
public class LongTermInvestor implements InvestorVisitor {
​
    /**
     * 访问基金信息
     *
     * @param fund 基金
     */
    @Override
    public void visit(Fund fund) {
        fund.getHistoryRetreatRatePosition();
    }
​
    /**
     * 访问股票信息
     *
     * @param stock 股票
     */
    @Override
    public void visit(Stock stock) {
        stock.getPEPosition();
    }
}
2.3 测试验证
2.3.1 测试验证类
public class VisitorTest {
    @Test
    public void test() {
        List<InvestmentProduct> investmentProducts = Arrays.asList(
                new Fund("110011", "易方达中小盘混合", "张坤", "优秀", 15.12),
                new Fund("320007", "诺安成长混合", "蔡嵩松", "优秀", 35.17),
                new Stock("600519", "贵州茅台", 60.57, 6.57),
                new Stock("000002", "万科A", 7.37, 50.33)
        );
        InvestorVisitor shortTermInvestor = new ShortTermInvestor();
        System.out.println("短期投资者关注点:");
        investmentProducts.forEach(investmentProduct -> {
            investmentProduct.accept(shortTermInvestor);
        });
        InvestorVisitor longTermInvestor = new LongTermInvestor();
        System.out.println("长期投资者关注点:");
        investmentProducts.forEach(investmentProduct -> {
            investmentProduct.accept(longTermInvestor);
        });
    }
}
2.3.2 测试结果
短期投资者关注点:
20:53:52.449 [main] INFO  o.d.p.vistor.model.product.impl.Fund - 基金易方达中小盘混合经理为张坤,历史战绩优秀
20:53:52.451 [main] INFO  o.d.p.vistor.model.product.impl.Fund - 基金诺安成长混合经理为蔡嵩松,历史战绩优秀
20:53:52.452 [main] INFO  o.d.p.v.model.product.impl.Stock - 个股贵州茅台净利润同比增长为6.57%, 业绩一般
20:53:52.453 [main] INFO  o.d.p.v.model.product.impl.Stock - 个股万科A净利润同比增长为50.33%, 业绩上涨
长期投资者关注点:
20:53:52.454 [main] INFO  o.d.p.vistor.model.product.impl.Fund - 基金易方达中小盘混合历史最大回撤率为15.12%, 风险较低
20:53:52.455 [main] INFO  o.d.p.vistor.model.product.impl.Fund - 基金诺安成长混合历史最大回撤率为35.17%, 风险较大
20:53:52.455 [main] INFO  o.d.p.v.model.product.impl.Stock - 个股贵州茅台市盈率为60.57, 处于高估值
20:53:52.455 [main] INFO  o.d.p.v.model.product.impl.Stock - 个股万科A市盈率为7.37, 处于低估值

Process finished with exit code 0

四、访问者模式结构

访问者模式结构-模式结构图.png

  1. 访问者(Visitor)接口声明了一些以对象结构的具体元素作为参数的访问者方法。

  2. 具体访问者(Concrete Visitor)会为不同的具体元素类实现相同行为的几个不同版本。

  3. 元素(Element)接口声明了一个方法来接收访问者。该方法必须有一个参数被声明为访问者接口类型。

  4. 具体元素(Concrete Element)必须实现接收方法。该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。

    注:即使元素基类实现了该方法,所有子类都必须对其进行重写并调用访问者对象中的合适方法。

  5. 客户端(Client)通常会作为集合或其他复杂对象(例如一个组合树)的代表。客户端通常不知晓所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。

设计模式并不难学,其本身就是多年经验提炼出的开发指导思想,关键在于多加练习,带着使用设计模式的思想去优化代码,就能构建出更合理的代码。

源码地址:github.com/yiyufxst/de…

参考资料:

重学 Java 设计模式:juejin.cn/post/685003…

深入设计模式:refactoringguru.cn/design-patt…