组合模式
事实上组合模式和桥接模式的组合完全不一样。组合模式用于 整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。
文件夹和子文件夹的关系:文件夹中可以存放文件,也可以新建文件夹,子文件夹也一样。
总公司子公司的关系:总公司可以设立部门,也可以设立分公司,子公司也一样。
树枝和分树枝的关系:树枝可以长出叶子,也可以长出树枝,分树枝也一样。
在这些关系中,虽然整体包含了部分,但无论整体或部分,都具有一致的行为。
组合模式:又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
我们注意到人员结构中有两种结构,一是管理者,如老板,PM,CFO,CTO,二是职员。其中有的管理者不仅仅要管理职员,还会管理其他的管理者。这就是一个典型的整体与部分的结构。
3.1.不使用组合模式的设计方案
新建管理者类:
public class Manager {
// 职位
private String position;
// 工作内容
private String job;
// 管理的管理者
private List<Manager> managers = new ArrayList<>();
// 管理的职员
private List<Employee> employees = new ArrayList<>();
public Manager(String position, String job) {
this.position = position;
this.job = job;
}
public void addManager(Manager manager) {
managers.add(manager);
}
public void removeManager(Manager manager) {
managers.remove(manager);
}
public void addEmployee(Employee employee) {
employees.add(employee);
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
}
// 做自己的本职工作
public void work() {
System.out.println("我是" + position + ",我正在" + job);
}
// 检查下属
public void check() {
work();
for (Employee employee : employees) {
employee.work();
}
for (Manager manager : managers) {
manager.check();
}
}
}
新建职员类:
public class Employee {
// 职位
private String position;
// 工作内容
private String job;
public Employee(String position, String job) {
this.position = position;
this.job = job;
}
// 做自己的本职工作
public void work() {
System.out.println("我是" + position + ",我正在" + job);
}
}
客户端建立人员结构关系:
public class Client {
@Test
public void test() {
Manager boss = new Manager("老板", "唱怒放的生命");
Employee HR = new Employee("人力资源", "聊微信");
Manager PM = new Manager("产品经理", "不知道干啥");
Manager CFO = new Manager("财务主管", "看剧");
Manager CTO = new Manager("技术主管", "划水");
Employee UI = new Employee("设计师", "画画");
Employee operator = new Employee("运营人员", "兼职客服");
Employee webProgrammer = new Employee("程序员", "学习设计模式");
Employee backgroundProgrammer = new Employee("后台程序员", "CRUD");
Employee accountant = new Employee("会计", "背九九乘法表");
Employee clerk = new Employee("文员", "给老板递麦克风");
boss.addEmployee(HR);
boss.addManager(PM);
boss.addManager(CFO);
PM.addEmployee(UI);
PM.addManager(CTO);
PM.addEmployee(operator);
CTO.addEmployee(webProgrammer);
CTO.addEmployee(backgroundProgrammer);
CFO.addEmployee(accountant);
CFO.addEmployee(clerk);
boss.check();
}
}
运行测试方法,输出如下(为方便查看,笔者添加了缩进):
我是老板,我正在唱怒放的生命 我是人力资源,我正在聊微信 我是产品经理,我正在不知道干啥 我是设计师,我正在画画
我是运营人员,我正在兼职客服 我是技术主管,我正在划水 我是程序员,我正在学习设计模式 我是后台程序员,我正在CRUD
我是财务主管,我正在看剧 我是会计,我正在背九九乘法表 我是文员,我正在给老板递麦克风
这样我们就设计出了公司的结构,但是这样的设计有两个弊端:
name 字段,job 字段,work 方法重复了。
管理者对其管理的管理者和职员需要区别对待。
组合模式最主要的功能就是让用户可以一致对待整体和部分结构,将两者都作为一个相同的组件,所以我们先新建一个抽象的组件类:
public abstract class Component {
// 职位
private String position;
// 工作内容
private String job;
public Component(String position, String job) {
this.position = position;
this.job = job;
}
// 做自己的本职工作
public void work() {
System.out.println("我是" + position + ",我正在" + job);
}
abstract void addComponent(Component component);
abstract void removeComponent(Component component);
abstract void check();
}
管理者继承自此抽象类:
public class Manager extends Component {
// 管理的组件
private List<Component> components = new ArrayList<>();
public Manager(String position, String job) {
super(position, job);
}
@Override
public void addComponent(Component component) {
components.add(component);
}
@Override
void removeComponent(Component component) {
components.remove(component);
}
// 检查下属
@Override
public void check() {
work();
for (Component component : components) {
component.check();
}
}
}
职员同样继承自此抽象类:
public class Employee extends Component {
public Employee(String position, String job) {
super(position, job);
}
@Override
void addComponent(Component component) {
System.out.println("职员没有管理权限");
}
@Override
void removeComponent(Component component) {
System.out.println("职员没有管理权限");
}
@Override
void check() {
work();
}
}
装饰模式
我们发现装饰品并不会改变物品本身,只是起到一个锦上添花的作用。装饰模式也一样,它的主要作用就是:
增强一个类原有的功能
为一个类添加新的功能
并且 装饰模式也不会改变原有的类。
装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器,与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”。
1. 用于增强功能的装饰模式
我们用程序来模拟一下戴上装饰品提高我们颜值的过程:
新建颜值接口:
public interface IBeauty {
int getBeautyValue();
}
新建 Me 类,实现颜值接口:
public class Me implements IBeauty {
@Override
public int getBeautyValue() {
return 100;
}
}
戒指装饰类,将 Me 包装起来:
public class RingDecorator implements IBeauty {
private final IBeauty me;
public RingDecorator(IBeauty me) {
this.me = me;
}
@Override
public int getBeautyValue() {
return me.getBeautyValue() + 20;
}
}
客户端测试:
public class Client {
@Test
public void show() {
IBeauty me = new Me();
System.out.println("我原本的颜值:" + me.getBeautyValue());
IBeauty meWithRing = new RingDecorator(me);
System.out.println("戴上了戒指后,我的颜值:" + meWithRing.getBeautyValue());
}
}
运行程序,输出如下:
我原本的颜值:100
戴上了戒指后,我的颜值:120
这就是最简单的增强功能的装饰模式。以后我们可以添加更多的装饰类,比如:
耳环装饰类:
public class EarringDecorator implements IBeauty {
private final IBeauty me;
public EarringDecorator(IBeauty me) {
this.me = me;
}
@Override
public int getBeautyValue() {
return me.getBeautyValue() + 50;
}
}
项链装饰类:
public class NecklaceDecorator implements IBeauty {
private final IBeauty me;
public NecklaceDecorator(IBeauty me) {
this.me = me;
}
@Override
public int getBeautyValue() {
return me.getBeautyValue() + 80;
}
}
客户端测试:
public class Client {
@Test
public void show() {
IBeauty me = new Me();
System.out.println("我原本的颜值:" + me.getBeautyValue());
// 随意挑选装饰
IBeauty meWithNecklace = new NecklaceDecorator(me);
System.out.println("戴上了项链后,我的颜值:" + meWithNecklace.getBeautyValue());
// 多次装饰
IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me)));
System.out.println("戴上耳环、戒指、项链后,我的颜值:" + meWithManyDecorators.getBeautyValue());
// 任意搭配装饰
IBeauty meWithNecklaceAndRing = new NecklaceDecorator(new RingDecorator(me));
System.out.println("戴上戒指、项链后,我的颜值:" + meWithNecklaceAndRing.getBeautyValue());
}
}
我原本的颜值:100
戴上了项链后,我的颜值:180
戴上耳环、戒指、项链后,我的颜值:250
戴上戒指、项链后,我的颜值:200
可以看到,装饰器也实现了 IBeauty 接口,并且没有添加新的方法,也就是说这里的装饰器仅用于增强功能,并不会改变 Me 原有的功能,这种装饰模式称之为 透明装饰模式,由于没有改变接口,也没有新增方法,所以透明装饰模式可以无限装饰。
装饰模式是 继承 的一种替代方案。本例如果不使用装饰模式,而是改用继承实现的话,戴着戒指的 Me 需要派生一个子类、戴着项链的 Me 需要派生一个子类、戴着耳环的 Me 需要派生一个子类、戴着戒指 + 项链的需要派生一个子类…各种各样的排列组合会造成类爆炸。而采用了装饰模式就只需要为每个装饰品生成一个装饰类即可,所以说就 增加对象功能 来说,装饰模式比生成子类实现更为灵活。
用于添加功能的装饰模式
我们用程序来模拟一下房屋装饰粘钩后,新增了挂东西功能的过程:
新建房屋接口:
public interface IHouse {
void live();
}
房屋类:
public class House implements IHouse{
@Override
public void live() {
System.out.println("房屋原有的功能:居住功能");
}
}
粘钩装饰类:
public class StickyHookDecorator implements IStickyHookHouse {
private final IHouse house;
public StickyHookDecorator(IHouse house) {
this.house = house;
}
@Override
public void live() {
house.live();
}
@Override
public void hangThings() {
System.out.println("有了粘钩后,新增了挂东西功能");
}
}
客户端测试:
public class Client {
@Test
public void show() {
IHouse house = new House();
house.live();
IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
stickyHookHouse.live();
stickyHookHouse.hangThings();
}
}
房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了粘钩后,新增了挂东西功能
这就是用于 新增功能 的装饰模式。我们在接口中新增了方法:hangThings,然后在装饰器中将 House 类包装起来,之前 House 中的方法仍然调用 house 去执行,也就是说我们并没有修改原有的功能,只是扩展了新的功能,这种模式在装饰模式中称之为 半透明装饰模式。
为什么叫半透明呢?由于新的接口 IStickyHookHouse 拥有之前 IHouse 不具有的方法,所以我们如果要使用装饰器中添加的功能,就不得不区别对待 装饰前的对象和装饰后的对象。也就是说客户端要使用新方法,必须知道具体的装饰类 StickyHookDecorator,所以这个装饰类对客户端来说是可见的、不透明的。而被装饰者不一定要是 House,它可以是实现了 IHouse 接口的任意对象,所以被装饰者对客户端是不可见的、透明的。由于一半透明,一半不透明,所以称之为半透明装饰模式。
我们可以添加更多的装饰器:
新建镜子装饰器的接口,继承自房屋接口:
public interface IMirrorHouse extends IHouse {
void lookMirror();
}
镜子装饰类:
public class MirrorDecorator implements IMirrorHouse{
private final IHouse house;
public MirrorDecorator(IHouse house) {
this.house = house;
}
@Override
public void live() {
house.live();
}
@Override
public void lookMirror() {
System.out.println("有了镜子后,新增了照镜子功能");
}
}
客户端测试:
public class Client {
@Test
public void show() {
IHouse house = new House();
house.live();
IMirrorHouse mirrorHouse = new MirrorDecorator(house);
mirrorHouse.live();
mirrorHouse.lookMirror();
}
}
运行程序,输出如下:
房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了镜子后,新增了照镜子功能
public class Client {
@Test
public void show() {
IHouse house = new House();
house.live();
IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
IMirrorHouse houseWithStickyHookMirror = new MirrorDecorator(stickyHookHouse);
houseWithStickyHookMirror.live();
houseWithStickyHookMirror.hangThings(); // 这里会报错,找不到 hangThings 方法
houseWithStickyHookMirror.lookMirror();
}
}
第二次装饰时,无法获得上一次装饰添加的方法。原因很明显,当我们用 IMirrorHouse 装饰器后,接口变为了 IMirrorHouse,这个接口中并没有 hangThings 方法。
可以,但那样做的话两个装饰类之间有了依赖关系,那就不是装饰模式了。装饰类不应该存在依赖关系,而应该在原本的类上进行装饰。这就意味着,半透明装饰模式中,我们无法多次装饰。
只要添加了新功能的装饰模式都称之为 半透明装饰模式,他们都具有不可以多次装饰的特点。仔细理解上文半透明名称的由来就知道了,“透明”指的是我们无需知道被装饰者具体的类,既增强了功能,又添加了新功能的装饰模式仍然具有半透明特性。
I/O 中的装饰模式
I/O 指的是 Input/Output,即输入、输出。我们以 Input 为例。先在 src 文件夹下新建一个文件 readme.text,随便写点文字:
public void io() throws IOException {
InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt"));
byte[] buffer = new byte[1024];
while (in.read(buffer) != -1) {
System.out.println(new String(buffer));
}
in.close();
}
这样写有一个问题,如果读取过程中出现了 IO 异常,InputStream 就不能正确关闭,所以我们要用try…finally来保证 InputStream 正确关闭:
public void io() throws IOException {
InputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream("src/readme.txt"));
byte[] buffer = new byte[1024];
while (in.read(buffer) != -1) {
System.out.println(new String(buffer));
}
} finally {
if (in != null) {
in.close();
}
}
}
这种写法实在是太丑了,而 IO 操作又必须这么写,显然 Java 也意识到了这个问题,所以 Java 7 中引入了try(resource)语法糖,IO 的代码就可以简化如下:
public void io() throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt"))) {
byte[] buffer = new byte[1024];
while (in.read(buffer) != -1) {
System.out.println(new String(buffer));
}
}
}
观察获取 InputStream 这句代码:
InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt"));
是不是和我们之前多次装饰的代码非常相似:
// 多次装饰
IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me)));
事实上,查看 I/O 的源码可知,Java I/O 的设计框架便是使用的 装饰者模式,InputStream 的继承关系如下:
其中,InputStream 是一个抽象类,对应上文例子中的 IHouse,其中最重要的方法是 read 方法,这是一个抽象方法:
public abstract class InputStream implements Closeable {
public abstract int read() throws IOException;
// ...
}
这个方法会读取输入流的下一个字节,并返回字节表示的 int 值(0~255),返回 -1 表示已读到末尾。由于它是抽象方法,所以具体的逻辑交由子类实现。
上图中,左边的三个类 FileInputStream、ByteArrayInputStream、ServletInputStream 是 InputStream 的三个子类,对应上文例子中实现了 IHouse 接口的 House。
右下角的三个类 BufferedInputStream、DataInputStream、CheckedInputStream 是三个具体的装饰者类,他们都为 InputStream 增强了原有功能或添加了新功能。
FilterInputStream 是所有装饰类的父类,它没有实现具体的功能,仅用来包装了一下 InputStream:
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
//...
}
我们以 BufferedInputStream 为例。原有的 InputStream 读取文件时,是一个字节一个字节读取的,这种方式的执行效率并不高,所以我们可以设立一个缓冲区,先将内容读取到缓冲区中,缓冲区读满后,将内容从缓冲区中取出来,这样就变成了一段一段读取,用内存换取效率。BufferedInputStream 就是用来做这个的。它继承自
FilterInputStream:
public class BufferedInputStream extends FilterInputStream {
private static final int DEFAULT_BUFFER_SIZE = 8192;
protected volatile byte buf[];
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
//...
}
我们先来看它的构造方法,在构造方法中,新建了一个 byte[] 作为缓冲区,从源码中我们看到,Java 默认设置的缓冲区大小为 8192 byte,也就是 8 KB。
然后我们来查看 read 方法:
public class BufferedInputStream extends FilterInputStream {
//...
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
private void fill() throws IOException {
// 往缓冲区内填充读取内容的过程
//...
}
}
在 read 方法中,调用了 fill 方法,fill 方法的作用就是往缓冲区中填充读取的内容。这样就实现了增强原有的功能。
在源码中我们发现,BufferedInputStream 没有添加 InputStream 中没有的方法,所以 BufferedInputStream 使用的是 透明的装饰模式。
DataInputStream 用于更加方便地读取 int、double 等内容,观察 DataInputStream 的源码可以发现,DataInputStream 中新增了 readInt、readLong 等方法,所以 DataInputStream 使用的是 半透明装饰模式。
这就是装饰模式,注意不要和适配器模式混淆了。两者在使用时都是包装一个类,但两者的区别其实也很明显:
纯粹的适配器模式 仅用于改变接口,不改变其功能,部分情况下我们需要改变一点功能以适配新接口。但使用适配器模式时,接口一定会有一个 回炉重造 的过程。
装饰模式 不改变原有的接口,仅用于增强原有功能或添加新功能,强调的是 锦上添花。
掌握了装饰者模式之后,理解 Java I/O 的框架设计就非常容易了。但对于不理解装饰模式的人来说,各种各样相似的 InputStream 非常容易让开发者感到困惑。这一点正是装饰模式的缺点:容易造成程序中有大量相似的类。虽然这更像是开发者的缺点,我们应该做的是提高自己的技术,掌握了这个设计模式之后它就是我们的一把利器。现在我们再看到 I/O 不同的 InputStream 装饰类,只需要关注它增强了什么功能或添加了什么功能即可。
外观模式
外观模式非常简单,体现的就是 Java 中封装的思想。将多个子系统封装起来,提供一个更简洁的接口供外部调用。
外观模式:外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式。
举个例子,比如我们每天打开电脑时,都需要做三件事:
打开浏览器
打开 IDE
打开微信
每天下班时,
关机前需要做三件事:
关闭浏览器
关闭 IDE
关闭微信
新建浏览器类:
public class Browser {
public static void open() {
System.out.println("打开浏览器");
}
public static void close() {
System.out.println("关闭浏览器");
}
}
新建 IDE 类:
public class IDE {
public static void open() {
System.out.println("打开 IDE");
}
public static void close() {
System.out.println("关闭 IDE");
}
}
新建微信类:
public class Wechat {
public static void open() {
System.out.println("打开微信");
}
public static void close() {
System.out.println("关闭微信");
}
}
客户端调用:
public class Client {
@Test
public void test() {
System.out.println("上班:");
Browser.open();
IDE.open();
Wechat.open();
System.out.println("下班:");
Browser.close();
IDE.close();
Wechat.close();
}
}
由于我们每天都要做这几件事,所以我们可以使用 外观模式,将这几个子系统封装起来,提供更简洁的接口:
public class Facade {
public void open() {
Browser.open();
IDE.open();
Wechat.open();
}
public void close() {
Browser.close();
IDE.close();
Wechat.close();
}
}
客户端就可以简化代码,只和这个外观类打交道:
public class Client {
@Test
public void test() {
Facade facade = new Facade();
System.out.println("上班:");
facade.open();
System.out.println("下班:");
facade.close();
}
}
享元模式
体现的是 程序可复用 的特点,为了节约宝贵的内存,程序应该尽可能地复用,就像《极限编程》作者 Kent 在书里说到的那样:Don’t repeat yourself. 简单来说 享元模式就是共享对象,提高复用性,官方的定义倒是显得文绉绉的:
享元模式:运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。
有个细节值得注意:有些对象本身不一样,但通过一点点变化后就可以复用,我们编程时可能稍不注意就会忘记复用这些对象。比如说伟大的《超级玛丽》,谁能想到草和云更改一下颜色就可以实现复用呢?
代理模式
现在我们有一个 人 类,他整天就只负责吃饭、睡觉:
人类的接口
public interface IPerson {
void eat();
void sleep();
}
人 类:
public class Person implements IPerson{
@Override
public void eat() {
System.out.println("我在吃饭");
}
@Override
public void sleep() {
System.out.println("我在睡觉");
}
}
客户端测试:
public class Client {
@Test
public void test() {
Person person = new Person();
person.eat();
person.sleep();
}
}
我们可以把这个类包装到另一个类中,实现完全一样的行为:
public class PersonProxy implements IPerson {
private final Person person;
public PersonProxy(Person person) {
this.person = person;
}
@Override
public void eat() {
person.eat();
}
@Override
public void sleep() {
person.sleep();
}
}
将客户端修改为调用这个新的类:
public class Client {
@Test
public void test() {
Person person = new Person();
PersonProxy proxy = new PersonProxy(person);
proxy.eat();
proxy.sleep();
}
}
我们在客户端和 Person 类之间新增了一个中间件 PersonProxy,这个类就叫做代理类,他实现了和 Person 类一模一样的行为。
代理模式:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
新建网络请求接口:
public interface IHttp {
void request(String sendData);
void onSuccess(String receivedData);
}
新建 Http 请求工具类:
public class HttpUtil implements IHttp {
@Override
public void request(String sendData) {
System.out.println("网络请求中...");
}
@Override
public void onSuccess(String receivedData) {
System.out.println("网络请求完成。");
}
}
新建 Http 代理类:
public class HttpProxy implements IHttp {
private final HttpUtil httpUtil;
public HttpProxy(HttpUtil httpUtil) {
this.httpUtil = httpUtil;
}
@Override
public void request(String sendData) {
httpUtil.request(sendData);
}
@Override
public void onSuccess(String receivedData) {
httpUtil.onSuccess(receivedData);
}
}
到这里,和我们上述吃饭睡觉的代码是一模一样的,现在我们在 HttpProxy 中新增打印日志信息:
public class HttpProxy implements IHttp {
private final HttpUtil httpUtil;
public HttpProxy(HttpUtil httpUtil) {
this.httpUtil = httpUtil;
}
@Override
public void request(String sendData) {
System.out.println("发送数据:" + sendData);
httpUtil.request(sendData);
}
@Override
public void onSuccess(String receivedData) {
System.out.println("收到数据:" + receivedData);
httpUtil.onSuccess(receivedData);
}
}
客户端验证:
public class Client {
@Test
public void test() {
HttpUtil httpUtil = new HttpUtil();
HttpProxy proxy = new HttpProxy(httpUtil);
proxy.request("request data");
proxy.onSuccess("received result");
}
}
这就是代理模式的一个应用,除了 打印日志,它还可以用来做权限管理。读者看到这里可能已经发现了,这个代理类看起来和装饰模式的 FilterInputStream 一模一样,但两者的目的不同,装饰模式是为了 增强功能或添加功能,代理模式主要是为了加以控制。
动态代理
上例中的代理被称之为静态代理,动态代理与静态代理的原理一模一样,只是换了一种写法。使用动态代理,需要把一个类传入,然后根据它正在调用的方法名判断是否需要加以控制。用伪代码表示如下:
public class HttpProxy {
private final HttpUtil httpUtil;
public HttpProxy(HttpUtil httpUtil) {
this.httpUtil = httpUtil;
}
// 假设调用 httpUtil 的任意方法时,都要通过这个方法间接调用, methodName 表示方法名,args 表示方法中传入的参数
public visit(String methodName, Object[] args) {
if (methodName.equals("request")) {
// 如果方法名是 request,打印日志,并调用 request 方法,args 的第一个值就是传入的参数
System.out.println("发送数据:" + args[0]);
httpUtil.request(args[0].toString());
} else if (methodName.equals("onSuccess")) {
// 如果方法名是 onSuccess,打印日志,并调用 onSuccess 方法,args 的第一个值就是传入的参数
System.out.println("收到数据:" + args[0]);
httpUtil.onSuccess(args[0].toString());
}
}
}
伪代码看起来还是很简单的,实现起来唯一的难点就是 怎么让 httpUtil 调用任意方法时,都通过一个方法间接调用。这里需要用到反射技术,不了解反射技术也没有关系,不妨把它记做固定的写法。实际的动态代理类代码如下:
public class HttpProxy implements InvocationHandler {
private HttpUtil httpUtil;
public IHttp getInstance(HttpUtil httpUtil) {
this.httpUtil = httpUtil;
return (IHttp) Proxy.newProxyInstance(httpUtil.getClass().getClassLoader(), httpUtil.getClass().getInterfaces(), this);
}
// 调用 httpUtil 的任意方法时,都要通过这个方法调用
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
if (method.getName().equals("request")) {
// 如果方法名是 request,打印日志,并调用 request 方法
System.out.println("发送数据:" + args[0]);
result = method.invoke(httpUtil, args);
} else if (method.getName().equals("onSuccess")) {
// 如果方法名是 onSuccess,打印日志,并调用 onSuccess 方法
System.out.println("收到数据:" + args[0]);
result = method.invoke(httpUtil, args);
}
return result;
}
}
先看 getInstance 方法,Proxy.newProxyInstance 方法是 Java 系统提供的方法,专门用于动态代理。其中传入的第一个参数是被代理的类的 ClassLoader,第二个参数是被代理类的 Interfaces,这两个参数都是 Object 中的,每个类都有,这里就是固定写法。我们只要知道系统需要这两个参数才能让我们实现我们的目的:调用被代理类的任意方法时,都通过一个方法间接调用。现在我们给系统提供了这两个参数,系统就会在第三个参数中帮我们实现这个目的。
第三个参数是 InvocationHandler 接口,这个接口中只有一个方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
那么不用猜就知道,现在我们调用被代理类 httpUtil 的任意方法时,都会通过这个 invoke 方法调用了。invoke 方法中,第一个参数我们暂时用不上,第二个参数 method 就是调用的方法,使用 method.getName() 可以获取到方法名,第三个参数是调用 method 方法需要传入的参数。本例中无论 request 还是 onSuccess 都只有一个 String 类型的参数,对应到这里就是 args[0]。返回的 Object 是 method 方法的返回值,本例中都是无返回值的。
我们在 invoke 方法中判断了当前调用方法的方法名,如果现在调用的方法是 request,那么打印请求参数,并使用这一行代码继续执行当前方法:
result = method.invoke(httpUtil, args);
修改客户端验证一下:
public class Client {
@Test
public void test() {
HttpUtil httpUtil = new HttpUtil();
IHttp proxy = new HttpProxy().getInstance(httpUtil);
proxy.request("request data");
proxy.onSuccess("received result");
}
}
动态代理本质上与静态代理没有区别,它的好处是 节省代码量。比如被代理类有 20 个方法,而我们只需要控制其中的两个方法,就可以用动态代理通过方法名对被代理类进行动态的控制,而如果用静态方法,我们就需要将另外的 18 个方法也写出来,非常繁琐。这就是动态代理的优势所在。
总结:
创建一个接口
一个方法实现了他的接口 在不改变原有的代码的时候,讲次类交给其他的类来处理 代理的类继承了所有的主要类的接口在每一个接口中除了能够做原本接口所做的事情外,还是能够做新增的事务
解释器模式
在设计模式中,解释器模式就是用来自定义语法的,它的定义如下。
解释器模式(Interpreter Pattern):给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
所以我们可以根据这个共同点提取出一个返回整数的接口,数字和计算符都作为该接口的实现类。在计算时,使用栈结构存储数据,将数字和计算符统一作为此接口的实现类压入栈中计算。
数字和计算符公共的接口:
interface Expression {
int intercept();
}
上文已经说到,数字和计算符都属于表达式的一部分,他们的共同点是都会返回一个整数。从表达式计算出整数的过程,我们称之为解释
(intercept)
public class Number implements Expression {
int number;
public Number(char word) {
switch (word) {
case '零':
number = 0;
break;
case '一':
number = 1;
break;
case '二':
number = 2;
break;
case '三':
number = 3;
break;
case '四':
number = 4;
break;
case '五':
number = 5;
break;
case '六':
number = 6;
break;
case '七':
number = 7;
break;
case '八':
number = 8;
break;
case '九':
number = 9;
break;
default:
break;
}
}
@Override
public int intercept() {
return number;
}
}
在 Number 类的构造函数中,先将传入的字符转换为对应的数字。在解释时将转换后的数字返回即可。
无论是加法还是减法,他们都是对左右两个表达式进行操作,所以我们可以将计算符提取出共同的抽象父类:
abstract class Operator implements Expression {
Expression left;
Expression right;
Operator(Expression left, Expression right) {
this.left = left;
this.right = right;
}
}
在此抽象父类中,我们存入了两个变量,表达计算符左右两边的表达式。
加法类实现如下:
class Add extends Operator {
Add(Expression left, Expression right) {
super(left, right);
}
@Override
public int intercept() {
return left.intercept() + right.intercept();
}
}
减法类:
class Sub extends Operator {
Sub(Expression left, Expression right) {
super(left, right);
}
@Override
public int intercept() {
return left.intercept() - right.intercept();
}
}
加法类和减法类都继承自 Operator 类,在对他们进行解释时,将左右两边表达式解释出的值相加或相减即可。
数字类和计算符内都定义好了,这时我们只需要再编写一个计算类将他们综合起来,统一计算即可。
计算类:
class Calculator {
int calculate(String expression) {
Stack<Expression> stack = new Stack<>();
for (int i = 0; i < expression.length(); i++) {
char word = expression.charAt(i);
switch (word) {
case '加':
stack.push(new Add(stack.pop(), new Number(expression.charAt(++i))));
break;
case '减':
stack.push(new Sub(stack.pop(), new Number(expression.charAt(++i))));
break;
default:
stack.push(new Number(word));
break;
}
}
return stack.pop().intercept();
}
}
在计算类中,我们使用栈结构保存每一步操作。遍历 expression 公式:
遇到数字则将其压入栈中;
遇到计算符时,先将栈顶元素 pop 出来,再和下一个数字一起传入计算符的构造函数中,组成一个计算符公式压入栈中。
需要注意的是,入栈出栈过程并不会执行真正的计算,栈操作只是将表达式组装成一个嵌套的类对象而已。比如:
“一加一”表达式,经过入栈出栈操作后,生成的对象是 new Add(new Number(‘一’), new Number(‘一’))
“二加五减三”表达式,经过入栈出栈操作后,生成的对象是 `new Sub(new Add(new Number(‘二’), new Number(‘五’)), new Number(‘三’))`
最后一步 stack.pop().intercept(),将栈顶的元素弹出,执行 intercept() ,这时才会执行真正的计算。计算时会将中文的数字和运算符分别解释成计算机能理解的指令。
public class Client {
@Test
public void test() {
Calculator calculator = new Calculator();
String expression1 = "一加一";
String expression2 = "一加一加一";
String expression3 = "二加五减三";
String expression4 = "七减五加四减一";
String expression5 = "九减五加三减一";
// 输出: 一加一 等于 2
System.out.println(expression1 + " 等于 " + calculator.calculate(expression1));
// 输出: 一加一加一 等于 3
System.out.println(expression2 + " 等于 " + calculator.calculate(expression2));
// 输出: 二加五减三 等于 4
System.out.println(expression3 + " 等于 " + calculator.calculate(expression3));
// 输出: 七减五加四减一 等于 5
System.out.println(expression4 + " 等于 " + calculator.calculate(expression4));
// 输出: 九减五加三减一 等于 6
System.out.println(expression5 + " 等于 " + calculator.calculate(expression5));
}
}
这就是解释器模式,我们将一句中文的公式解释给计算机,然后计算机为我们运算出了正确的结果。
分析本例中公式的组成,我们可以发现几条显而易见的性质:
数字类不可被拆分,属于计算中的最小单元;
加法类、减法类可以被拆分成两个数字(或两个公式)加一个计算符,他们不是计算的最小单元。
在解释器模式中,我们将不可拆分的最小单元称之为终结表达式,可以被拆分的表达式称之为非终结表达式。
解释器模式具有一定的拓展性,当需要添加其他计算符时,我们可以通过添加 Operator 的子类来完成。但添加后需要按照运算优先级修改计算规则。可见一个完整的解释器模式是非常复杂的,实际开发中几乎没有需要自定义解释器的情况。
解释器模式有一个常见的应用,在我们平时匹配字符串时,用到的正则表达式就是一个解释器。正则表达式中,表示一个字符的表达式属于终结表达式,除终结表达式外的所有表达式都属于非终结表达式。
迭代器模式
设想一个场景:我们有一个类中存在一个列表。这个列表需要提供给外部类访问,但我们不希望外部类修改其中的数据。
public class MyList {
private List<String> data = Arrays.asList("a", "b", "c");
}
通常来说,将成员变量提供给外部类访问有两种方式:
将此列表设置为 public 变量;
添加 getData() 方法,返回此列表。
但这两种方式都有一个致命的缺点,它们无法保证外部类不修改其中的数据。外部类拿到 data 对象后,可以随意修改列表内部的元素,这会造成极大的安全隐患。
分析可知,我们可以通过提供两个方法实现此效果:
提供一个 String next() 方法,使得外部类可以按照次序,一条一条的读取数据;
提供一个 boolean hasNext() 方法,告知外部类是否还有下一条数据。
代码实现如下:
public class MyList {
private List<String> data = Arrays.asList("a", "b", "c");
private int index = 0;
public String next() {
// 返回数据后,将 index 加 1,使得下次访问时返回下一条数据
return data.get(index++);
}
public boolean hasNext() {
return index < data.size();
}
}
客户端就可以使用一个 while 循环来访问此列表了:
public class Client {
@Test
public void test() {
MyList list = new MyList();
// 输出:abc
while (list.hasNext()) {
System.out.print(list.next());
}
}
}
由于没有给外部类暴露 data 成员变量,所以我们可以保证数据是安全的。
但这样的实现还有一个问题:当遍历完成后,hasNext() 方法就会一直返回 false,无法再一次遍历了,所以我们必须在一个合适的地方把 index 重置成 0。
在哪里重置比较合适呢?实际上,使用 next() 方法和 hasNext() 方法来遍历列表是一个完全通用的方法,我们可以为其创建一个接口,取名为 Iterator,Iterator 的意思是迭代器,迭代的意思是重复反馈,这里是指我们依次遍历列表中的元素。
public interface Iterator {
boolean hasNext();
String next();
}
然后在 MyList 类中,每次遍历时生成一个迭代器,将 index 变量放到迭代器中。由于每个迭代器都是新生成的,所以每次遍历时的 index 自然也就被重置成 0 了。代码如下:
public class MyList {
private List<String> data = Arrays.asList("a", "b", "c");
// 每次生成一个新的迭代器,用于遍历列表
public Iterator iterator() {
return new Itr();
}
private class Itr implements Iterator {
private int index = 0;
@Override
public boolean hasNext() {
return index < data.size();
}
@Override
public String next() {
return data.get(index++);
}
}
}
客户端访问此列表的代码修改如下:
public class Client {
@Test
public void test() {
MyList list = new MyList();
// 获取迭代器,用于遍历列表
Iterator iterator = list.iterator();
// 输出:abc
while (iterator.hasNext()) {
System.out.print(iterator.next());
}
}
}
迭代器模式(Iterator Pattern):提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。
迭代器模式的核心就在于定义出 next() 方法和 hasNext() 方法,让外部类使用这两个方法来遍历列表,以达到隐藏列表内部细节的目的。
事实上,Java 已经为我们内置了 Iterator 接口,源码中使用了泛型使得此接口更加的通用:
public interface Iterator<E> {
boolean hasNext();
E next();
}
并且,本例中使用的迭代器模式是仿照 ArrayList 的源码实现的,ArrayList 源码中使用迭代器模式的部分代码如下:
public class ArrayList<E> {
...
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
protected int limit = ArrayList.this.size;
int cursor;
public boolean hasNext() {
return cursor < limit;
}
public E next() {
...
}
}
}
我们平时常用的 for-each 循环,也是迭代器模式的一种应用。在 Java 中,只要实现了 Iterable 接口的类,都被视为可迭代访问的。Iterable 中的核心方法只有一个,也就是刚才我们在 MyList 类中实现过的用于获取迭代器的 iterator() 方法:
public interface Iterable<T> {
Iterator<T> iterator();
}
只要我们将 MyList 类修改为继承此接口,便可以使用 for-each 来迭代访问其中的数据了:
public class MyList implements Iterable<String> {
private List<String> data = Arrays.asList("a", "b", "c");
@NonNull
@Override
public Iterator<String> iterator() {
// 每次生成一个新的迭代器,用于遍历列表
return new Itr();
}
private class Itr implements Iterator<String> {
private int index = 0;
@Override
public boolean hasNext() {
return index < data.size();
}
@Override
public String next() {
return data.get(index++);
}
}
}
客户端使用 for-each 访问:
public class Client {
@Test
public void test() {
MyList list = new MyList();
// 输出:abc
for (String item : list) {
System.out.print(item);
}
}
}
这就是迭代器模式。基本上每种语言都会在源码层面为所有列表提供迭代器,我们只需要直接拿来用即可,这是一个比较简单又很常用的设计模式。
中介者模式
当类与类之间的关系呈现网状时,引入一个中介者,可以使类与类之间的关系变成星形。将每个类与多个类的耦合关系简化为每个类与中介者的耦合关系。
举个例子,在我们打麻将时,每两个人之间都可能存在输赢关系。如果每笔交易都由输家直接发给赢家,就会出现一种网状耦合关系。
玩家类:
class Player {
// 初始资金 100 元
public int money = 100;
public void win(Player player, int money) {
// 输钱的人扣减相应的钱
player.money -= money;
// 自己的余额增加
this.money += money;
}
}
此类中有一个 money 变量,表示自己的余额。当自己赢了某位玩家的钱时,调用 win 方法修改输钱的人和自己的余额。
需要注意的是,我们不需要输钱的方法,因为在 win 方法中,已经将输钱的人对应余额扣除了。
客户端代码:
public class Client {
@Test
public void test() {
Player player1 = new Player();
Player player2 = new Player();
Player player3 = new Player();
Player player4 = new Player();
// player1 赢了 player3 5 元
player1.win(player3, 5);
// player2 赢了 player1 10 元
player2.win(player1, 10);
// player2 赢了 player4 10 元
player2.win(player4, 10);
// player4 赢了 player3 7 元
player4.win(player3, 7);
// 输出:四人剩余的钱:105,120,88,97
System.out.println("四人剩余的钱:" + player1.money + "," + player2.money + "," + player3.money + "," + player4.money);
}
}
在客户端中,每两位玩家需要进行交易时,都会增加程序耦合度,相当于每位玩家都需要和其他所有玩家打交道,这是一种不好的做法。
此时,我们可以引入一个中介类——微信群,只要输家将自己输的钱发到微信群里,赢家从微信群中领取对应金额即可。网状的耦合结构就变成了星形结构:
此时,微信群就充当了一个中介者的角色,由它来负责与所有人进行交易,每个玩家只需要与微信群打交道即可。
微信群类:
class Group {
public int money;
}
此类中只有一个 money 变量表示群内的余额。
玩家类修改如下:
class Player {
public int money = 100;
public Group group;
public Player(Group group) {
this.group = group;
}
public void change(int money) {
// 输了钱将钱发到群里 或 在群里领取自己赢的钱
group.money += money;
// 自己的余额改变
this.money += money;
}
}
玩家类中新增了一个构造方法,在构造方法中将中介者传进来。每当自己有输赢时,只需要将钱发到群里或者在群里领取自己赢的钱,然后修改自己的余额即可。
客户端代码对应修改如下:
public class Client {
@Test
public void test(){
Group group = new Group();
Player player1 = new Player(group);
Player player2 = new Player(group);
Player player3 = new Player(group);
Player player4 = new Player(group);
// player1 赢了 5 元
player1.change(5);
// player2 赢了 20 元
player2.change(20);
// player3 输了 12 元
player3.change(-12);
// player4 输了 3 元
player4.change(-3);
// 输出:四人剩余的钱:105,120,88,97
System.out.println("四人剩余的钱:" + player1.money + "," + player2.money + "," + player3.money + "," + player4.money);
}
}
可以看到,通过引入中介者,客户端的代码变得更加清晰了。大家不需要再互相打交道,所有交易通过中介者完成即可。
事实上,这段代码还存在一点不足。因为我们忽略了一个前提:微信群里的钱不可以为负数。也就是说,输家必须先将钱发到微信群内,赢家才能去微信群里领钱。
备忘录模式
备忘录模式最常见的实现莫过于游戏中的存档、读档功能了,通过存档、读档,使得我们可以随时恢复到之前的状态。
玩家类:
class Player {
// 生命值
private int life = 100;
// 魔法值
private int magic = 100;
public void fightBoss() {
life -= 100;
magic -= 100;
if (life <= 0) {
System.out.println("壮烈牺牲");
}
}
public int getLife() {
return life;
}
public void setLife(int life) {
this.life = life;
}
public int getMagic() {
return magic;
}
public void setMagic(int magic) {
this.magic = magic;
}
}
我们为玩家定义了两个属性:生命值和魔法值。其中有一个 fightBoss() 方法,每次打 Boss 都会扣减 100 点体力。如果生命值小于等于 0,则提示用户已“壮烈牺牲”。
客户端实现如下:
public class Client {
@Test
public void test() {
Player player = new Player();
// 存档
int savedLife = player.getLife();
int savedMagic = player.getMagic();
// 打 Boss,打不过,壮烈牺牲
player.fightBoss();
// 读档,恢复到打 Boss 之前的状态
player.setLife(savedLife);
player.setMagic(savedMagic);
}
}
备忘录模式也应该采取类似的做法。我们不应该采用将单个属性挨个存取的方式来进行读档、存档。更好的做法是将存档、读档交给需要存档的类内部去实现。
新建备忘录类:
class Memento {
int life;
int magic;
Memento(int life, int magic) {
this.life = life;
this.magic = magic;
}
}
玩家类中,通过备忘录类实现存档、读档:
class Player {
...
// 存档
public Memento saveState() {
return new Memento(life, magic);
}
// 读档
public void restoreState(Memento memento) {
this.life = memento.life;
this.magic = memento.magic;
}
}
客户端类对应修改如下:
public class Client {
@Test
public void test() {
Player player = new Player();
// 存档
Memento memento = player.saveState();
// 打 Boss,打不过,壮烈牺牲
player.fightBoss();
// 读档
player.restoreState(memento);
}
}
备忘录模式:
在不破坏封装的条件下,通过备忘录对象存储另外一个对象内部状态的快照,在将来合适的时候把这个对象还原到存储起来的状态。
观察者模式
观察者模式的思想就是一个对象发生一个事件后,逐一通知监听着这个对象的监听者,监听者可以对这个事件马上做出响应。
当我们打开灯的开关时,灯马上亮了;当我们关闭灯的开关时,灯马上熄了。这个过程中,灯就对我们控制开关的事件做出了响应,这就是一个最简单的一对一观察者模式。
警察称之为观察者(Observer)
张三称之为被观察者(Observable,可观察的)
警察观察张三的这个行为称之为订阅(subscribe),或者注册(register)
张三违法后,警察抓捕张三的行动称之为响应(update)
众所周知,张三坏事做尽,是一个老法外狂徒了,所以不止一个警察会盯着张三,也就是说一个被观察者可以有多个观察者。当被观察者有事件发生时,所有观察者都能收到通知并响应。观察者模式主要处理的是一种一对多的依赖关系。它的定义如下:
观察者模式(Observer Pattern):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者的接口:
public interface Observer {
void update(String event);
}
被观察者的父类:
public class Observable {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
observers.remove(observer);
}
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
}
addObserver:将 observer 对象添加到观察者列表中
removeObserver:将 observer 对象从观察者列表中移除
notifyObservers:通知所有观察者有事件发生,具体实现是调用所有观察者的 update 方法
警察属于观察者:
public class PoliceObserver implements Observer {
@Override
public void update(String event) {
System.out.println("警察收到消息,罪犯在" + event);
}
}
罪犯属于被观察者:
public class CriminalObservable extends Observable {
public void crime(String event) {
System.out.println("罪犯正在" + event);
notifyObservers(event);
}
}
Java 源码中的观察者模式
实际上,Java 已经为我们提供了的 Observable 类和 Observer 类,我们在用到观察者模式时,无需自己创建这两个基类,我们来看一下 Java 中提供的源码:
java.util.Observer 类:
public interface Observer {
void update(Observable o, Object arg);
}
Observer 类和我们上例中的定义基本一致,都是只有一个 update 方法用于响应 Observable 的事件。区别有两点:
update 方法将 Observable 对象也提供给了 Observer
update 方法中的参数类型变成了 Object
java.util.Observable 类:
public class Observable {
private boolean changed = false;
private Vector<Observer> obs;
public Observable() {
obs = new Vector<>();
}
public synchronized void addObserver(java.util.Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
public synchronized void deleteObserver(java.util.Observer o) {
obs.removeElement(o);
}
public void notifyObservers() {
notifyObservers(null);
}
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!hasChanged())
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length - 1; i >= 0; i--)
((Observer) arrLocal[i]).update(this, arg);
}
public synchronized void deleteObservers() {
obs.removeAllElements();
}
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
public synchronized boolean hasChanged() {
return changed;
}
public synchronized int countObservers() {
return obs.size();
}
}
Observable 类和我们上例中的定义也是类似的,区别在于:
用于保存观察者列表的容器不是 ArrayList,而是 Vector
添加了一个 changed 字段,以及 setChanged 和 clearChanged 方法。分析可知,当 changed 字段为 true 时,才会通知所有观察者,否则不通知观察者。所以当我们使用此类时,想要触发 notifyObservers 方法,必须先调用 setChanged 方法。这个字段相当于在被观察者和观察者之间添加了一个可控制的阀门。
提供了 countObservers 方法,用于计算观察者数量
添加了一些 synchronized 关键字保证线程安全
状态模式
状态模式(State Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
如果一个对象有多种状态,并且每种状态下的行为不同,一般的做法是在这个对象的各个行为中添加 if-else 或者 switch-case 语句。但更好的做法是为每种状态创建一个状态对象,使用状态对象替换掉这些条件判断语句,使得状态控制更加灵活,扩展性也更好。
当普通用户点击模拟面试功能时,提示用户:模拟面试是 Plus 会员专享功能;
当 PLUS 会员点击模拟面试功能时,开始一场模拟面试。
定义一个用户状态枚举类:
public enum State {
NORMAL, PLUS
}
NORMAL 代表普通用户状态,PLUS 代表 PLUS 会员状态。
用户的功能接口:
public interface IUser {
void mockInterview();
}
用户状态切换接口:
public interface ISwitchState {
void purchasePlus();
void expire();
}
此接口中定义了两个方法:purchasePlus 方法表示购买 Plus 会员,用户状态变为 PLUS 会员状态,expire 方法表示会员过期,用户状态变为普通用户状态。
用户类:
public class User implements IUser, ISwitchState {
private State state = State.NORMAL;
@Override
public void mockInterview() {
if (state == State.PLUS) {
System.out.println("开始模拟面试");
} else {
System.out.println("模拟面试是 Plus 会员专享功能");
}
}
@Override
public void purchasePlus() {
state = State.PLUS;
}
@Override
public void expire() {
state = State.NORMAL;
}
}
用户类实现了 IUser 接口,IUser 接口中的每个功能都需要判断用户是否为 Plus 会员,也就是说每个方法中都有 if (state == State.PLUS) {} else {} 语句,如果状态不止两种,还需要用上 switch-case 语句来判断状态,这就是不使用状态模式的弊端:
应使用多态取代条件表达式。
接下来我们就利用多态特性重构这份代码。为每个状态新建一个状态类,普通用户:
class Normal implements IUser {
@Override
public void mockInterview() {
System.out.println("模拟面试是 Plus 会员专享功能");
}
}
PLUS 会员:
class Plus implements IUser {
@Override
public void mockInterview() {
System.out.println("开始模拟面试");
}
}
每个状态类都实现了 IUser 接口,在接口方法中实现自己特定的行为。
用户类:
class User implements IUser, ISwitchState {
IUser state = new Normal();
@Override
public void mockInterview() {
state.mockInterview();
}
@Override
public void purchasePlus() {
state = new Plus();
}
@Override
public void expire() {
state = new Normal();
}
}
User 类都只需要调用状态类的对应方法即可。
客户端测试:
public class Client {
@Test
public void test() {
// 用户初始状态为普通用户
User user = new User();
// 输出:模拟面试是 Plus 会员专享功能
user.mockInterview();
// 用户购买 Plus 会员,状态改变
user.purchasePlus();
// 输出:开始模拟面试
user.mockInterview();
// Plus 会员过期,变成普通用户,状态改变
user.expire();
// 输出:模拟面试是 Plus 会员专享功能
user.mockInterview();
}
}
可以看到,用户状态改变后,行为也随着改变了,这就是状态模式定义的由来,它的优点是:将与特定状态相关的行为封装到一个状态对象中,
策略模式
策略模式(Strategy Pattern):定义了一系列算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
我们以排序算法为例。排序算法有许多种,如冒泡排序、选择排序、插入排序,算法不同但目的相同,我们可以将其定义为不同的策略,让用户自由选择采用哪种策略完成排序。
首先定义排序算法接口:
interface ISort {
void sort(int[] arr);
}
接口中只有一个 sort 方法,传入一个整型数组进行排序,所有的排序算法都实现此接口。
冒泡排序:
class BubbleSort implements ISort{
@Override
public void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 如果左边的数大于右边的数,则交换,保证右边的数字最大
arr[j + 1] = arr[j + 1] + arr[j];
arr[j] = arr[j + 1] - arr[j];
arr[j + 1] = arr[j + 1] - arr[j];
}
}
}
}
}
选择排序:
class SelectionSort implements ISort {
@Override
public void sort(int[] arr) {
int minIndex;
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
// 记录最小值的下标
minIndex = j;
}
}
// 将最小元素交换至首位
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
插入排序:
class InsertSort implements ISort {
@Override
public void sort(int[] arr) {
// 从第二个数开始,往前插入数字
for (int i = 1; i < arr.length; i++) {
int currentNumber = arr[i];
int j = i - 1;
// 寻找插入位置的过程中,不断地将比 currentNumber 大的数字向后挪
while (j >= 0 && currentNumber < arr[j]) {
arr[j + 1] = arr[j];
j--;
}
// 两种情况会跳出循环:1. 遇到一个小于或等于 currentNumber 的数字,跳出循环,currentNumber 就坐到它后面。
// 2. 已经走到数列头部,仍然没有遇到小于或等于 currentNumber 的数字,也会跳出循环,此时 j 等于 -1,currentNumber 就坐到数列头部。
arr[j + 1] = currentNumber;
}
}
}
这三种都是基本的排序算法,就不再详细介绍了。接下来我们需要创建一个环境类,将每种算法都作为一种策略封装起来,客户端将通过此环境类选择不同的算法完成排序。
class Sort implements ISort {
private ISort sort;
Sort(ISort sort) {
this.sort = sort;
}
@Override
public void sort(int[] arr) {
sort.sort(arr);
}
// 客户端通过此方法设置不同的策略
public void setSort(ISort sort) {
this.sort = sort;
}
}
setSort 方法就是用来选择不同的排序策略的,客户端调用如下:
public class Client {
@Test
public void test() {
int[] arr = new int[]{6, 1, 2, 3, 5, 4};
Sort sort = new Sort(new BubbleSort());
// 这里可以选择不同的策略完成排序
// sort.setSort(new InsertSort());
// sort.setSort(new SelectionSort());
sort.sort(arr);
// 输出 [1, 2, 3, 4, 5, 6]
System.out.println(Arrays.toString(arr));
}
}
所以使用策略模式时,更好的做法是与工厂模式结合,将不同的策略对象封装到工厂类中,用户只需要传递不同的策略类型,然后从工厂中拿到对应的策略对象即可。接下来我们就来一起实现这种工厂模式与策略模式结合的混合模式。
创建排序策略枚举类:
enum SortStrategy {
BUBBLE_SORT,
SELECTION_SORT,
INSERT_SORT
}
在 Sort 类中使用简单工厂模式:
class Sort implements ISort {
private ISort sort;
Sort(SortStrategy strategy) {
setStrategy(strategy);
}
@Override
public void sort(int[] arr) {
sort.sort(arr);
}
// 客户端通过此方法设置不同的策略
public void setStrategy(SortStrategy strategy) {
switch (strategy) {
case BUBBLE_SORT:
sort = new BubbleSort();
break;
case SELECTION_SORT:
sort = new SelectionSort();
break;
case INSERT_SORT:
sort = new InsertSort();
break;
default:
throw new IllegalArgumentException("There's no such strategy yet.");
}
}
}
利用简单工厂模式,我们将创建策略类的职责移到了 Sort 类中。如此一来,客户端只需要和 Sort 类打交道,通过 SortStrategy 选择不同的排序策略即可。
客户端:
public class Client {
@Test
public void test() {
int[] arr = new int[]{6, 1, 2, 3, 5, 4};
Sort sort = new Sort(SortStrategy.BUBBLE_SORT);
// 可以通过选择不同的策略完成排序
// sort.setStrategy(SortStrategy.SELECTION_SORT);
// sort.setStrategy(SortStrategy.INSERT_SORT);
sort.sort(arr);
// 输出 [1, 2, 3, 4, 5, 6]
System.out.println(Arrays.toString(arr));
}
}
模板方法模式
模板方法模式(Template Method Pattern):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
通俗地说,模板方法模式就是一个关于继承的设计模式。
每一个被继承的父类都可以认为是一个模板,它的某些步骤是稳定的,某些步骤被延迟到子类中实现。
本人 ___ 因 ___ 需请假 ___ 天,望批准!
这个模板用代码表示如下
abstract class LeaveRequest {
void request() {
System.out.print("本人");
System.out.print(name());
System.out.print("因");
System.out.print(reason());
System.out.print("需请假");
System.out.print(duration());
System.out.print("天,望批准");
}
abstract String name();
abstract String reason();
abstract String duration();
}
在这份模板中,所有的其他步骤(固定字符串)都是稳定的,只有姓名、请假原因、请假时长是抽象的,需要延迟到子类去实现。
继承此模板,实现具体步骤的子类:
class MyLeaveRequest extends LeaveRequest {
@Override
String name() {
return "阿笠";
}
@Override
String reason() {
return "参加力扣周赛";
}
@Override
String duration() {
return "0.5";
}
}
在使用模板方法模式时,我们可以为不同的模板方法设置不同的控制权限:
如果不希望子类覆写模板中的某个方法,使用 final 修饰此方法;
如果要求子类必须覆写模板中的某个方法,使用 abstract 修饰此方法
如果没有特殊要求,可使用 protected 或 public 修饰此方法,子类可根据实际情况考虑是否覆写。
访问者模式
所以餐厅的做法是将所有的食物都准备好,顾客按照需求自由取用。此时,顾客和餐厅之间就形成了一种访问者与被访问者的关系。
准备好各种食物的餐厅:
class Restaurant {
private String lobster = "lobster";
private String watermelon = "watermelon";
private String steak = "steak";
private String banana = "banana";
}
为顾客提供的接口:
public interface IVisitor {
void chooseLobster(String lobster);
void chooseWatermelon(String watermelon);
void chooseSteak(String steak);
void chooseBanana(String banana);
}
接口中提供了四个方法, 让顾客依次选择每种食物。
在餐厅中提供接收访问者的方法:
class Restaurant {
...
public void welcome(IVisitor visitor) {
visitor.chooseLobster(lobster);
visitor.chooseWatermelon(watermelon);
visitor.chooseSteak(steak);
visitor.chooseBanana(banana);
}
}
比如顾客 Aurora 类:
public class Aurora implements IVisitor {
@Override
public void chooseLobster(String lobster) {
System.out.println("Aurora gets a " + lobster);
}
@Override
public void chooseWatermelon(String watermelon) {
System.out.println("Aurora gets a " + watermelon);
}
@Override
public void chooseSteak(String steak) {
System.out.println("Aurora doesn't like " + steak);
}
@Override
public void chooseBanana(String banana) {
System.out.println("Aurora doesn't like " + banana);
}
}
客户端测试:
public class Client {
@Test
public void test() {
Restaurant restaurant = new Restaurant();
IVisitor Aurora = new Aurora();
restaurant.welcome(Aurora);
}
}
可以看到,Aurora 对每一种食物做出了自己的选择,这就是一个最简单的访问者模式,它已经体现出了访问者模式的核心思想:将数据的结构和对数据的操作分离。
我们并没有在餐厅类中处理顾客的需求,而是将所有的食物通过接口暴露出去,欢迎每位顾客来访问。顾客只要实现访问者接口就能访问到所有的食物,然后在接口方法中做出自己的选择。
单分派与双重分派
Food 类:
public class Food {
public String name() {
return “food”;
}
}
Watermelon 类,继承自 Food 类:
public class Watermelon extends Food {
@Override
public String name() {
return “watermelon”;
}
}
在 Watermelon 类中,我们重写了name()方法。
客户端:
public class Client {
@Test
public void test() {
Food food = new Watermelon();
System.out.println(food.name());
}
}
再来看一段测试代码:
public class Client {
@Test
public void test() {
Food food = new Watermelon();
eat(food);
}
public void eat(Food food) {
System.out.println("eat food");
}
public void eat(Watermelon watermelon) {
System.out.println("eat watermelon");
}
}
在这段代码中,我们仍然 new 出了一个 Watermelon 对象,他的声明类型是 Food,在客户端中有eat(Food food)和eat(Watermelon watermelon)两个重载方法,这段代码会调用哪一个方法呢?
我们运行这段代码会发现输出的是:
eat food
这是由于 Java 在调用重载方法时,只会根据方法签名中声明的参数类型来判断调用哪个方法,不会去判断参数运行时的具体类型是什么。
从这两个例子中,我们可以看出 Java 对重写方法和重载方法的调用方式是不同的。
调用重写方法时,与对象的运行时类型有关;
调用重载方法时,只与方法签名中声明的参数类型有关,与对象运行时的具体类型无关。
了解了重写方法和重载方法调用方式的区别之后,我们将其综合起来就能理解何谓双重分派了。
测试代码:
public class Client {
@Test
public void test() {
Food food = new Watermelon();
eat(food);
}
public void eat(Food food) {
System.out.println("eat food: " + food.name());
}
public void eat(Watermelon watermelon) {
System.out.println("eat watermelon" + watermelon.name());
}
}
在这段测试代码中,仍然是 new 出了一个 Watermelon 对象,它的声明类型为 Food。运行test()函数,输出如下:
eat food: watermelon
在面向对象的编程语言中,我们将方法调用称之为分派,这段测试代码运行时,经过了两次分派:
调用重载方法:选择调用eat(Food food)还是eat(Watermelon watermelon)。虽然这里传入的这个参数实际类型是Watermelon,但这里会调用eat(Food food),这是由于调用哪个重载方法是在编译期就确定了的,也称之为静态分派。
调用重写方法:选择调用Food的name方法还是Watermelon的name方法。这里会根据参数运行时的实际类型,调用Watermelon的name方法,称之为动态分派。
在面向对象的编程语言中,我们将方法调用称之为分派,这段测试代码运行时,经过了两次分派:
这段定义可能不太好理解,通俗地讲,
单分派和双重分派的区别就是,程序在选择重载方法和重写方法时:
如果两种情况都是动态分派的,则称之为双重分派;
如果其中一种情况是动态分派,另一种是静态分派,则称之为单分派。
自助餐程序 2.0 版
在上面的例子中,为了突出访问者模式的特点,我们将每种食物都简化为了 String 类型,实际开发中,每种食物都应该是一个单独的对象,统一继承自父类 Food:
public abstract class Food {
public abstract String name();
}
继承自 Food 的四种食物:
龙虾:
public class Lobster extends Food {
@Override
public String name() {
return "lobster";
}
}
西瓜:
public class Watermelon extends Food {
@Override
public String name() {
return "watermelon";
}
}
牛排:
public class Steak extends Food {
@Override
public String name() {
return "steak";
}
}
香蕉:
public class Banana extends Food {
@Override
public String name() {
return "banana";
}
}
IVisitor 接口对应修改为:
public interface IVisitor {
void chooseFood(Lobster lobster);
void chooseFood(Watermelon watermelon);
void chooseFood(Steak steak);
void chooseFood(Banana banana);
}
每种食物都继承自 Food,所以我们将接口中的方法名都修改为了 chooseFood。
餐厅类修改如下:
class Restaurant {
// 准备当天的食物
private List<Food> prepareFoods() {
List<Food> foods = new ArrayList<>();
// 简单模拟,每种食物添加 10 份
for (int i = 0; i < 10; i++) {
foods.add(new Lobster());
foods.add(new Watermelon());
foods.add(new Steak());
foods.add(new Banana());
}
return foods;
}
// 欢迎顾客来访
public void welcome(IVisitor visitor) {
// 获取当天的食物
List<Food> foods = prepareFoods();
// 将食物依次提供给顾客选择
for (Food food : foods) {
// 由于单分派机制,此处无法编译通过
visitor.chooseFood(food);
}
}
}
餐厅类中新增了prepareFoods方法,在这个方法中,我们简单模拟了准备多个食物的过程,将每种食物添加了 10 份。在接收访问者的welcome方法中,遍历所有食物,分别提供给顾客。
看起来很美好,实际上,visitor.chooseFood(food)这一行是无法编译通过的,原因就在于上一节中提到的单分派机制。虽然每种食物都继承自 Food 类,但由于接口中没有chooseFood(Food food)这个重载方法,所以这一行会报错"Cannot resolve method chooseFood"。
试想,如果 Java 在调用重载方法时也采用动态分派,也就是根据参数的运行时类型选择对应的重载方法,这里遇到的问题就迎刃而解了,我们的访问者模式讲到这里也就可以结束了。
但由于 Java 是单分派语言,所以我们不得不想办法解决这个 bug,目的就是使用单分派的 Java 语言模拟出双分派的效果,能够根据运行时的具体类型调用对应的重载方法。
我们很容易想到一种解决方式,采用 instanceOf 判断对象的具体子类型,再将父类强制转换为具体子类型,调用对应的接口方法:
// 通过 instanceOf 判断具体子类型,再强制向下转型
if (food instanceof Lobster) visitor.chooseFood((Lobster) food);
else if (food instanceof Watermelon) visitor.chooseFood((Watermelon) food);
else if (food instanceof Steak) visitor.chooseFood((Steak) food);
else if (food instanceof Banana) visitor.chooseFood((Banana) food);
else throw new IllegalArgumentException("Unsupported type of food.");
首先在 Food 类中添加 accept(Visitor visitor) 抽象方法:
public abstract class Food {
public abstract String name();
// Food 中添加 accept 方法,接收访问者
public abstract void accept(IVisitor visitor);
}
在具体子类中,实现此方法:
public class Lobster extends Food {
@Override
public String name() {
return "lobster";
}
@Override
public void accept(IVisitor visitor) {
visitor.chooseFood(this);
}
}
经过这两步修改,餐厅类就可以将接收访问者的方法修改如下:
class Restaurant {
// 准备当天的食物
private List<Food> prepareFoods() {
List<Food> foods = new ArrayList<>();
// 简单模拟,每种食物添加 10 份
for (int i = 0; i < 10; i++) {
foods.add(new Lobster());
foods.add(new Watermelon());
foods.add(new Steak());
foods.add(new Banana());
}
return foods;
}
// 欢迎顾客来访
public void welcome(IVisitor visitor) {
// 获取当天的食物
List<Food> foods = prepareFoods();
// 将食物依次提供给顾客选择
for (Food food : foods) {
// 由于重写方法是动态分派的,所以这里会调用具体子类的 accept 方法,
food.accept(visitor);
}
}
}
经过这三步修改,我们将访问者来访的代码由:
visitor.chooseFood(food);
改成了
food.accept(visitor);
这样我们就将重载方法模拟成了动态分派。这里的实现非常巧妙,由于 Java 调用重写方法时是动态分派的,所以food.accept(visitor)会调用具体子类的 accept 方法,在具体子类的 accept 方法中,调用visitor.chooseFood(this),由于这个 accept 方法是属于具体子类的,所以这里的 this 一定是指具体的子类型,不会产生歧义。
顾客 Aurora 类:
public class Aurora implements IVisitor {
@Override
public void chooseFood(Lobster lobster) {
System.out.println("Aurora gets a " + lobster.name());
}
@Override
public void chooseFood(Watermelon watermelon) {
System.out.println("Aurora gets a " + watermelon.name());
}
@Override
public void chooseFood(Steak steak) {
System.out.println("Aurora doesn't like " + steak.name());
}
@Override
public void chooseFood(Banana banana) {
System.out.println("Aurora doesn't like " + banana.name());
}
}
顾客 Kevin 类:
public class Kevin implements IVisitor {
@Override
public void chooseFood(Lobster lobster) {
System.out.println("Kevin doesn't like " + lobster.name());
}
@Override
public void chooseFood(Watermelon watermelon) {
System.out.println("Kevin doesn't like " + watermelon.name());
}
@Override
public void chooseFood(Steak steak) {
System.out.println("Kevin gets a " + steak.name());
}
@Override
public void chooseFood(Banana banana) {
System.out.println("Kevin gets a " + banana.name());
}
}
客户端测试:
public class Client {
@Test
public void test() {
Restaurant restaurant = new Restaurant();
IVisitor Aurora = new Aurora();
IVisitor Kevin = new Kevin();
restaurant.welcome(Aurora);
restaurant.welcome(Kevin);
}
}
这就是访问者模式,它的核心思想其实非常简单,就是第一小节中体现的将数据的结构与对数据的操作分离。之所以说它复杂,主要在于大多数语言都是单分派语言,所以不得不模拟出一个双重分派,也就是用重写方法的动态分派特性将重载方法也模拟成动态分派。
参考 https://zhuanlan.zhihu.com/p/166579495
参考 https://zhuanlan.zhihu.com/p/91836046
参考 https://zhuanlan.zhihu.com/p/259864836