好记性不如烂笔头,在java逐渐框架化,很多底层的思想和原理不被熟知,但是其重要性不言而喻,习其表面不如学其根本
案例:POJO类,它除了getter和setter之外是不包含任何业务逻辑的,也就是说它只对应一组数据并不包含任何功能。举个最常见的例子,比如数据库对应的实体类,一般我们不会在类里封装上业务逻辑,而是放在专门的Service类里去处理,也就是Service作为拜访者去访问实体类封装的数据。
使用场景之一:我们有很多的实体数据封装类(各类食品)都要进行一段相同的业务处理(计算价格),而每个实体类对应着不同的业务逻辑(水果按斤卖,啤酒论瓶卖),但我们又不想每个类对应一个业务逻辑类(类太繁多),而是汇总到一处业务处理(结账台),那我们应该如何设计呢?
超市结账举例,首先是各种商品的实体类,包括糖、酒、和水果,它们都应该共享一些共通属性,那就先抽象出一个商品类
package com.yitian.visitor;
import java.time.LocalDate;
/**
* @Description 各种商品的实体类,包括糖、酒、和水果,它们都应该共享一些共通属性,那就先抽象出一个商品类
* @Author yitianRen
* @Date 2019/8/30 14:02
* @Version 1.0
**/
public abstract class Product {
protected String name;// 品名
protected LocalDate producedDate;// 生产日期
protected float price;// 价格
public Product(String name, LocalDate producedDate, float 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 float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
}
我们抽象出来的都是些最基本的商品属性,简单的数据封装,标准的POJO类,接下来我们把这些属性和方法都继承下来给具体商品类,它们依次是糖果、酒、和水果。
package com.yitian.visitor;
import java.time.LocalDate;
/**
* @Description 水果多属性重量
* @Author yitianRen
* @Date 2019/8/30 14:30
* @Version 1.0
**/
public class Fruit extends Product{
private float weight;
public Fruit(String name, LocalDate producedDate, float price, float weight) {
super(name, producedDate, price);
this.weight = weight;
}
public float getWeight() {
return weight;
}
public void setWeight(float weight) {
this.weight = weight;
}
}
package com.yitian.visitor;
import java.time.LocalDate;
/**
* @Description 糖果类
* @Author yitianRen
* @Date 2019/8/30 14:05
* @Version 1.0
**/
public class Candy extends Product{
public Candy(String name, LocalDate producedDate, float price) {
super(name, producedDate, price);
}
}
package com.yitian.visitor;
import java.time.LocalDate;
/**
* @Description 酒
* @Author yitianRen
* @Date 2019/8/30 14:29
* @Version 1.0
**/
public class Wine extends Product{
public Wine(String name, LocalDate producedDate, float price) {
super(name, producedDate, price);
}
}
基本没什么特别的,除了水果是论斤销售,所以我们加了个重量属性,仅此而已。接下来就是我们的结算业务逻辑了,超市规定即将过期的给予一定打折优惠,日常促销可以吸引更多顾客。
package com.yitian.visitor;
/**
* @Author:yitianRen
* @Date:14:38 2019/8/30
* Description:(访问者)
*/
public interface Visitor {
void visit(Candy candy);// 糖果重载方法
void visit(Wine wine);// 酒类重载方法
void visit(Fruit fruit);// 水果重载方法
}
针对不同商品有不同的折扣和处理方式
package com.yitian.visitor;
import java.text.NumberFormat;
import java.time.LocalDate;
/**
* @Description 针对不同的商品有不同的折扣
* @Author yitianRen
* @Date 2019/8/30 14:42
* @Version 1.0
**/
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() + "】打折后价格=====");
float rate = 0;
long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();
if (days > 180) {
System.out.println("超过半年过期糖果,请勿食用!");
} else {
rate = 0.9f;
}
float 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() + "】打折后价格=====");
float rate = 0;
long days = billDate.toEpochDay() - fruit.getProducedDate().toEpochDay();
if (days > 7) {
System.out.println("¥0.00元(超过一周过期水果,请勿食用!)");
} else if (days > 3) {
rate = 0.5f;
} else {
rate = 1;
}
float discountPrice = fruit.getPrice() * fruit.getWeight() * rate;
System.out.println(NumberFormat.getCurrencyInstance().format(discountPrice));
}
}
上面是不同商品的计算模式,下面看一下客户端输出结果
import com.yitian.visitor.Candy;
import com.yitian.visitor.DiscountVisitor;
import com.yitian.visitor.Visitor;
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
//小黑兔奶糖,生产日期:2018-10-1,原价:¥20.00
Candy candy = new Candy("小黑兔奶糖", LocalDate.of(2018, 10, 1), 20.00f);
Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2019, 1, 1));
discountVisitor.visit(candy);
/*结算日期:2019-01-01
=====糖果【小黑兔奶糖】打折后价格=====
¥18.00*/
}
}
单个商品这种输出无可厚非,抽象类会自动识别重载方法,但是如果我们购买多个商品呢?让我来看一下:
import com.yitian.visitor.*;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
/// 三件商品加入购物车
List<Product> products = Arrays.asList(
new Candy("小黑兔奶糖", LocalDate.of(2018, 10, 1), 20.00f),
new Wine("猫泰白酒", LocalDate.of(2017, 1, 1), 1000.00f),
new Fruit("草莓", LocalDate.of(2018, 12, 26), 10.00f, 2.5f)
);
Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2018, 1, 1));
// 迭代购物车轮流结算
for (Product product : products) {
discountVisitor.visit(product);// 此处报错
}
}
}
注意重点来了,我们顺利地加入购物车并迭代轮流结算每个产品,可是第最后一行会报错,编译器对泛化后的product很是茫然,这到底是糖还是酒?该调用哪个visit方法呢?很多朋友疑问为什么不能在运行时根据对象类型动态地派发给对应的重载方法?试想,如果我们新加一个蔬菜产品类Vegetable,但没有在Visitor里加入其重载方法visit(Vegetable vegetable),那运行起来岂不是更糟糕?所以编译器提前就应该禁止此种情形通过编译。
难道我们设计思路错了?有没有办法把产品派发到相应的重载方法?答案是肯定的,这里涉及到一个新的概念,我们需要利用“双派发”(double dispatch)巧妙地绕过这个错误,既然访问者访问不了,我们从被访问者(产品资源)入手,来看代码,先定义一个接待者接口。
package com.yitian.visitor;
/**
*@Author:yitianRen
*@Date:10:08 2019/9/10
*Description: 接待者 双派发
*/
public interface Acceptable {
//主动接受拜访者
void accept(Visitor visitor);
}
可以看到这个“接待者”定义了一个接待方法,凡是“来访者”身份的都予以接受。我们先用糖果类实现这个接口,并主动接受来访者的拜访。
package com.yitian.visitor;
import java.time.LocalDate;
/**
* @Description 糖果类
* @Author yitianRen
* @Date 2019/8/30 14:05
* @Version 1.0
**/
public class Candy extends Product implements Acceptable{
public Candy(String name, LocalDate producedDate, float price) {
super(name, producedDate, price);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
糖果类顺理成章地成为了“接待者”(其他品类雷同,此处忽略代码),并把自己(this)交给了来访者),这样绕来绕去起到什么作用呢?别急,我们先来看双派发到底是怎样实现的。
package com.yitian.visitor;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/10 10:17
* @Version 1.0
**/
public class VisitorMain {
public static void main(String[] args) {
// 三件商品加入购物车 使用接口接待不同类型商品调用各自折扣
List<Acceptable> products = Arrays.asList(
new Candy("小黑兔奶糖", LocalDate.of(2018, 10, 1), 20.00f),
new Wine("猫泰白酒", LocalDate.of(2017, 1, 1), 1000.00f),
new Fruit("草莓", LocalDate.of(2018, 12, 26), 10.00f, 2.5f)
);
Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2019, 1, 1));
for (Acceptable product : products) {
product.accept(discountVisitor);
}
/*
结算日期:2019-01-01
=====糖果【小黑兔奶糖】打折后价格=====
¥18.00
=====酒品【猫泰白酒】无折扣价格=====
¥1,000.00
=====水果【草莓】打折后价格=====
¥12.50
*/
}
}
注意看购物车List<Product>已经被改为泛型Acceptable了,也就是说所有商品统统被泛化且当作“接待者”了,由于泛型化后的商品像是被打了包裹一样让拜访者无法识别品类,所以在迭代里面我们让这些商品对象主动去“接待”来访者product.accept(discountVisitor);。这类似于警察(访问者)办案时嫌疑人(接待者)需主动接受调查并出示自己的身份证给警察,如此就可以基于个人信息查询前科并展开相关调查。
如此一来,在运行时的糖果自己是认识自己的,它就把自己递交给来访者,此时的this必然就属糖果类了,所以能得偿所愿地派发到Visitor的visit(Fruit fruit)重载方法,这样便实现了“双派发”,也就是说我们先派发给商品去主动接待,然后又把自己派发回给访问者,我不认识你,你告诉我你是谁。
终于,我们巧妙地用双派发解决了方法重载的多态派发问题,如虎添翼,访问者模式框架至此搭建竣工,之后再添加业务逻辑不必再改动数据实体类了,比如我们再增加一个针对六一儿童节打折业务,加大对糖果类、玩具类的打折力度,而不需要为每个POJO类添加对应打折方法,数据资源(实现接待者接口)与业务(实现访问者接口)被分离开来,且业务处理集中化、多态化、亦可扩展。纯粹的数据,不应该多才多艺。
如此一来,在运行时的糖果自己是认识自己的,它就把自己递交给来访者,此时的this必然就属糖果类了,所以能得偿所愿地派发到Visitor的visit(Fruit fruit)重载方法,这样便实现了“双派发”,也就是说我们先派发给商品去主动接待,然后又把自己派发回给访问者,我不认识你,你告诉我你是谁。
终于,我们巧妙地用双派发解决了方法重载的多态派发问题,如虎添翼,访问者模式框架至此搭建竣工,之后再添加业务逻辑不必再改动数据实体类了,比如我们再增加一个针对六一儿童节打折业务,加大对糖果类、玩具类的打折力度,而不需要为每个POJO类添加对应打折方法,数据资源(实现接待者接口)与业务(实现访问者接口)被分离开来,且业务处理集中化、多态化、亦可扩展。纯粹的数据,不应该多才多艺。
总结:当我们去访问一个具有公共属性的类时,我们不清楚他有哪些孩子类,那我可以转变思想,孩子主动去接待访问者,主动告诉访问者我是哪个!这是我理解的访问者模式
文章来源于 微信公众号 java知音 设计模式