记录生活中的点点滴滴

0%

设计模式

这篇md文件从15号建立,今天算是完成了,写了十多天。这一遍算是就是了解一下设计模式,就是入入门,把这十多天学习的过程总结一下,方便日后继续学习。反正感觉设计模式还是很牛逼的,就是它可以让你喜欢上这些代码,反正学它不亏,挺好!

设计模式简介

软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现) 的各种问题,所提出的解决方案。

设计模式的目的

编写软件过程中,程序员面临着来自 耦合性,内聚性以及可维护性,可扩展性,重 用性,灵活性 等多方面的挑战,设计模式是为了让程序(软件),具有更好

  • 代码重用性 (即:相同功能的代码,不用多次编写)
  • 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
  • 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
  • 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
  • 使程序呈现高内聚,低耦合的特性

分享金句: 设计模式包含了面向对象的精髓,“懂了设计模式,你就懂了面向对象分析和设计 (OOA/D)的精要”

设计原则核心思想

  1. 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代 码混在一起。
  2. 针对接口编程,而不是针对实现编程。
  3. 为了交互对象之间的松耦合设计而努力

设计模式七大原则

  1. 单一职责原则
  2. 接口隔离原则
  3. 依赖倒转(倒置)原则
  4. 里氏替换原则
  5. 开闭原则
  6. 迪米特法则
  7. 合成复用原则

设计模式类型

  1. 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式。
  2. 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享 元模式、代理模式。
  3. 行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者 模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)。

单例模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

单例模式有八种方式:

  1. 饿汉式(静态常量)
  2. 饿汉式(静态代码块)
  3. 懒汉式(线程不安全)
  4. 懒汉式(线程安全,同步方法)
  5. 懒汉式(线程安全,同步代码块)
  6. 双重检查
  7. 静态内部类
  8. 枚举

饿汉式(静态变量)

1
2
3
4
5
6
7
8
9
10
//1.饿汉式(静态变量)
class Singleton1{
//构造器私有化,外部不能new
private Singleton1(){}
//本类内部创建对象实例
private static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance(){
return instance;
}
}

测试:

1
2
3
4
5
6
7
8
@Test
public void Test1(){
Singleton1 s1 = Singleton1.getInstance();
Singleton1 s2 = Singleton1.getInstance();
System.out.println("s1 == s2:" + (s1 == s2));
System.out.println("s1.hashCode():" + s1.hashCode());
System.out.println("s2.hashCode():" + s2.hashCode());
}

输出结果:

1
2
3
s1 == s2:true
s1.hashCode():1032986144
s2.hashCode():1032986144

优缺点说明:

1) 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同 步问题。

2) 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始 至终从未使用过这个实例,则会造成内存的浪费

3) 这种方式基于classloader机制避免了多线程的同步问题,不过,instance在类装载 时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载 的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类 装载,这时候初始化instance就没有达到lazy loading的效果

4) 结论:这种单例模式可用,可能造成内存浪费

饿汉式(静态代码块)

1
2
3
4
5
6
7
8
9
10
11
12
13
//2.饿汉式(静态代码块)
class Singleton2{
//构造器私有化,外部不能new
private Singleton2(){}
//本类内部创建对象实例
private static Singleton2 instance;
static {
instance = new Singleton2();
}
public static Singleton2 getInstance(){
return instance;
}
}

测试:

1
2
3
4
5
6
7
8
@Test
public void Test2(){
Singleton2 s1 = Singleton2.getInstance();
Singleton2 s2 = Singleton2.getInstance();
System.out.println("s1 == s2:" + (s1 == s2));
System.out.println("s1.hashCode():" + s1.hashCode());
System.out.println("s2.hashCode():" + s2.hashCode());
}

输出结果:

1
2
3
s1 == s2:true
s1.hashCode():1032986144
s2.hashCode():1032986144

优缺点说明:

1) 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块 中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优 缺点和上面是一样的。

2) 结论:这种单例模式可用,但是可能造成内存浪费

懒汉式(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
//3.懒汉式(线程不安全)
class Singleton3{
private Singleton3(){}
private static Singleton3 instance;
public static Singleton3 getInstance(){
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}

测试:

1
2
3
4
5
6
7
8
@Test
public void Test3(){
Singleton3 s1 = Singleton3.getInstance();
Singleton3 s2 = Singleton3.getInstance();
System.out.println("s1 == s2:" + (s1 == s2));
System.out.println("s1.hashCode():" + s1.hashCode());
System.out.println("s2.hashCode():" + s2.hashCode());
}

输出结果:

1
2
3
s1 == s2:true
s1.hashCode():1032986144
s2.hashCode():1032986144

优缺点说明:

1) 起到了Lazy Loading的效果,但是只能在单线程下使用。

2) 如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及 往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以 在多线程环境下不可使用这种方式

3) 结论:在实际开发中,不要使用这种方式.

既然线程不安全,我们可以测试一下,用多线程去获取实例,看看他们的hashCode是否一致:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void Test9(){
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton3 s = Singleton3.getInstance();
System.out.println(s.hashCode());
}
}).start();
}
}

输出结果:

可以看出,确实会出现线程不安全的情况,创建了多个单例

懒汉式(线程安全,同步方法)

1
2
3
4
5
6
7
8
9
10
11
//4.懒汉式(线程安全,同步方法)
class Singleton4{
private Singleton4(){}
private static Singleton4 instance;
public synchronized static Singleton4 getInstance(){
if(instance == null){
instance = new Singleton4();
}
return instance;
}
}

测试:

1
2
3
4
5
6
7
8
@Test
public void Test4(){
Singleton4 s1 = Singleton4.getInstance();
Singleton4 s2 = Singleton4.getInstance();
System.out.println("s1 == s2:" + (s1 == s2));
System.out.println("s1.hashCode():" + s1.hashCode());
System.out.println("s2.hashCode():" + s2.hashCode());
}

输出结果:

1
2
3
s1 == s2:true
s1.hashCode():1032986144
s2.hashCode():1032986144

优缺点说明:

1) 解决了线程不安全问题

2) 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行 同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例, 直接return就行了。方法进行同步效率太低

3) 结论:在实际开发中,不推荐使用这种方式

懒汉式(线程安全,同步代码块)

1
2
3
4
5
6
7
8
9
10
11
12
13
//5.懒汉式(线程安全,同步代码块)
class Singleton5{
private Singleton5(){}
private static Singleton5 instance;
public static Singleton5 getInstance(){
if(instance == null){
synchronized (Singleton5.class){
instance = new Singleton5();
}
}
return instance;
}
}

测试:

1
2
3
4
5
6
7
8
@Test
public void Test5(){
Singleton5 s1 = Singleton5.getInstance();
Singleton5 s2 = Singleton5.getInstance();
System.out.println("s1 == s2:" + (s1 == s2));
System.out.println("s1.hashCode():" + s1.hashCode());
System.out.println("s2.hashCode():" + s2.hashCode());
}

输出结果:

1
2
3
s1 == s2:true
s1.hashCode():1032986144
s2.hashCode():1032986144

优缺点说明:

1) 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低, 改为同步产生实例化的的代码块

2) 但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一 致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行, 另一个线程也通过了这个判断语句,这时便会产生多个实例

3) 结论:在实际开发中,不能使用这种方式

双重检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//6.双重检查
class Singleton6{
private Singleton6(){}
private static volatile Singleton6 instance;
public static Singleton6 getInstance(){
if(instance == null){
synchronized (Singleton6.class){
if(instance == null){
instance = new Singleton6();
}
}
}
return instance;
}
}

测试:

1
2
3
4
5
6
7
8
@Test
public void Test6(){
Singleton6 s1 = Singleton6.getInstance();
Singleton6 s2 = Singleton6.getInstance();
System.out.println("s1 == s2:" + (s1 == s2));
System.out.println("s1.hashCode():" + s1.hashCode());
System.out.println("s2.hashCode():" + s2.hashCode());
}

输出结果:

1
2
3
s1 == s2:true
s1.hashCode():1032986144
s2.hashCode():1032986144

优缺点说明:

1) Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两 次if (singleton == null)检查,这样就可以保证线程安全了

2) 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null), 直接return实例化对象,也避免的反复进行方法同步

3) 线程安全;延迟加载;效率较高

4) 结论:在实际开发中,推荐使用这种单例设计模式

静态内部类

1
2
3
4
5
6
7
8
9
10
//7.静态内部类
class Singleton7{
private Singleton7(){}
private static class SingletonInstance{
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance(){
return SingletonInstance.INSTANCE;
}
}

测试:

1
2
3
4
5
6
7
8
@Test
public void Test7(){
Singleton7 s1 = Singleton7.getInstance();
Singleton7 s2 = Singleton7.getInstance();
System.out.println("s1 == s2:" + (s1 == s2));
System.out.println("s1.hashCode():" + s1.hashCode());
System.out.println("s2.hashCode():" + s2.hashCode());
}

输出结果:

1
2
3
s1 == s2:true
s1.hashCode():917819120
s2.hashCode():917819120

优缺点说明:

1) 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。

2) 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化 时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的 实例化。

3) 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们 保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

4) 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高

5) 结论:推荐使用.

枚举

1
2
3
4
5
6
7
//8.枚举
enum Singleton8{
INSTANCE;
void sayHello(){
System.out.println("hello~~");
}
}

测试:

1
2
3
4
5
6
7
8
9
@Test
public void Test8(){
Singleton8 s1 = Singleton8.INSTANCE;
Singleton8 s2 = Singleton8.INSTANCE;
System.out.println("s1 == s2:" + (s1 == s2));
System.out.println("s1.hashCode():" + s1.hashCode());
System.out.println("s2.hashCode():" + s2.hashCode());
s1.sayHello();
}

输出结果:

1
2
3
4
s1 == s2:true
s1.hashCode():1032986144
s2.hashCode():1032986144
hello~~

优缺点说明:

1) 这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而 且还能防止反序列化重新创建新的对象。

2) 这种方式是Effective Java作者Josh Bloch 提倡的方式

3) 结论:推荐使用

工厂模式

工厂顾名思义就是创建产品,根据产品是具体产品还是具体工厂可分为简单工厂模式和工厂方法模式,根据工厂的抽象程度可分为工厂方法模式和抽象工厂模式。该模式用于封装和管理对象的创建,是一种创建型模式。

模式的问题:你如何能轻松方便地构造对象实例,而不必关心构造对象实例的细节和复杂过程呢?

解决方案:建立一个工厂来创建对象

简单工厂模式

1) 简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一 个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族 中最简单实用的模式

2) 简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行 为(代码)

3) 在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会 使用到工厂模式

该模式对对象创建管理方式最为简单,因为其仅仅简单的对不同类对象的创建进行了一层薄薄的封装。该模式通过向工厂传递类型来指定要创建的对象,其UML类图如下:

Phone类:手机标准规范类(AbstractProduct)

1
2
3
public interface Phone {
void make();
}

MiPhone类:制造小米手机(Product1)

1
2
3
4
5
6
7
8
9
public class MiPhone implements Phone {
public MiPhone(){
this.make();
}
@Override
public void make() {
System.out.println("make MiPhone!");
}
}

IPhone类:制造苹果手机(Product2)

1
2
3
4
5
6
7
8
9
public class IPhone implements Phone {
public IPhone(){
this.make();
}
@Override
public void make() {
System.out.println("make Apple Phone!");
}
}

PhoneFactory类:手机代工厂(Factory)

1
2
3
4
5
6
7
8
9
10
public class PhoneFactory {
public Phone makePhone(String phoneType){
if(phoneType.equalsIgnoreCase("MiPhone")){
return new MiPhone();
}else if(phoneType.equalsIgnoreCase("IPhone")){
return new IPhone();
}
return null;
}
}

测试:

1
2
3
4
5
@Test
public void Test1(){
new PhoneFactory().makePhone("IPhone"); //make Apple Phone!
new PhoneFactory().makePhone("MiPhone"); //make MiPhone!
}

工厂方法模式

和简单工厂模式中工厂负责生产所有产品相比,工厂方法模式将生成具体产品的任务分发给具体的产品工厂,其UML类图如下:

定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方 法模式将对象的实例化推迟到子类

AbstractFactory类:生产不同产品的工厂的抽象类

1
2
3
public interface AbstractFactory {
Phone makePhone();
}

XiaoMiFactory类:生产小米手机的工厂(ConcreteFactory1)

1
2
3
4
5
6
public class XiaoMiFactory implements AbstractFactory {
@Override
public Phone makePhone() {
return new MiPhone();
}
}

AppleFactory类:生产苹果手机的工厂(ConcreteFactory2)

1
2
3
4
5
6
public class AppleFactory implements AbstractFactory {
@Override
public Phone makePhone() {
return new IPhone();
}
}

测试:

1
2
3
4
5
@Test
public void Test2(){
new XiaoMiFactory().makePhone(); //make MiPhone!
new AppleFactory().makePhone(); //make Apple Phone!
}

抽象工厂模式

1) 抽象工厂模式:定义了一个interface用于创建相关或有依赖关系的对象簇,而无需 指明具体的类

2) 抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合。

3) 从设计层面看,抽象工厂模式就是对简单工厂模式的改进(或者称为进一步的抽象)。

4) 将工厂抽象成两层,AbsFactory(抽象工厂) 和 具体实现的工厂子类。程序员可以 根据创建对象类型使用对应的工厂子类。这样将单个的简单工厂类变成了工厂簇, 更利于代码的维护和扩展。

抽象工厂模式通过在AbstarctFactory中增加创建产品的接口,并在具体子工厂中实现新加产品的创建,当然前提是子工厂支持生产该产品。否则继承的这个接口可以什么也不干。

其UML类图如下:

PC类:定义PC产品的接口(AbstractPC)

1
2
3
public interface PC {
void make();
}

MiPC类:定义小米电脑产品(MIPC)

1
2
3
4
5
6
7
8
9
public class MiPC implements PC {
public MiPC(){
this.make();
}
@Override
public void make() {
System.out.println("make MiPC");
}
}

MAC类:定义苹果电脑产品(MAC)

1
2
3
4
5
6
7
8
9
10
public class MAC implements PC {

public MAC(){
this.make();
}
@Override
public void make() {
System.out.println("make MAC");
}
}

下面需要修改工厂相关的类的定义:

AbstractFactory类:增加PC产品制造接口

1
2
3
4
public interface AbstractFactory {
Phone makePhone();
PC makePC();
}

XiaoMiFactory类:增加小米PC的制造(ConcreteFactory1)

1
2
3
4
5
6
7
8
9
10
11
public class XiaoMiFactory implements AbstractFactory {
@Override
public Phone makePhone() {
return new MiPhone();
}

@Override
public PC makePC() {
return new MiPC();
}
}

AppleFactory类:增加苹果PC的制造(ConcreteFactory2)

1
2
3
4
5
6
7
8
9
10
11
public class AppleFactory implements AbstractFactory {
@Override
public Phone makePhone() {
return new IPhone();
}

@Override
public PC makePC() {
return new MAC();
}
}

测试:

1
2
3
4
5
@Test
public void Test3(){
new cn.gs.factory.abstractFactory.XiaoMiFactory().makePC(); //make MiPC
new cn.gs.factory.abstractFactory.AppleFactory().makePC(); //make MAC
}

原型模式

1)原型模式(Prototype模式)是指:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象

2)原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节

3)工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建,即对象.clone()

原型模式UML类图:

Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Person implements Cloneable{
private String name;
private int age;

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

//克隆该实例,使用默认的clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Person person = null;
try {
person = (Person)super.clone();
}catch (Exception e){
System.out.println(e.getMessage());
}
return person;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//重写clone方法实现对象的快速复制(浅拷贝)
@Test
public void Test1(){
Person p1 = new Person();
p1.setName("宋江");
p1.setAge(44);
System.out.println(p1.hashCode()); //811587677

Person p2 = p1;
System.out.println(p2.hashCode()); //811587677

Person p3 = null;
try {
p3 = (Person) p1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println(p3.hashCode()); //1166807841
}

这就是一个简单的原型模式的应用,通过重写 Cloneable 接口的 clone()方法,我们就可以在测试中获取多个属性相同的对象,这种方法是浅拷贝,如果要实现深拷贝的效果,我们还需其他方法:

  • 深拷贝实现方式1:重写clone方法来实现深拷贝
  • 深拷贝实现方式2:通过对象序列化实现深拷贝(推荐)

深拷贝–重写clone方法

Address类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Address implements Cloneable {
private String city;
private String country;

public String getCity() {
return city;
}

@Override
protected Object clone() throws CloneNotSupportedException {
Address address = null;
try {
address = (Address) super.clone();
}catch (Exception e){
System.out.println(e.getMessage());
}
return address;
}

@Override
public String toString() {
return "Address{" +
"city='" + city + '\'' +
", country='" + country + '\'' +
'}';
}

public void setCity(String city) {
this.city = city;
}

public String getCountry() {
return country;
}

public void setCountry(String country) {
this.country = country;
}
}

User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class User implements Cloneable{
private String name;
private int age;
private Address address;

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}

//通过重写clone方法实现深拷贝
@Override
public Object clone() throws CloneNotSupportedException {
User user = null;
//完成基本数据类型和String的克隆
user = (User)super.clone();
//对引用进行单独的处理
user.address = (Address)address.clone();
return user;
}

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public Address getAddress() {
return address;
}

public void setAddress(Address address) {
this.address = address;
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//通过重写clone方法实现深拷贝
@Test
public void Test3() throws CloneNotSupportedException {
cn.gs.prototype.byOverrideClone.Address address = new cn.gs.prototype.byOverrideClone.Address();
address.setCity("上海市");
address.setCountry("黄浦区");
cn.gs.prototype.byOverrideClone.User user = new cn.gs.prototype.byOverrideClone.User();
user.setName("李白");
user.setAge(20);
user.setAddress(address);

System.out.println(user.hashCode()); //1166807841

cn.gs.prototype.byOverrideClone.User user1 = (cn.gs.prototype.byOverrideClone.User) user.clone();
System.out.println(user1.hashCode()); //289639718
}

深拷贝–对象序列化

Address类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Address implements Serializable {
private String city;
private String country;

public String getCity() {
return city;
}

@Override
public String toString() {
return "Address{" +
"city='" + city + '\'' +
", country='" + country + '\'' +
'}';
}

public void setCity(String city) {
this.city = city;
}

public String getCountry() {
return country;
}

public void setCountry(String country) {
this.country = country;
}
}

User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class User implements Serializable {
private String name;
private int age;
private Address address;

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}

//通过对象序列化实现深拷贝
public Object deepClone(){
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);//当前这个对象以对象流输出
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
User user = (User)ois.readObject();
return user;
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {

try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public Address getAddress() {
return address;
}

public void setAddress(Address address) {
this.address = address;
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//通过对象序列化实现深拷贝
@Test
public void Test2(){
Address address = new Address();
address.setCity("北京市");
address.setCountry("大兴区");
User user = new User();
user.setName("王五");
user.setAge(40);
user.setAddress(address);

System.out.println(user);
System.out.println(user.hashCode()); //1629911510

User user1 = user;
System.out.println(user1.hashCode()); //1629911510

User user2 = (User)user.deepClone();
System.out.println(user2.hashCode()); //188576144
}

建造者模式

定义:

The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so the same construction process can create different representations.
将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示

建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节

使用场景:

当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式。

当一个类的构造函数参数超过4个,而且这些参数有些是可选的时,我们通常有两种办法来构建它的对象。 例如我们现在有如下一个类计算机类Computer,其中cpu与ram是必填参数,而其他3个是可选参数,那么我们如何构造这个类的实例呢,通常有两种常用的方式:

1
2
3
4
5
6
7
public class Computer {
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
}

第一:折叠构造函数模式(telescoping constructor pattern ),这个我们经常用,如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Computer {
...
public Computer(String cpu, String ram) {
this(cpu, ram, 0);
}
public Computer(String cpu, String ram, int usbCount) {
this(cpu, ram, usbCount, "罗技键盘");
}
public Computer(String cpu, String ram, int usbCount, String keyboard) {
this(cpu, ram, usbCount, keyboard, "三星显示器");
}
public Computer(String cpu, String ram, int usbCount, String keyboard, String display) {
this.cpu = cpu;
this.ram = ram;
this.usbCount = usbCount;
this.keyboard = keyboard;
this.display = display;
}
}

第二种:Javabean 模式,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Computer {
...

public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public String getRam() {
return ram;
}
public void setRam(String ram) {
this.ram = ram;
}
public int getUsbCount() {
return usbCount;
}
...
}

那么这两种方式有什么弊端呢?

第一种主要是使用及阅读不方便。你可以想象一下,当你要调用一个类的构造函数时,你首先要决定使用哪一个,然后里面又是一堆参数,如果这些参数的类型很多又都一样,你还要搞清楚这些参数的含义,很容易就传混了

第二种方式在构建过程中对象的状态容易发生变化,造成错误。因为那个类中的属性是分步设置的,所以就容易出错

为了解决这两个痛点,builder模式就横空出世了

  1. 在Computer 中创建一个静态内部类 Builder,然后将Computer 中的参数都复制到Builder类中
  2. 在Computer中创建一个private的构造函数,参数为Builder类型
  3. 在Builder中创建一个public的构造函数,参数为Computer中必填的那些参数,cpu 和ram
  4. 在Builder中创建设置函数,对Computer中那些可选参数进行赋值,返回值为Builder类型的实例
  5. 在Builder中创建一个build()方法,在其中构建Computer的实例并返回

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Computer {
private final String cpu;//必须
private final String ram;//必须
private final int usbCount;//可选
private final String keyboard;//可选
private final String display;//可选

private Computer(Builder builder){
this.cpu = builder.cpu;
this.ram = builder.ram;
this.usbCount = builder.usbCount;
this.keyboard = builder.keyboard;
this.display = builder.display;
}
public static class Builder{
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
public Builder(String cpu,String ram){
this.cpu = cpu;
this.ram = ram;
}
public Builder setUsbCount(int usbCount){
this.usbCount = usbCount;
return this;
}
public Builder setKeyboard(String keyboard){
this.keyboard = keyboard;
return this;
}
public Builder setDisplay(String display){
this.display = display;
return this;
}
public Computer build(){
return new Computer(this);
}
}
//省略getter方法和toString方法
}

使用:

在客户端使用链式调用,一步一步的把对象构建出来

1
2
3
4
5
@Test
public void Test1(){
Computer computer = new Computer.Builder("因特尔", "三星").setUsbCount(3).setDisplay("三星24寸").build();
System.out.println(computer); //Computer{cpu='因特尔', ram='三星', usbCount=3, keyboard='null', display='三星24寸'}
}

其实上面的内容是Builder在Java中一种简化的使用方式,经典的Builder 模式与其有一定的不同

传统Builder模式

如上图所示,builder模式有4个角色。

  • Product: 最终要生成的对象,例如 Computer实例。
  • Builder: 构建者的抽象基类(有时会使用接口代替)。其定义了构建Product的抽象步骤,其实体类需要实现这些步骤。其会包含一个用来返回最终产品的方法Product getProduct()
  • ConcreteBuilder: Builder的实现类。
  • Director: 决定如何构建最终产品的算法. 其会包含一个负责组装的方法void Construct(Builder builder), 在这个方法中通过调用builder的方法,就可以设置builder,等设置完成后,就可以通过builder的 getProduct() 方法获得最终的产品。

第一步:我们的目标Computer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Computer {
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
public Computer(String cpu,String ram){
this.cpu = cpu;
this.ram = ram;
}

@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", ram='" + ram + '\'' +
", usbCount=" + usbCount +
", keyboard='" + keyboard + '\'' +
", display='" + display + '\'' +
'}';
}

public void setUsbCount(int usbCount) {
this.usbCount = usbCount;
}

public void setKeyboard(String keyboard) {
this.keyboard = keyboard;
}

public void setDisplay(String display) {
this.display = display;
}
}

第二步:抽象构建者类

1
2
3
4
5
6
public abstract class ComputerBuilder {
public abstract void setUsbCount();
public abstract void setKeyBoard();
public abstract void setDisplay();
public abstract Computer getComputer();
}

第三步:实体构建者类,我们可以根据要构建的产品种类产生多了实体构建者类,这里我们需要构建两种品牌的电脑,苹果电脑和联想电脑,所以我们生成了两个实体构建者类

苹果电脑构建者类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MacComputerBuilder extends ComputerBuilder {
private Computer computer;
public MacComputerBuilder(String cpu,String ram){
computer = new Computer(cpu,ram);
}
@Override
public void setUsbCount() {
computer.setUsbCount(2);
}

@Override
public void setKeyBoard() {
computer.setKeyboard("苹果键盘");
}

@Override
public void setDisplay() {
computer.setDisplay("苹果显示器");
}

@Override
public Computer getComputer() {
return computer;
}
}

联想电脑构建者类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class LenovoComputerBuilder extends ComputerBuilder {
private Computer computer;
public LenovoComputerBuilder(String cpu,String ram){
computer = new Computer(cpu, ram);
}
@Override
public void setUsbCount() {
computer.setUsbCount(5);
}

@Override
public void setKeyBoard() {
computer.setKeyboard("联想键盘");
}

@Override
public void setDisplay() {
computer.setDisplay("脸型显示器");
}

@Override
public Computer getComputer() {
return computer;
}
}

第四步:指导者类(Director)

1
2
3
4
5
6
7
public class ComputerDirector {
public void makeComputer(ComputerBuilder builder){
builder.setUsbCount();
builder.setKeyBoard();
builder.setDisplay();
}
}

使用:

首先生成一个director (1),然后生成一个目标builder (2),接着使用director组装builder (3),组装完毕后使用builder创建产品实例 (4)

1
2
3
4
5
6
7
8
@Test
public void Test2(){
ComputerDirector director = new ComputerDirector();
ComputerBuilder builder = new MacComputerBuilder("I5处理器","三星111");
System.out.println(builder.getComputer()); //Computer{cpu='I5处理器', ram='三星111', usbCount=0, keyboard='null', display='null'}
director.makeComputer(builder);
System.out.println(builder.getComputer()); //Computer{cpu='I5处理器', ram='三星111', usbCount=2, keyboard='苹果键盘', display='苹果显示器'}
}

可以看到,最开始的使用方式是传统builder模式的变种,首先其省略了director 这个角色,将构建算法交给了client端,其次将builder 写到了要构建的产品类里面,最后采用了链式调用

抽象工厂模式VS建造者模式

建造者模式是把对象的创建分散开来,比如我来组成头部,我来组成身体

1
2
3
4
5
interface builderDemo {
void bulidHead();
void bulidBody();
void bulidFoot();
}

然后由具体类来实现,实现创建对象的分解
举例:mybatis里 build sqlsessionfactory
抽象工厂是生产多个产品族(一个产品族包含几个不同产品的某一系列)
举例:宝马的不同车系

1
2
3
4
5
abstract class createBaoma{
abstract Baoma320 createBaoma320(); // 具体实现 使用 A1 B1 C1
abstract Baoma520 createBaoma520(); // 具体实现 使用 A2 B2 C2
abstract Baoma740 createBaoma740(); // 具体实现 使用 A3 B3 C3
}

建造者模式所有函数加到一起才能生成一个对象
抽象工厂一个函数生成一个对象

适配器模式

适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。

根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。

角色:

Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。

Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。

Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。

类适配器

Adaptee(适配者类)

1
2
3
4
5
public class Adaptee {
public void SpectificRequest(){
System.out.println("Adaptee源类的request方法");
}
}

Target(目标抽象类):

1
2
3
public interface Target {
public void Request();
}

Adapter(适配器类):

1
2
3
4
5
6
public class Adapter extends Adaptee implements Target {
@Override
public void Request() {
this.SpectificRequest();
}
}

测试:

1
2
3
4
5
6
//类的适配器模式测试
@Test
public void Test1(){
Target adapter = new Adapter();
adapter.Request();
}

UML图:

对象适配器

Adaptee(适配者类)

1
2
3
4
5
public class Adaptee {
public void SpectificRequest(){
System.out.println("Adaptee源类的request方法");
}
}

Target(目标抽象类):

1
2
3
public interface Target {
public void Request();
}

Adapter(适配器类):

1
2
3
4
5
6
7
8
9
10
public class Adapter implements Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
@Override
public void Request(){
this.adaptee.SpectificRequest();
}
}

测试:

1
2
3
4
5
6
//对象的适配器模式测试
@Test
public void Test2(){
cn.gs.adapter.objExample.Target adapter = new cn.gs.adapter.objExample.Adapter(new Adaptee());
adapter.Request();
}

与类的适配器模式相同,对象的适配器模式也是把适配的类的API转换成为目标类的API

与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接到Adaptee类

电压适配器

再来一个好理解的例子,我们国家的民用电都是 220V,日本是 110V,而我们的手机充电一般需要 5V,这时候要充电,就需要一个电压适配器,将 220V 或者 100V 的输入电压变换为 5V 输出

AC接口,输出电压值:

1
2
3
public interface AC {
int outputAC();
}

我们国家的AC:

1
2
3
4
5
6
7
public class AC220 implements AC {
public final int output = 220;
@Override
public int outputAC() {
return this.output;
}
}

日本的AC:

1
2
3
4
5
6
7
public class AC110 implements AC {
public final int output = 110;
@Override
public int outputAC() {
return this.output;
}
}

电压适配器DC5Adapter接口:

1
2
3
4
public interface DC5Adapter {
boolean support(AC ac);//看是否符合电压值
int outputDC5(AC ac);//输出转换后的电压值
}

我们国家的适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ChinaPowerAdapter implements DC5Adapter {
public static final int voltage = 220;
@Override
public boolean support(AC ac) {
return voltage == ac.outputAC();
}

@Override
public int outputDC5(AC ac) {
int adapterInput = ac.outputAC();
int adapterOutput = adapterInput / 44;
System.out.println("使用ChinaPowerAdapter变压输入器,输入ac:"+adapterInput+"V,输出:"+adapterOutput+"V");
return adapterOutput;
}
}

日本的适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JapanPowerAdapter implements DC5Adapter {
public static final int voltage = 110;
@Override
public boolean support(AC ac) {
return voltage == ac.outputAC();
}

@Override
public int outputDC5(AC ac) {
int adapterInput = ac.outputAC();
int adapterOutput = adapterInput / 22;
System.out.println("使用JapanPowerAdapter变压输入器,输入ac:"+adapterInput+"V,输出:"+adapterOutput+"V");
return adapterOutput;
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//转换电压测试
@Test
public void Test3(){
AC ac1 = new AC220();
AC ac2 = new AC110();
DC5Adapter adapter1 = new ChinaPowerAdapter();
if(adapter1.support(ac1)){
adapter1.outputDC5(ac1); //使用ChinaPowerAdapter变压输入器,输入ac:220V,输出:5V
}
if(adapter1.support(ac2)){
adapter1.outputDC5(ac2);
}
DC5Adapter adapter2 = new JapanPowerAdapter();
if(adapter2.support(ac1)){
adapter2.outputDC5(ac1);
}
if(adapter2.support(ac2)){
adapter2.outputDC5(ac2); //使用JapanPowerAdapter变压输入器,输入ac:110V,输出:5V
}
}

适配器模式总结

主要优点

  1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  2. 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  3. 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

具体来说,类适配器模式还有如下优点:

  • 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。

对象适配器模式还有如下优点:

  • 一个对象适配器可以把多个不同的适配者适配到同一个目标;
  • 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。

类适配器模式的缺点如下:

  1. 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;
  2. 适配者类不能为最终类,如在Java中不能为final类,C#中不能为sealed类;
  3. 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。

对象适配器模式的缺点如下:

  • 与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

适用场景

  • 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
  • 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

桥接模式

基本介绍

1)桥接模式(Bridge模式)是指:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变

2)是一种结构型设计模式

3)Bridge模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责。它的主要特点是把抽象(Abstraction)与行为实现(Implementation)分离开来,从而可以保持各部分的独立性以及应对他们的功能扩展

举个栗子

比如我们有奔驰、宝马等车型,还有红色、蓝色等颜色,这样有四种组合,但是如果用我们平常的思维就需要写各自的对应类,但是如果我们用桥接模式就可以只写车型、颜色两个类,下面我们用桥接模式实现一下

桥接模式的角色和职责:

1.Client 调用端

这是Bridge模式的调用者

2.抽象类(Abstraction)

抽象类接口(接口这货抽象类)维护队行为实现(implementation)的引用。它的角色就是桥接类

对应车型

3.RefinedAbstraction

这是Abstraction的子类。

对应具体车型,如宝马、奔驰

4.Implementor

行为实现类接口(Abstraction接口定义了基于Implementor接口的更高层次的操作)

对应颜色

5.ConcreteImplementor

Implementor的子类

对应具体颜色,如红色、蓝色

桥接模式的UML图如下:

首先定义Implementor接口:

1
2
3
public interface Implementor {
void operation();
}

再写具体的颜色实现类:

1
2
3
4
5
6
public class ConcreteImplementorA implements Implementor {
@Override
public void operation() {
System.out.println("颜色设置为红色");
}
}
1
2
3
4
5
6
public class ConcreteImplementorB implements Implementor {
@Override
public void operation() {
System.out.println("颜色设置为蓝色");
}
}

下面定义桥接类Abstraction,其中有对Implementor接口的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Abstraction {
private Implementor implementor;
public Abstraction(Implementor implementor){
this.implementor = implementor;
}

public Implementor getImplementor() {
return implementor;
}
public void operation(){
implementor.operation();
}
}

再写具体的车型实现类:

1
2
3
4
5
6
7
8
9
10
11
12
public class RefinedAbstractionA extends Abstraction {
public RefinedAbstractionA(Implementor implementor) {
super(implementor);
}

@Override
public void operation() {
System.out.println("车型为奔驰");
super.operation();
System.out.println("----------");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class RefinedAbstractionB extends Abstraction {
public RefinedAbstractionB(Implementor implementor) {
super(implementor);
}

@Override
public void operation() {
System.out.println("车型为宝马");
super.operation();
System.out.println("----------");
}
}

最后写测试:

1
2
3
4
5
6
7
8
9
10
@Test
public void Test1(){
Implementor i1 = new ConcreteImplementorA();
Abstraction r1 = new RefinedAbstractionA(i1);
r1.operation();

Implementor i2 = new ConcreteImplementorB();
Abstraction r2 = new RefinedAbstractionB(i2);
r2.operation();
}

输出结果:

1
2
3
4
5
6
车型为奔驰
颜色设置为红色
----------
车型为宝马
颜色设置为蓝色
----------

总结:

1.桥接模式的优点

(1)实现了抽象和实现部分的分离

桥接模式分离了抽象部分和实现部分,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,分别定义接口,这有助于系统进行分层设计,从而产生更好的结构化系统。对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了。

(2)更好的可扩展性

由于桥接模式把抽象部分和实现部分分离了,从而分别定义接口,这就使得抽象部分和实现部分可以分别独立扩展,而不会相互影响,大大的提供了系统的可扩展性。

(3)可动态的切换实现

由于桥接模式实现了抽象和实现的分离,所以在实现桥接模式时,就可以实现动态的选择和使用具体的实现。

(4)实现细节对客户端透明,可以对用户隐藏实现细节。

2.桥接模式的缺点

(1)桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程。

(2)桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围有一定的局限性。

3.桥接模式的使用场景

(1)如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。

(2)抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。

(3)一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。

(4)虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。

(5)对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用

装饰者模式

定义

在不改变原有对象的基础之上,将功能附加到对象上。提供了比继承更有弹性的替代方案(扩展原有对象功能)

类型

结构型

适用场景

  1. 扩展一个类的功能或者给一个类添加附加职责
  2. 给一个对象动态的添加功能,或动态撤销功能。

优点

  1. 继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能。(继承在扩展功能是静态的,必须在编译时就确定好,而使用装饰者可以在运行时决定,装饰者也建立在继承的基础之上的)
  2. 通过使用不同装饰类以及这些类的排列组合,可以实现不同的效果。
  3. 符合开闭原则

缺点

  1. 会出现更多的代码,更多的类,增加程序的复杂性。
  2. 动态装饰时,多层装饰时会更复杂。(使用继承来拓展功能会增加类的数量,使用装饰者模式不会像继承那样增加那么多类的数量但是会增加对象的数量,当对象的数量增加到一定的级别时,无疑会大大增加我们代码调试的难度)

UML图

好的,现在开始写代码,场景就是路边摊卖煎饼果子,可以加鸡蛋、香肠等,用装饰者模式实现一下

首先我们定义一个抽象的煎饼果子

1
2
3
4
5
//抽象的煎饼果子
public abstract class ABattercake {
protected abstract String getDesc();
protected abstract int cost();
}

实体煎饼果子类,实体煎饼果子继承了抽象煎饼果子类

1
2
3
4
5
6
7
8
9
10
11
12
//实体煎饼果子类
public class Battercake extends ABattercake {
@Override
protected String getDesc() {
return "煎饼";
}

@Override
protected int cost() {
return 8;
}
}

装饰父类,这里也是可以使用抽象类,注意构造器和这个里面的花费、描述方法的写法

这里注入一个抽象煎饼类的对象。我们的获取描述花费的操作都委托抽象煎饼类来执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//装饰父类
public class AbstractDecorator extends ABattercake {

private ABattercake aBattercake;
public AbstractDecorator(ABattercake aBattercake){
this.aBattercake = aBattercake;
}

@Override
protected String getDesc() {
return this.aBattercake.getDesc();
}

@Override
protected int cost() {
return this.aBattercake.cost();
}
}

鸡蛋的装饰类,这里注意他的构造器,参数是父类的对象抽象煎饼类对象,这里获取描述和花费方法都是调用了父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//鸡蛋的装饰类
public class EggDecorator extends AbstractDecorator {
public EggDecorator(ABattercake aBattercake) {
super(aBattercake);
}

@Override
protected String getDesc() {
return super.getDesc() + " 加一个鸡蛋";
}

@Override
protected int cost() {
return super.cost() + 1;
}
}

香肠装饰类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//香肠装饰类
public class SausageDecorator extends AbstractDecorator {
public SausageDecorator(ABattercake aBattercake) {
super(aBattercake);
}
@Override
protected String getDesc() {
return super.getDesc() + " 加一根香肠";
}

@Override
protected int cost() {
return super.cost() + 2;
}
}

测试:

1
2
3
4
5
6
7
8
@Test
public void Test1(){
ABattercake aBattercake = new Battercake();
aBattercake = new EggDecorator(aBattercake);
aBattercake = new SausageDecorator(aBattercake);
aBattercake = new EggDecorator(aBattercake);
System.out.println(aBattercake.getDesc()+" 销售价格:"+aBattercake.cost());
}

输出结果:

1
煎饼 加一个鸡蛋 加一根香肠 加一个鸡蛋 销售价格:12

组合模式

组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构

这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式

Compose objects into tree structures to represent part-whole hierarchies.Composite lets clients treat individual objects and compositions of objects uniformly.
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。 (来自《设计模式之禅》)

组成元素

  1. 抽象构件角色(Composite):是组合中对象声明接口,实现所有类共有接口的默认行为。
  2. 树叶构件角色(Leaf):上述提到的单个对象,叶节点没有子节点。
  3. 树枝构件角色(Composite):定义有子部件的组合部件行为,存储子部件,在Component接口中实现与子部件有关的操作。
  4. 客户端(Client):使用 Component 部件的对象。

优点

  1. 高层模块(客户端)调用简单。组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
  2. 节点自由增加,更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;

缺点

  1. 在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则
  2. 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
  3. 不容易限制容器中的构件;
  4. 不容易用继承的方法来增加构件的新功能;

组合模式的使用场景:

  1. 你想表示对象的部分-整体层次结构,如树形菜单,文件、文件夹的管理。文件系统由文件和目录组成,每个文件里装有内容,而每个目录的内容可以有文件和目录,目录就相当于是由单个对象或组合对象组合而成,如果你想要描述的是这样的数据结构,那么你就可以使用组合模式。
  2. 算术表达式包括操作数、操作符和另一个操作数,其中,另一个操作符也可以是操作数、操作符和另一个操作数。
  3. 在 JAVA AWT 和 SWING 中,对于 Button 和 Checkbox 是树叶,Container 是树枝。
  4. 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
  5. 在现实生活中,存在很多“部分-整体”的关系。汽车与轮胎、发动机的关系。医院与科室、医生的关系。学校与学院、学生、老师的关系

代码案例实现

想象一个场景,一个大学有多个学院,每个学员有多个专业,这样的场景如果用继承父类的方法求实现,不利于遍历和一些操作,这样就可以用组合模式去实现

首先,写抽象构件角色(Composite):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public abstract class OrganzitionComponent {
private String name; //名字
private String desc; //说明
protected void add(OrganzitionComponent organzitionComponent){
//默认实现
throw new UnsupportedOperationException();
}
protected void remove(OrganzitionComponent organzitionComponent){
throw new UnsupportedOperationException();
}
//构造器
public OrganzitionComponent(String name,String desc){
super();
this.name = name;
this.desc = desc;
}

public String getName() {
return name;
}

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

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}
//print方法,抽象方法,子类全都要实现
protected abstract void print();
}

它是所有类共有接口的默认行为,有名字和描述属性,还有add remove print方法

print方法是后面全部子类都要去实现的,所以写成抽象方法,add remove方法由于叶子节点不去实现,所以不能写成抽象的

接下来,写树枝构件角色(Composite),即是学校类和学院类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//University就是Component,可以管理College
public class University extends OrganzitionComponent {

List<OrganzitionComponent> organzitionComponents = new ArrayList<OrganzitionComponent>();

public University(String name, String desc) {
super(name, desc);
}

//重写add方法
@Override
protected void add(OrganzitionComponent organzitionComponent) {
organzitionComponents.add(organzitionComponent);
}

//重写remove方法
@Override
protected void remove(OrganzitionComponent organzitionComponent) {
organzitionComponents.remove(organzitionComponent);
}

@Override
public String getName() {
return super.getName();
}

@Override
public String getDesc() {
return super.getDesc();
}

//print方法,就是输出University包含的学院
@Override
protected void print() {
System.out.println("-----------" + getName() + "--------------");
//遍历OrganzitionComponents
for(OrganzitionComponent o : this.organzitionComponents){
o.print();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class College extends OrganzitionComponent {
//list集合中是Department
List<OrganzitionComponent> organzitionComponents = new ArrayList<OrganzitionComponent>();
public College(String name, String desc) {
super(name, desc);
}
//重写add方法
@Override
protected void add(OrganzitionComponent organzitionComponent) {
//在实际开发中,add方法可能和University不同
organzitionComponents.add(organzitionComponent);
}

//重写remove方法
@Override
protected void remove(OrganzitionComponent organzitionComponent) {
organzitionComponents.remove(organzitionComponent);
}

@Override
public String getName() {
return super.getName();
}

@Override
public String getDesc() {
return super.getDesc();
}

//print方法,就是输出University包含的学院
@Override
protected void print() {
System.out.println("-----------" + getName() + "--------------");
//遍历OrganzitionComponents
for(OrganzitionComponent o : this.organzitionComponents){
o.print();
}
}
}

实际开发中,add remove肯定会有不同,但是我们测试就不用管那么多,这两个类内容基本相同

最后,写树叶构件角色(Leaf),即系Department:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Department extends OrganzitionComponent {

public Department(String name, String desc) {
super(name, desc);
}

//add remove方法就不写了,因为他是叶子节点

@Override
public String getName() {
return super.getName();
}

@Override
public String getDesc() {
return super.getDesc();
}

@Override
protected void print() {
System.out.println(getName() + "\t" + getDesc());
}
}

由于它是叶子节点,就不用写add remove方法,也没有list集合属性

最后,进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void Test1(){
//从大到小创建对象 学校
OrganzitionComponent university = new University("清华大学","中国最顶尖大学");

//创建学院
OrganzitionComponent college1 = new College("计算机学院","计算机学院");
OrganzitionComponent college2 = new College("信息工程学院","信息工程学院");

//创建系
college1.add(new Department("软件工程","软件工程学费高"));
college1.add(new Department("网络工程","网络工程偏网络方向"));
college1.add(new Department("计算机科学与技术","计算机学院老牌专业"));

college2.add(new Department("通信工程","通信工程不好学"));
college2.add(new Department("电子科学与技术","电子技术偏底层硬件"));

university.add(college1);
university.add(college2);

university.print();
}

输出结果:

1
2
3
4
5
6
7
8
-----------清华大学--------------
-----------计算机学院--------------
软件工程 软件工程学费高
网络工程 网络工程偏网络方向
计算机科学与技术 计算机学院老牌专业
-----------信息工程学院--------------
通信工程 通信工程不好学
电子科学与技术 电子技术偏底层硬件

外观模式

概述

外观模式,我们通过外观的包装,使应用程序只能看到外观对象,而不会看到具体的细节对象,这样无疑会降低应用程序的复杂度,并且提高了程序的可维护性
例子1:一个电源总开关可以控制四盏灯、一个风扇、一台空调和一台电视机的启动和关闭。该电源总开关可以同时控制上述所有电器设备,电源总开关即为该系统的外观模式设计

问题

为了降低复杂性,常常将系统划分为若干个子系统。但是如何做到各个系统之间的通信和相互依赖关系达到最小呢?

解决方案

外观模式:为子系统中的一组接口提供一个一致的界面, Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。引入外观角色之后,用户只需要直接与外观角色交互,用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度

适用性

  1. 当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。

    这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。facade可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过facade层

  2. 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入 facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性 和可移植性

  3. 当你需要构建一个层次结构的子系统时,使用 facade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过facade进行通讯,从而简化了它们之间的依赖关系

结构

构建模式的组成

外观角色(Facade):是模式的核心,他被客户client角色调用,知道各个子系统的功能。同时根据客户角色已有的需求预订了几种功能组合

子系统角色(Subsystem classes):实现子系统的功能,并处理由Facade对象指派的任务。对子系统而言,facade和client角色是未知的,没有Facade的任何相关信息;即没有指向Facade的实例。

客户角色(client):调用facade角色获得完成相应的功能。

效果

Facade模式有下面一些优点:

1)对客户屏蔽子系统组件 , 减少了客户处理的对象数目并使得子系统使用起来更加容易 。通过引入外观模式,客户代码将变得很简单,与之关联的对象也很少

2)实现了子系统与客户之间的松耦合关系 ,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可

3)降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程 ,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象

4)只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类

Facade 模式的缺点:

1)不能很好地限制客户使用子系统类 ,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性

2)在不引入 抽象外观类 的情况下, 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”

实现

首先定义各个家居类,设计为单例模式,都用对应的开关方法

Airconditioner类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Airconditioner {
private Airconditioner(){}
private static Airconditioner instance = new Airconditioner();
public static Airconditioner getInstance(){
return instance;
}
public void on(){
System.out.println("打开空调...");
}
public void off(){
System.out.println("关闭空调...");
}
}

Fan类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Fan {
private Fan(){}
private static Fan instance = new Fan();
public static Fan getInstance(){
return instance;
}
public void on(){
System.out.println("打开风扇...");
}
public void off(){
System.out.println("关闭风扇...");
}
}

Light类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Light {
private Light(){}
private static Light instance = new Light();
public static Light getInstance(){
return instance;
}
public void on(){
System.out.println("打开电灯...");
}
public void off(){
System.out.println("关闭电灯...");
}
}

Tv类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Tv {
private Tv(){}
private static Tv instance = new Tv();
public static Tv getInstace(){
return instance;
}
public void on(){
System.out.println("打开电视...");
}
public void off(){
System.out.println("关闭电视...");
}
}

然后,定义我们的外观角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class HomeFacade {
//定义各个子系统对象
private Airconditioner airconditioner;
private Fan fan;
private Light light;
private Tv tv;

//构造器
public HomeFacade(){
airconditioner = Airconditioner.getInstance();
fan = Fan.getInstance();
light = Light.getInstance();
tv = Tv.getInstace();
}

//定义进入房间和离开房间的方法
public void in(){
airconditioner.on();
fan.on();
light.on();
tv.on();
}
public void leave(){
airconditioner.off();
fan.off();
light.off();
tv.off();
}
}

最后,测试:

1
2
3
4
5
6
7
@Test
public void Test1(){
HomeFacade facade = new HomeFacade();
facade.in();
System.out.println("-------------------");
facade.leave();
}

输出结果:

1
2
3
4
5
6
7
8
9
打开空调...
打开风扇...
打开电灯...
打开电视...
-------------------
关闭空调...
关闭风扇...
关闭电灯...
关闭电视...

享元模式

基本介绍

1)享元模式(Flyweight Pattern)也叫蝇量模式: 运用共享技术有效地支持大量细粒度的对象

2)常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个

3)享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率

4)享元模式经典的应用场景就是池技术了,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式

模式结构

角色说明

Flyweight: 享元接口,通过这个接口传入外部状态并作用于外部状态;

ConcreteFlyweight: 具体的享元实现对象,必须是可共享的,需要封装享元对象的内部状态;

UnsharedConcreteFlyweight: 非共享的享元实现对象,并不是所有的享元对象都可以共享,非共享的享元对象通常是享元对象的组合对象;

FlyweightFactory: 享元工厂,主要用来创建并管理共享的享元对象,并对外提供访问共享享元的接口;

内部状态和外部状态

  1. 享元模式提出了两个要求:细粒度和共享对象。这里就涉及到内部状态和外部状态了,即将对象的信息分为两个部分:内部状态和外部状态
  2. 内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变
  3. 外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。

举个栗子

我们要搭建不同平台的一个网站,有新闻端的、微信端的等,当别人需要时,我们不用再做一遍,只需要从我们的池子中给他们对应的平台

首先,先写Flyweight享元接口,其实就是Website类

1
2
3
public abstract class Website {
public abstract void use();
}

再声明具体的享元类:

1
2
3
4
5
6
7
8
9
10
public class ConcreteWebsite extends Website {
private String type = "";//网站发布的形式
public ConcreteWebsite(String type){
this.type = type;
}
@Override
public void use() {
System.out.println("网站发布的形式为:" + this.type);
}
}

接着创建一个享元工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//网站工厂类,根据需要返回一个网站
public class WebsiteFactory {
//集合,充当池的作用
private HashMap<String,ConcreteWebsite> pool = new HashMap<>();

//根据网站的类型,返回一个网站,如果没有就创建一个网站,并放到池中返回
public Website getWebsiteCategory(String type){
if(!pool.containsKey(type)){
pool.put(type,new ConcreteWebsite(type));
}
return (Website)pool.get(type);
}
//获取网站的总数
public int getWebsiteCount(){
return pool.size();
}

}

最后测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void Test1(){
//创建一个工厂类
WebsiteFactory factory = new WebsiteFactory();

//客户要一个以新闻形式的网站
Website website1 = factory.getWebsiteCategory("新闻");
website1.use();

//客户要一个以微信形式的网站
Website website2 = factory.getWebsiteCategory("微信");
website2.use();

//客户又要一个以新闻形式的网站
Website website3 = factory.getWebsiteCategory("新闻");
website3.use();

//客户又要一个以新闻形式的网站
Website website4 = factory.getWebsiteCategory("新闻");
website4.use();

System.out.println("网站的总数:" + factory.getWebsiteCount());
}

输出结果:

1
2
3
4
5
网站发布的形式为:新闻
网站发布的形式为:微信
网站发布的形式为:新闻
网站发布的形式为:新闻
网站的总数:2

可以看出,我们需要四个网站,使用享元模式,只用到了两个实例对象

如果我们还需要设置网站的使用人(User类),那这就是一个外部状态,需要修改我们现有的程序,并接着创建相对应的非共享的享元实现对象,就不接着展开了

代理模式

代理模式的基本介绍:

  1. 代理模式:为一个对象提供一个替身,以控制对这个对象的访问

    即通过代理对象访问目标对象

    这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能

  2. 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象

  3. 代理模式有不同的形式, 主要有三种:静态代理动态代理(JDK代理、接口代理)和Cglib代理(可以在内存动态的创建对象,而不需要实现接口,他是属于动态代理的范畴)

静态代理

基本介绍:

静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类

静态代理优缺点:

  1. 优点:在不修改目标对象的功能前提下, 能通过代理对象对目标功能扩展
  2. 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类
  3. 一旦接口增加方法,目标对象与代理对象都要维护

下面我们通过一个案例来实现:

首先,定义一个 ITeacherDao 的接口,有一个 teach 的方法:

1
2
3
public interface ITeacherDao {
void teach();//授课方法
}

然后,实现这个接口:

1
2
3
4
5
6
public class TeachDao implements ITeacherDao {
@Override
public void teach() {
System.out.println("老师授课中....");
}
}

接着,定义代理类:

1
2
3
4
5
6
7
8
9
10
11
12
public class TeachDaoProxy implements ITeacherDao {
private ITeacherDao target;//目标对象,通过接口来聚合
public TeachDaoProxy(ITeacherDao target){
this.target = target;
}
@Override
public void teach() {
System.out.println("开始代理...");
target.teach();
System.out.println("代理结束...");
}
}

最后,测试:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void Test1(){
//被代理对象
TeachDao teachDao = new TeachDao();

//创建代理对象
TeachDaoProxy proxy = new TeachDaoProxy(teachDao);

//通过代理对象,调用被代理的方法
proxy.teach();
}

输出结果:

1
2
3
开始代理...
老师授课中....
代理结束...

动态代理

基本介绍:

  1. 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理
  2. 代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象
  3. 动态代理也叫做:JDK代理、接口代理

JDK中生成代理对象的API:

  1. 代理类所在包:java.lang.reflect.Proxy

  2. JDK实现代理只需要使用 newProxyInstance 方法,但是该方法需要接收三个参数,完整的写法是:

    1
    2
    3
    static Object newProxyInstance(ClassLoader loader,
    Class<?>[] interfaces,
    InvocationHandler h)

下面我们通过一个案例来实现:

和静态代理一样,我们先定义被代理对象的接口和实现类:

1
2
3
public interface ITeacherDao {
void teach();//授课方法
}
1
2
3
4
5
6
public class TeachDao implements ITeacherDao {
@Override
public void teach() {
System.out.println("老师授课中....");
}
}

然后定义代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ProxyFactory {
//维护目标对象
private Object target;
//构造器,对target初始化
public ProxyFactory(Object target){
this.target = target;
}

/**
* public static Object newProxyInstance(ClassLoader loader,
* Class<?>[] interfaces,
* InvocationHandler h)
* 1.ClassLoader loader :指定当前目标对象使用的类加载器,获取加载器的方法固定
* 2.Class<?>[] interfaces :目标对象实现的接口类型,使用泛型方法确认类型
* 3.InvocationHandler h :时间处理,执行目标对象的方法时,会触发事情处理器方法会把当前执行的目标对象方法作为参数传入
* @return
*/
//给目标对象生成一个代理对象
public Object getProxyInstance(){
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK代理开始...");
//反射机制调用目标对象的方法
Object returnObj = method.invoke(target,args);
System.out.println("JDK代理结束...");
return returnObj;
}
});
}
}

最后,进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void Test1(){
//创建目标对象
TeachDao target = new TeachDao();
//给目标对象,创建代理对象,可以转成ITeacherDao
ITeacherDao proxyInstance = (ITeacherDao) new ProxyFactory(target).getProxyInstance();

//proxyInstance=class com.sun.proxy.$Proxy4 //内存中动态生成了代理对象
System.out.println("proxyInstance=" + proxyInstance.getClass());

//通过代理对象,调用目标对象的方法
proxyInstance.teach();
}

输出结果:

1
2
3
4
proxyInstance=class com.sun.proxy.$Proxy4
JDK代理开始...
老师授课中....
JDK代理结束...

Cglib代理

基本介绍:

  1. 静态代理和JDK代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理-这就是Cglib代理

  2. Cglib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展, 有些书也将Cglib代理归属到动态代理

  3. Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如Spring AOP,实现方法拦截

  4. 在AOP编程中如何选择代理模式:

    • 目标对象需要实现接口,用JDK代理

    • 目标对象不需要实现接口,用Cglib代理

  5. Cglib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类

下面我们还通过上面的案例,用Cglib代理去实现:

首先,定义代理类(TeachDao),它不实现接口:

1
2
3
4
5
public class TeachDao {
public void teach(){
System.out.println("老师授课中....");
}
}

接着,定义代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ProxyFactory implements MethodInterceptor {
//维护一个目标对象
private Object target;
//构造器
public ProxyFactory(Object target){
this.target = target;
}
//返回一个代理对象,是target对象的代理对象
public Object getProxyInstance(){
//1.创建工具类
Enhancer enhancer = new Enhancer();
//2.设置父类
enhancer.setSuperclass(target.getClass());
//3.设置回调函数
enhancer.setCallback(this);
//4.创建子类对象,即代理对象
return enhancer.create();
}
//重写intercept方法,会调用目标对象的方法
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("Cglib代理模式开始...");
Object returnVal = method.invoke(target, objects);
System.out.println("Cglib代理模式提交...");
return returnVal;
}
}

最后测试:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void Test1(){
//创建目标对象
TeachDao target = new TeachDao();

//获取代理对象,并且将目标对象传递到代理对象
TeachDao proxyInstance = (TeachDao) new ProxyFactory(target).getProxyInstance();

//通过代理对象,调用目标对象的方法
proxyInstance.teach();
}

输出结果:

1
2
3
Cglib代理模式开始...
老师授课中....
Cglib代理模式提交...

模板方法模式

基本介绍

  1. 模板方法模式(Template Method Pattern),又叫模板模式(Template Pattern),指在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行
  2. 简单说,模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤
  3. 这种类型的设计模式属于行为型模式

举个栗子

编写制作豆浆的程序,说明如下:

  • 制作豆浆的流程选材—>添加配料—>浸泡—>放到豆浆机打碎
  • 通过添加不同的配料,可以制作出不同口味的豆浆
  • 选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的(红豆、花生豆浆。。。)

首先,写一个抽象豆浆类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//抽象类,表示豆浆
public abstract class SoyaMilk {
//模板方法,make,模板方法可以做成final,不让子类去覆盖
final void make(){
select();
addCondiments();
soak();
beat();
}
//选材料
void select(){
System.out.println("第一步:选择上好的黄豆");
}
//添加不同的配料,抽象方法,子类具体实现
abstract void addCondiments();
//浸泡
void soak(){
System.out.println("第三步,黄豆和配料开始浸泡,需要3小时");
}
//打豆浆
void beat(){
System.out.println("第四部:黄豆和配料放到豆浆机去豆浆机去打碎");
}

}

然后,定义红豆、花生豆浆类,它们继承豆浆类,只需要重写添加配料方法即可:

1
2
3
4
5
6
7
//红豆豆浆
public class RedBeanSoyaMilk extends SoyaMilk {
@Override
void addCondiments() {
System.out.println("加入上好的红豆");
}
}
1
2
3
4
5
6
7
//花生豆浆
public class PeanutSoyaMilk extends SoyaMilk {
@Override
void addCondiments() {
System.out.println("加入上好的花生");
}
}

测试:

1
2
3
4
5
6
7
8
9
10
@Test
public void Test1(){
System.out.println("-----制作红豆豆浆-------");
SoyaMilk redBeanSoyMilk = new RedBeanSoyaMilk();
redBeanSoyMilk.make();

System.out.println("-----制作花生豆浆-------");
SoyaMilk peanutSoyMilk = new PeanutSoyaMilk();
peanutSoyMilk.make();
}

输出结果:

1
2
3
4
5
6
7
8
9
10
-----制作红豆豆浆-------
第一步:选择上好的黄豆
加入上好的红豆
第三步,黄豆和配料开始浸泡,需要3小时
第四部:黄豆和配料放到豆浆机去豆浆机去打碎
-----制作花生豆浆-------
第一步:选择上好的黄豆
加入上好的花生
第三步,黄豆和配料开始浸泡,需要3小时
第四部:黄豆和配料放到豆浆机去豆浆机去打碎

模板方法模式的钩子方法

  1. 在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”
  2. 还是用上面做豆浆的例子来讲解,比如,我们还希望制作纯豆浆,不添加任何的配料,请使用钩子方法对前面的模板方法进行改造

还有一种纯豆浆,它不需要我们添加任何配料

那么,我们就要先在抽象的豆浆类中添加一个钩子方法:

1
2
3
4
//钩子方法
boolean customerWantCondiments(){
return true;
}

然后修改 make 方法:

1
2
3
4
5
6
7
8
final void make(){
select();
if (customerWantCondiments()){
addCondiments();
}
soak();
beat();
}

最后还要添加一个纯豆浆类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PureSoyaMilk extends SoyaMilk {
//重写钩子方法,返回false
@Override
boolean customerWantCondiments() {
return false;
}

//空实现添加配料方法即可
@Override
void addCondiments() {

}
}

测试:

1
2
3
4
5
6
7
8
9
10
@Test
public void Test1(){
System.out.println("-----制作红豆豆浆-------");
SoyaMilk redBeanSoyMilk = new RedBeanSoyaMilk();
redBeanSoyMilk.make();

System.out.println("-----制作纯豆浆-------");
SoyaMilk pureSoyaMilk = new PureSoyaMilk();
pureSoyaMilk.make();
}

输出结果:

1
2
3
4
5
6
7
8
9
-----制作红豆豆浆-------
第一步:选择上好的黄豆
加入上好的红豆
第三步,黄豆和配料开始浸泡,需要3小时
第四部:黄豆和配料放到豆浆机去豆浆机去打碎
-----制作纯豆浆-------
第一步:选择上好的黄豆
第三步,黄豆和配料开始浸泡,需要3小时
第四部:黄豆和配料放到豆浆机去豆浆机去打碎

模板方法模式的注意事项和细节

  1. 基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改
  2. 实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用
  3. 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现
  4. 该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大
  5. 一般模板方法都加上final关键字,防止子类重写模板方法
  6. 模板方法模式使用场景:当要完成在某个过程该过程要执行一系列步骤这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理

命令模式

基本介绍

  1. 命令模式(Command Pattern):在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计

  2. 命名模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦

  3. 在命名模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命名),同时命令模式也支持可撤销的操作

  4. 通俗易懂的理解:将军发布命令,士兵去执行。其中有几个角色:将军(命令发布者)、士兵(命令的具体执行者)、命令(连接将军和士兵)

    Invoker是调用者(将军),Receiver是被调用者(士兵),MyCommand是命令,实现了Command接口,持有接收对象

命令模式的原理类图

  • Invoker:是调用者角色
  • Command:是命令角色,需要执行的所有命令都在这里,可以是接口或抽象类
  • Receiver:接受者角色,知道如何实施和执行一个请求相关的操作
  • ConcreteCommand:将一个接受者对象与一个动作绑定,调用接受者相应的操作,实现execute

举个栗子

我们有一个家居的遥控板,每一行都有对应某个家居的开与关,对应这种情形我们就可以用命令模式去实现

首先,我们要定义一个命令接口:

1
2
3
4
5
6
7
//创建命令接口
public interface Command {
//执行动作(操作)
void execute();
//撤销动作(操作)
void undo();
}

然后再写被调用者电灯的类,有对应的开关方法:

1
2
3
4
5
6
7
8
public class LightReceiver {
public void on(){
System.out.println("电灯打开...");
}
public void off(){
System.out.println("电灯关闭...");
}
}

然后写电灯的开与关命令类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LightOnCommand implements Command {
//聚合LightReceiver
LightReceiver light;

public LightOnCommand(LightReceiver light){
this.light = light;
}
@Override
public void execute() {
light.on();
}

@Override
public void undo() {
light.off();
}
}

开命令的话,执行方法就是打开电灯,即 on() 方法,如上:

那么对应关命令,执行方法就是关闭电灯,即 off() 方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LightOffCommand implements Command {
//聚合LightReceiver
LightReceiver light;

public LightOffCommand(LightReceiver light){
this.light = light;
}
@Override
public void execute() {
light.off();
}

@Override
public void undo() {
light.on();
}
}

对了,还要定义一个空名命令的类,可以为我们省去判空的操作:

1
2
3
4
5
6
7
8
9
//没有任何命令,即空执行,用于初始化
//可以省略对空的判断
public class NoCommand implements Command {
@Override
public void execute() {}

@Override
public void undo() {}
}

最关键的一步就是写下面遥控器的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class RemoteController {
//开按钮的命令数组
Command[] onCommands;
Command[] offCommands;
//撤销的命令
Command undoCommand;
public RemoteController(){
onCommands = new Command[5];
offCommands = new Command[5];
for (int i = 0; i < 5; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();

}
}
//给我们的按钮设置你需要的命令
public void setCommand(int no,Command onCommand,Command offCommand){
onCommands[no] = onCommand;
offCommands[no] = offCommand;
}
//按开的按钮
public void onButtonWasPushed(int no){
onCommands[no].execute();
//记录这次的操作,用于撤销
undoCommand = onCommands[no];
}
//按关的按钮
public void offButtonWasPushed(int no){
offCommands[no].execute();
//记录这次的操作,用于撤销
undoCommand = offCommands[no];
}
//按撤销按钮
public void undoButtonWasPushed(){
//防止抛空指针异常
if(undoCommand == null){
return;
}
undoCommand.undo();
}
}

最后,进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void Test1(){
//使用命令模式,完成通过遥控器,对电灯的操作

//创建电灯的对象(接受者)
LightReceiver lightReceiver = new LightReceiver();
//创建电灯的开关命令
LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver);
LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver);
//需要一个遥控器
RemoteController remoteController = new RemoteController();
//给我们的遥控器设置相关命令
//比如no=0的是电灯的开关操作
remoteController.setCommand(0,lightOnCommand,lightOffCommand);

System.out.println("----------按下灯的开的按钮-----------");
remoteController.onButtonWasPushed(0);
System.out.println("----------按下灯的关的按钮-----------");
remoteController.offButtonWasPushed(0);
System.out.println("----------按下撤销按钮-----------");
remoteController.undoButtonWasPushed();
}

输出结果:

1
2
3
4
5
6
----------按下灯的开的按钮-----------
电灯打开...
----------按下灯的关的按钮-----------
电灯关闭...
----------按下撤销按钮-----------
电灯打开...

添加电视命令类

上面我们只有电灯这一个电器,我们还可以添加电视,让我们操作一下

首先,肯定要先写 TVReceiver 类:

1
2
3
4
5
6
7
8
public class TVReceiver {
public void on(){
System.out.println("电视机打开...");
}
public void off(){
System.out.println("电视机关闭...");
}
}

然后写电视的开关命令类,与上面的电灯开关类基本差不多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TVOnCommand implements Command {
TVReceiver tvReceiver;
public TVOnCommand(TVReceiver tvReceiver){
this.tvReceiver = tvReceiver;
}
@Override
public void execute() {
tvReceiver.on();
}

@Override
public void undo() {
tvReceiver.off();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TVoffCommand implements Command {
TVReceiver tvReceiver;
public TVoffCommand(TVReceiver tvReceiver){
this.tvReceiver = tvReceiver;
}
@Override
public void execute() {
tvReceiver.off();
}

@Override
public void undo() {
tvReceiver.on();
}
}

RemoteController 类,我们不用改就可以,接着就可以进行测试了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Test
public void Test1(){
LightReceiver lightReceiver = new LightReceiver();
LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver);
LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver);
RemoteController remoteController = new RemoteController();
remoteController.setCommand(0,lightOnCommand,lightOffCommand);

System.out.println("----------按下灯的开的按钮-----------");
remoteController.onButtonWasPushed(0);
System.out.println("----------按下灯的关的按钮-----------");
remoteController.offButtonWasPushed(0);
System.out.println("----------按下撤销按钮-----------");
remoteController.undoButtonWasPushed();

TVReceiver tvReceiver = new TVReceiver();
TVOnCommand tvOnCommand = new TVOnCommand(tvReceiver);
TVoffCommand tVoffCommand = new TVoffCommand(tvReceiver);
remoteController.setCommand(1,tvOnCommand,tVoffCommand);
System.out.println("----------按下电视机的开的按钮-----------");
remoteController.onButtonWasPushed(1);
System.out.println("----------按下电视机的关的按钮-----------");
remoteController.offButtonWasPushed(1);
System.out.println("----------按下撤销按钮-----------");
remoteController.undoButtonWasPushed();

}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
----------按下灯的开的按钮-----------
电灯打开...
----------按下灯的关的按钮-----------
电灯关闭...
----------按下撤销按钮-----------
电灯打开...
----------按下电视机的开的按钮-----------
电视机打开...
----------按下电视机的关的按钮-----------
电视机关闭...
----------按下撤销按钮-----------
电视机打开...

访问者模式

访问者模式介绍

最复杂的设计模式,并且使用频率不高,《设计模式》的作者评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。

访问者模式是一种将数据操作和数据结构分离的设计模式。(觉得太抽象,可以看下面的例子)。

访问者模式的使用场景

  1. 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
  2. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。

访问者模式的UML类图

角色介绍

  • Visitor:接口或者抽象类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。
  • ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。
  • Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。
  • ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。

依然很抽象,看下面的栗子吧

举个栗子

年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量

由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了

1
2
3
4
5
6
7
8
9
10
11
//员工基类
public abstract class Staff {
public String name;
public int kpi;
public Staff(String name){
this.name = name;
this.kpi = new Random().nextInt(10);
}
//核心方法,接受Vistor的访问
public abstract void accept(Visitor visitor);
}

Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。下面看看工程师和经理的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//工程师类
public class Engineer extends Staff {
public Engineer(String name){
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
//工程师一年的代码数量
public int getCodeLines(){
return new Random().nextInt(10*10000);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//产品经理
public class Manager extends Staff {
public Manager(String name){
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
//一年的产品数量
public int getProducts(){
return new Random().nextInt(10);
}
}

工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。

然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的 showReport 方法查看所有员工的业绩,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//员工报表类
public class BusinessReport {
private List<Staff> mStaffs = new LinkedList<>();
public BusinessReport(){
mStaffs.add(new Manager("经理-A"));
mStaffs.add(new Engineer("工程师-A"));
mStaffs.add(new Engineer("工程师-B"));
mStaffs.add(new Engineer("工程师-C"));
mStaffs.add(new Manager("经理-B"));
mStaffs.add(new Engineer("工程师-D"));
}

/**
* 为访问者展示报表
* @param visitor 公司高层,如CEO CFO
*/
public void showReport(Visitor visitor){
for (Staff staff : mStaffs) {
staff.accept(visitor);
}
}
}

下面看看 Visitor 类型的定义, Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数,具体代码如下:

1
2
3
4
5
6
public interface Visitor {
//访问经理模式
void visit(Manager manager);
//访问工程师模式
void visit(Engineer engineer);
}

首先定义了一个 Visitor 接口,该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。具体实现类为 CEOVisitor、CTOVisitor类,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
//CEO访问者
public class CEOVisitor implements Visitor {
@Override
public void visit(Manager manager) {
System.out.println("产品经理:"+manager.name+",KPI:"+manager.kpi+",新产品数量:"+manager.getProducts());
}

@Override
public void visit(Engineer engineer) {
System.out.println("工程师:"+engineer.name+",KPI:"+engineer.kpi);
}
}

在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断,然后分别处理,代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class ReportUtil {
public void visit(Staff staff) {
if (staff instanceof Manager) {
Manager manager = (Manager) staff;
System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
", 新产品数量: " + manager.getProducts());
} else if (staff instanceof Engineer) {
Engineer engineer = (Engineer) staff;
System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
}
}
}

这就导致了 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护,当类型较多时,这个 ReportUtil 就会很复杂。而使用 Visitor 模式,通过同一个函数对不同对元素类型进行相应对处理,使结构更加清晰、灵活性更高

再添加一个CTO的 Visitor 类:

1
2
3
4
5
6
7
8
9
10
11
12
//CTO访问者
public class CTOVisitor implements Visitor {
@Override
public void visit(Manager manager) {
System.out.println("产品经理:"+manager.name+",产品数量:"+manager.getProducts());
}

@Override
public void visit(Engineer engineer) {
System.out.println("工程师:"+engineer.name+",代码行数:"+engineer.getCodeLines());
}
}

重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码

下面是客户端代码:

1
2
3
4
5
6
7
8
9
public class Clent {
public static void main(String[] args) {
BusinessReport report = new BusinessReport();
System.out.println("-----------CEO看报表-------------");
report.showReport(new CEOVisitor());
System.out.println("-----------CTO看报表-------------");
report.showReport(new CTOVisitor());
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-----------CEO看报表-------------
产品经理:经理-A,KPI:9,新产品数量:4
工程师:工程师-A,KPI:6
工程师:工程师-B,KPI:6
工程师:工程师-C,KPI:6
产品经理:经理-B,KPI:8,新产品数量:0
工程师:工程师-D,KPI:9
-----------CTO看报表-------------
产品经理:经理-A,产品数量:5
工程师:工程师-A,代码行数:88758
工程师:工程师-B,代码行数:95192
工程师:工程师-C,代码行数:15243
产品经理:经理-B,产品数量:2
工程师:工程师-D,代码行数:73324

在上述示例中,Staff 扮演了 Element 角色,而 Engineer 和 Manager 都是 ConcreteElement;CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对象;而 BusinessReport 就是 ObjectStructure;Client就是客户端代码

访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护

总结

我们要根据具体情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,是否需要经常定义新的操作,使用访问者模式是否能优化我们的代码,而不是使我们的代码变得更复杂。

  • 访问者模式的优点。
    1. 各角色职责分离,符合单一职责原则
      通过UML类图和上面的示例可以看出来,Visitor、ConcreteVisitor、Element 、ObjectStructure,职责单一,各司其责。
    2. 具有优秀的扩展性
      如果需要增加新的访问者,增加实现类 ConcreteVisitor 就可以快速扩展。
    3. 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化
      员工属性(数据结构)和CEO、CTO访问者(数据操作)的解耦。
    4. 灵活性
  • 访问者模式的缺点。
    1. 具体元素对访问者公布细节,违反了迪米特原则
      CEO、CTO需要调用具体员工的方法。
    2. 具体元素变更时导致修改成本大
      变更员工属性时,多个访问者都要修改。
    3. 违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有以来抽象
      访问者 visit 方法中,依赖了具体员工的具体方法

迭代器模式

定义:提供一种方法访问一个容器对象中各个元素,而又不暴露该对象的内部细节

类型:行为类模式

类图:

迭代器模式的结构:

  • 抽象容器:一般是一个接口,提供一个iterator()方法,例如java中的Collection接口,List接口,Set接口等
  • 具体容器:就是抽象容器的具体实现类,比如List接口的有序列表实现ArrayList,List接口的链表实现LinkList,Set接口的哈希列表的实现HashSet等
  • 抽象迭代器:定义遍历元素所需要的方法,一般来说会有这么三个方法:取得第一个元素的方法first(),取得下一个元素的方法next(),判断是否遍历结束的方法isDone()(或者叫hasNext()),移出当前对象的方法remove()
  • 迭代器实现:实现迭代器接口中定义的方法,完成集合的迭代

代码实现:

先写迭代器Iterator:

1
2
3
4
public interface Iterator {
public Object next();
public boolean hasNext();
}

具体的迭代器,通过指针变量实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ConcreteIterator implements Iterator {
private List list = new ArrayList();
private int cursor = 0;

public ConcreteIterator(List list){
this.list = list;
}
@Override
public Object next() {
Object obj = null;
if(this.hasNext()){
obj = this.list.get(cursor++);
}
return obj;
}

@Override
public boolean hasNext() {
if(cursor == list.size()){
return false;
}
return true;
}
}

抽象容器:

1
2
3
4
5
public interface Aggregate {
public void add(Object obj);
public void remove(Object obj);
public Iterator iterator();
}

具体容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ConcreteAggregate implements Aggregate {
private List list = new ArrayList();
@Override
public void add(Object obj) {
list.add(obj);
}

@Override
public void remove(Object obj) {
list.remove(obj);
}

@Override
public Iterator iterator() {
return new ConcreteIterator(list);
}
}

客户端测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
Aggregate aggregate = new ConcreteAggregate();
aggregate.add("小明");
aggregate.add("小红");
aggregate.add("小刚");

Iterator iterator = aggregate.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}

输出结果:

1
2
3
小明
小红
小刚

上面的代码中,Aggregate是容器类接口,大家可以想象一下Collection,List,Set等,Aggregate就是他们的简化版,容器类接口中主要有三个方法:添加对象方法add、删除对象方法remove、取得迭代器方法iterator。Iterator是迭代器接口,主要有两个方法:取得迭代对象方法next,判断是否迭代完成方法hasNext,大家可以对比java.util.List和java.util.Iterator两个接口自行思考

迭代器模式的优缺点

​ 迭代器模式的优点有:

  • 简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尚可以通过游标来取得,但用户需要在对集合了解很清楚的前提下,自行遍历对象,但是对于hash表来说,用户遍历起来就比较麻烦了。而引入了迭代器方法后,用户用起来就简单的多了。
  • 可以提供多种遍历方式,比如说对有序列表,我们可以根据需要提供正序遍历,倒序遍历两种迭代器,用户用起来只需要得到我们实现好的迭代器,就可以方便的对集合进行遍历了。
  • 封装性良好,用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心。

​ 迭代器模式的缺点:

  • 对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐,大家可能都有感觉,像ArrayList,我们宁可愿意使用for循环和get方法来遍历集合。

迭代器模式的适用场景

​ 迭代器模式是与集合共生共死的,一般来说,我们只要实现一个集合,就需要同时提供这个集合的迭代器,就像java中的Collection,List、Set、Map等,这些集合都有自己的迭代器。假如我们要实现一个这样的新的容器,当然也需要引入迭代器模式,给我们的容器实现一个迭代器

​ 但是,由于容器与迭代器的关系太密切了,所以大多数语言在实现容器的时候都给提供了迭代器,并且这些语言提供的容器和迭代器在绝大多数情况下就可以满足我们的需要,所以现在需要我们自己去实践迭代器模式的场景还是比较少见的,我们只需要使用语言中已有的容器和迭代器就可以了

观察者模式

所谓的观察者模式,是一种基于事件和响应的设计模式,常常用于传统的窗体应用程序,以及游戏开发领域

直接上栗子:

一个游戏,主角在迷宫里面走,地图上有怪物、陷阱、宝物等;

如果碰到怪物,主角就会减血;

如果碰到陷阱,主角就会被困住;

如果遇到宝石,主角就会加血….

怎么用代码去实现?

一般情况,我们会有两种常用方式去实现:“拉取”“推送”

拉取:怪物、陷阱等一直不停检测主角是否在范围内,在就攻击

推送:把怪物等聚合到主角类中,主角移动方法中,然后让这些成员类更新检查

这两种方法很容易想到,但是有很大的缺点:

拉取:如果事件没有发生,程序就会一直“空转”,浪费资源

推送: 迷宫中的怪物、陷阱、宝物在逻辑上并不属于主角,写成主角的成员,说不通

​ 游戏中肯定不止只有一种怪物、宝物等,以后还会增加,每次增加新元素都不得不修改主角类

在这种情况下,就引出了观察者模式

在面向对象编程中,我们要尽量面向抽象,而不是面向具体,这样才能减少代码的耦合,观察者模式很好的实现了这一点

UML图:

在上面的UML图中,主要有两组实体对象,一组是观察者一组是被观察者。所有的观察者,都实现了Observer接口;所有的被观察者,都继承自Subject抽象类

Subject类的成员OberverList,存储着已注册的观察者,当事件发生时,会通知列表中的所有观察者。需要注意的是,OberverList所依赖的是抽象的Observer接口,这样就避免了观察者与被观察者的紧耦合

接下来用代码实现它:

先定义观察者接口:

1
2
3
4
//观察者
public interface Observer {
public void update();
}

三个具体的观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//怪物
public class Monster implements Observer{
//判断主角是否在怪物攻击范围
public boolean isRange(){
//这里忽略细节,直接返回true
return true;
}

@Override
public void update() {
if(isRange()){
System.out.println("怪物 对主角攻击...");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//陷阱
public class Trap implements Observer{
//判断主角是否在陷阱攻击范围
public boolean isRange(){
//这里忽略细节,直接返回true
return true;
}

@Override
public void update() {
if(isRange()){
System.out.println("陷阱 把主角困住...");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//宝物
public class Treasure implements Observer{
//判断主角是否在宝物范围
public boolean isRange(){
//这里忽略细节,直接返回true
return true;
}

@Override
public void update() {
if(isRange()){
System.out.println("宝物 为主角加血...");
}
}
}

接下来写被观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//被观察者
public abstract class Subject {
//存有观察者的集合
private List<Observer> observerList = new ArrayList<>();
//添加观察者
public void attachObserver(Observer observer){
observerList.add(observer);
}
//移除观察者
public void detachObserver(Observer observer){
observerList.remove(observer);
}
//唤醒观察者更新
public void notifyObservers(){
for(Observer observer : observerList){
observer.update();
}
}
//移动方法
public abstract void move();
}

还有主角(具体的被观察者):

1
2
3
4
5
6
7
8
//主角
public class Hero extends Subject {
@Override
public void move() {
System.out.println("主角向前移动...");
notifyObservers();
}
}

最后客户端调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Clent {
public static void main(String[] args) {
//初始化对象
Hero hero = new Hero();

Monster monster = new Monster();
Trap trap = new Trap();
Treasure treasure = new Treasure();

//注册观察者
hero.attachObserver(monster);
hero.attachObserver(trap);
hero.attachObserver(treasure);

//移动事件
hero.move();
}
}

输出结果:

1
2
3
4
主角向前移动...
怪物 对主角攻击...
陷阱 把主角困住...
宝物 为主角加血...

可以看出,观察者是一个很巧妙的设计模式

  • 有许多游戏引擎的底层使用到了观察者模式,比如Unity 3D、Cross 2D,毕竟游戏开发中会涉及大量的事件响应逻辑
  • 此外,大家熟悉的Spring框架也用到了观察者模式,例如 ApplicationListenerApplicationContext 这两个接口

中介者模式

定义:用一个中介者对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使耦合松散,而且可以独立地改变它们之间的交互

类型:行为类模式

类图:

中介者模式的结构

​ 中介者模式又称为调停者模式,从类图中看,共分为3部分:

  • 抽象中介者:定义好同事类对象到中介者对象的接口,用于各个同事类之间的通信。一般包括一个或几个抽象的事件方法,并由子类去实现
  • 中介者实现类:从抽象中介者继承而来,实现抽象中介者中定义的事件方法。从一个同事类接收消息,然后通过消息影响其他同时类
  • 同事类:如果一个对象会影响其他的对象,同时也会被其他对象影响,那么这两个对象称为同事类。在类图中,同事类只有一个,这其实是现实的省略,在实际应用中,同事类一般由多个组成,他们之间相互影响,相互依赖。同事类越多,关系越复杂。并且,同事类也可以表现为继承了同一个抽象类的一组实现组成。在中介者模式中,同事类之间必须通过中介者才能进行消息传递

为什么要使用中介者模式

一般来说,同事类之间的关系是比较复杂的,多个同事类之间互相关联时,他们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定

如果引入中介者模式,那么同事类之间的关系将变为星型结构,从图中可以看到,任何一个类的变动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为

我们使用一个例子来说明一下什么是同事类:有两个类A和B,类中各有一个数字,并且要保证类B中的数字永远是类A中数字的100倍。也就是说,当修改类A的数时,将这个数字乘以100赋给类B,而修改类B时,要将数除以100赋给类A。类A类B互相影响,就称为同事类。代码如下:

先定义一个抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//抽象类,A B的父类
public abstract class AbstractColleague {
protected int number;

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}
//抽象方法,设置数字时修改关联对象
public abstract void setNumber(int number,AbstractColleague abstractColleague);
}

再写出A类与B类:

1
2
3
4
5
6
7
8
//A类
public class ColleagueA extends AbstractColleague {
@Override
public void setNumber(int number, AbstractColleague abstractColleague) {
this.number = number;
abstractColleague.setNumber(number*100);
}
}
1
2
3
4
5
6
7
8
//B类
public class ColleagueB extends AbstractColleague {
@Override
public void setNumber(int number, AbstractColleague abstractColleague) {
this.number = number;
abstractColleague.setNumber(number/100);
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client {
public static void main(String[] args) {
AbstractColleague a = new ColleagueA();
AbstractColleague b = new ColleagueB();

System.out.println("---------设置A影响B-----------");
a.setNumber(100,b);
System.out.println("a的number值:"+a.number);
System.out.println("b的number值:"+b.number);

System.out.println("---------设置B影响A-----------");
b.setNumber(6666,a);
System.out.println("a的number值:"+a.number);
System.out.println("b的number值:"+b.number);

}
}

输出结果:

1
2
3
4
5
6
---------设置A影响B-----------
a的number值:100
b的number值:10000
---------设置B影响A-----------
a的number值:66
b的number值:6666

上面的代码中,类A类B通过直接的关联发生关系,假如我们要使用中介者模式,类A类B之间则不可以直接关联,他们之间必须要通过一个中介者来达到关联的目的:

先定义抽象中介者:

1
2
3
4
5
6
7
8
9
10
11
12
13
//抽象中介者
public abstract class AbstractMediator {
//连个抽象类成员变量
protected AbstractColleague A;
protected AbstractColleague B;
public AbstractMediator(AbstractColleague A,AbstractColleague B){
this.A = A;
this.B = B;
}
//影响A和B各自的抽象方法
public abstract void AffectA();
public abstract void AffectB();
}

具体的抽象实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//中介者实现类
public class Mediator extends AbstractMediator {
public Mediator(AbstractColleague A, AbstractColleague B) {
super(A, B);
}

//处理B对A的影响
@Override
public void AffectA() {
int number = B.getNumber();
A.setNumber(number/100);
}

//处理A对B的影响
@Override
public void AffectB() {
int number = A.getNumber();
B.setNumber(number*100);
}
}

接下来,我们要修改抽象同事父类的 setNumber 方法,参数改为中介者:

1
2
//注意这里的参数不再是同事类,而是一个中介者
public abstract void setNumber(int number, AbstractMediator mediator);

还有同事A、B对应的方法修改:

1
2
3
4
5
6
7
public class ColleagueA extends AbstractColleague {
@Override
public void setNumber(int number, AbstractMediator mediator) {
this.number = number;
mediator.AffectB();
}
}
1
2
3
4
5
6
7
public class ColleagueB extends AbstractColleague {
@Override
public void setNumber(int number, AbstractMediator mediator) {
this.number = number;
mediator.AffectA();
}
}

最后进行客户端测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Client {
public static void main(String[] args) {
AbstractColleague a = new ColleagueA();
AbstractColleague b = new ColleagueB();
AbstractMediator mediator = new Mediator(a, b);

System.out.println("---------设置A影响B-----------");
a.setNumber(100,mediator);
System.out.println("a的number值:"+a.number);
System.out.println("b的number值:"+b.number);

System.out.println("---------设置B影响A-----------");
b.setNumber(6666,mediator);
System.out.println("a的number值:"+a.number);
System.out.println("b的number值:"+b.number);

}
}

输出结果:

1
2
3
4
5
6
---------设置A影响B-----------
a的number值:100
b的number值:10000
---------设置B影响A-----------
a的number值:66
b的number值:6666

总而言之,就是把原来处理对象关系的代码重新封装到一个中介类中,通过这个中介类来处理对象间的关系

中介者模式的优点

  • 适当地使用中介者模式可以避免同事类之间的过度耦合,使得各同事类之间可以相对独立地使用。
  • 使用中介者模式可以将对象间一对多的关联转变为一对一的关联,使对象间的关系易于理解和维护。
  • 使用中介者模式可以将对象的行为和协作进行抽象,能够比较灵活的处理对象间的相互作用。

适用场景

在面向对象编程中,一个类必然会与其他的类发生依赖关系,完全独立的类是没有意义的。一个类同时依赖多个类的情况也相当普遍,既然存在这样的情况,说明,一对多的依赖关系有它的合理性,适当的使用中介者模式可以使原本凌乱的对象关系清晰,但是如果滥用,则可能会带来反的效果。一般来说,只有对于那种同事类之间是网状结构的关系,才会考虑使用中介者模式。可以将网状结构变为星状结构,使同事类之间的关系变的清晰一些

中介者模式是一种比较常用的模式,也是一种比较容易被滥用的模式。对于大多数的情况,同事类之间的关系不会复杂到混乱不堪的网状结构,因此,大多数情况下,将对象间的依赖关系封装的同事类内部就可以的,没有必要非引入中介者模式。滥用中介者模式,只会让事情变的更复杂

备忘录模式

定义:备忘录(Memento)模式又叫作快照(Snapshot)模式或Token模式,是一种对象的行为模式。在备忘录模式里,一个备忘录是一个对象,它存储另一个对象(备忘录的原发器)在某个瞬间的内部状态。备忘的目的就是为了以后在需要的时候,可以将原发器对象的状态恢复(undo/rollback)到备忘录所保存的状态

本质:保存和恢复状态

设计意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样就可以将该对象恢复(undo/rollback)到原先保存的状态了

结构:

原发器(Originator)角色:原发器根据需要决定将自己的哪些内部状态保存到备忘录中,并可以使用备忘录来恢复内部状态

备忘录(Memento)角色:负责存储原发器对象的内部状态,但是具体需要存储哪些状态是由原发器对象来决定的。另外备忘录应该只能由原发器对象来访问它内部的数据,原发器外部的对象不应该访问到备忘录对象的内部数据

管理者(Caretaker)角色:备忘录管理者,或者称为备忘录负责人。主要负责保存好备忘录对象,但是不能对备忘录对象的内容进行操作或检查

举个栗子:

先定义一个备忘录接口:

1
2
3
//备忘录的窄接口,没有任何方法定义
public interface Memento {
}

再来写原发器角色,它里面会有备忘录对象的实现,此处将真正的备忘录对象当做原发器对象的一个私有内部类来实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Originator {
//示意,保存原发器的状态
private String state = "";
//创建备忘录,保存原发器的状态,返回创建好的备忘录对象
public Memento createMemento(){
return new MementoImpl(state);
}
//将原发器恢复到备忘录中保存的状态,返回保存有原发器状态的备忘录对象
public void recoverFromMemento(Memento memento){
MementoImpl mementoImpl = (MementoImpl)memento;
this.state = mementoImpl.getState();
}

public String getState() {
return state;
}

public void setState(String state) {
this.state = state;
}

//真正的备忘录对象,实现类备忘录接口,实现成私有的内部类,不让外部访问
private static class MementoImpl implements Memento{
//需要保存的状态
private String state = "";
public MementoImpl(String state){
super();
this.state = state;
}
public String getState(){
return state;
}
}
}

接下来是备忘录管理者的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//备忘录管理者
public class Caretaker {
//记录被保存的备忘录对象
private Memento memento = null;

public void setMemento(Memento memento) {
this.memento = memento;
}

public Memento getMemento() {
return memento;
}
}

创建客户端进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Client {
public static void main(String[] args) {
//创建一个原发器
Originator originator = new Originator();
//设置其初始状态
originator.setState("state 0");
//打印原发器当前的状态
System.out.println("原发器初始的状态:"+originator.getState());
//将原发器当前的状态保存在备忘录中
Memento memento = originator.createMemento();
//创建一个管理者
Caretaker c = new Caretaker();
//将创建好的备忘录交给管理者管理
c.setMemento(memento);
//改变原发器的状态
originator.setState("state 1");
//打印原发器当前的状态
System.out.println("原发器改变后的状态:"+originator.getState());
//将原发器恢复到备忘录保存的状态
originator.recoverFromMemento(c.getMemento());
System.out.println("原发器恢复后的状态:"+originator.getState());
}
}

输出结果:

1
2
3
原发器初始的状态:state 0
原发器改变后的状态:state 1
原发器恢复后的状态:state 0

在备忘录模式中,备忘录对象通常用来记录原发器中需要保存的内部状态,为了不破坏原发器对象的封装性,一般只让原发器自己来操作它的备忘录对象。为了保证这一点,通常会把备忘录对象作为原发器对象的内部类来实现,而且实现成私有的,这样就断了外部来访问这个备忘录对象的途径

把备忘录对象设计成为一个私有的内部类,外部只能通过备忘录对象的窄接口来获取备忘录对象,而这个接口没有任何方法,仅仅起到了一个标识对象类型的作用,从而保证内部的数据不会被外部获取或是操作,保证了原发器对象的封装性,也就不再暴露原发器对象在内部结构了

备忘录模式的适用性:

在以下条件下可以考虑使用备忘录模式:
• 如果必须保存一个对象在某一个时刻的全部或部分状态,方便在以后需要的时候,可以把该对象恢复到先前的状态。
• 如果需要保存一个对象的内部状态,但是如果用接口来让其它对象直接得到这些需要保存的状态,将会暴露对象的实现细节并破坏对象的封装性,这时可以使用备忘录模式,把备忘录对象实现成为原发器对象的私有内部类,从而保证只有原发器对象才能访问该备忘录对象。这样既保存了需要保存的状态,又不会暴露原发器对象的内部实现细节。

备忘录模式的实现:

增量存储:
如果需要频繁地创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内部相对于上一次存储状态后的增量改变。

结合原型模式:
在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。

离线存储:
备忘录的数据可以实现成为离线存储,除了存储在内存中,还可以把备忘录数据存储到文件中、XML中、数据库中,从而支持跨越会话的备份和恢复功能。

备忘录模式的优缺点:

使用备忘录模式的优点:

(1)更好的封装性
备忘录模式通过使用备忘录对象,来封装原发器对象的内部状态,虽然这个对象是保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法。这样有效地保证了对原发器对象内部状态的封装,不把原发器对象的内部实现细节暴露给外部。

(2)简化了原发器
在备忘录模式中,原发器不再需要管理和保存其内部状态的一个个版本,而是交由管理者或客户端对这些状态的版本进行管理,从而让原发器对象得到简化。

(3)窄接口和宽接口
备忘录模式,通过引入窄接口和宽接口,使得不同的地方,对备忘录对象的访问是不一样的。窄接口保证了只有原发器才可以访问备忘录对象存储的状态。

使用备忘录模式的缺点:

(1)标准的备忘录模式的实现机制是依靠缓存来实现的,因此,当需要备忘的数据量较大时,或者是存储的备忘录对象数据量不大但是数量很多的时候,或者是用户很频繁地创建备忘录对象的时候,这些都会导致非常大的开销。

(2)管理者负责维护备忘录,然而,管理者并不知道备忘录中有多少个状态。因此当存储备忘录时,一个本来很小的管理者,可能会产生大量的存储开销。

总结:

备忘录模式的功能,首先是在不破坏封装性的前提下,捕获一个对象的内部状态。这里要注意两点,一个是不破坏封装性,也就是对象不能暴露它不应该暴露的细节;另外一个是捕获的是对象的内部状态,而且通常还是运行期间某个时刻对象的内部状态。

之所以要捕获这些内部状态,是为了在以后的某个时候,可以将该对象的状态恢复到备忘录所保存的状态,这才是备忘录真正的目的。前面保存状态就是为了后面恢复,虽然不是一定要恢复,但是目的是为了恢复。

在备忘录模式中,备忘录对象通常用来记录原发器中需要保存的内部状态,为了不破坏原发器对象的封装性,一般只让原发器自己来操作它的备忘录对象。为了保证这一点,通常会把备忘录对象作为原发器对象的内部类来实现,而且实现成私有的,这样就断了外部来访问这个备忘录对象的途径。

解释器模式

什么是解释器模式

解释器这个名词想必大家都不会陌生,比如编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树,最终形成一颗抽象的语法分析树。诸如此类的例子也有很多,比如编译器、正则表达式等等。

如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子,这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。

就比如正则表达式,它就是解释器模型的一种应用,解释器为正则表达式定义了一个文法,如何表示一个特定的正则表达式,以及如何解释这个正则表达式。

解释器模式(Interpreter),给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。UML结构图如下:

抽象解释器(AbstractExpression):具体的解释任务由各个实现类完成

终结符表达式(TerminalExpression):实现与文法中的元素相关联的解释操作,通常一个解释器模式中只有一个终结表达式,但有多个实例,对应不同的终结符

非终结符表达式(NonterminalExpression):文法中的每条规则对应于一个非终结表达式,非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式

上下文(Context):上下文环境类,包含解释器之外的全局信息

客户端(Client):解析表达式,构建抽象语法树,执行具体的解释操作

解释器模式的应用

1. 何时使用

  • 当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时

2. 方法

  • 构建语法树,定义终结符与非终结符

3. 优点

  • 可扩展性好

4. 缺点

  • 解释器模式会引起类膨胀
  • 解释器模式采用递归调用方法,将会导致调试非常复杂
  • 使用了大量的循环和递归,效率是一个不容忽视的问题

5. 使用场景

  • 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
  • 一些重复出现的问题可以用一种简单的语言来表达
  • 一个简单语法需要解释的场景

6. 应用实例

  • 编译器
  • 运算表达式计算、正则表达式
  • 机器人

7. 注意事项

  • 尽量不要在重要的模块中使用解释器模式,否则维护会是一个很大的问题

解释器模式的实现

我们现在通过解释器模式来实现四则运算,如计算a+b的值。UML图如下:

1. 抽象表达式类

通过Map键值对,使键对应公式参数,如a b c等,值为运算时的具体值

1
2
3
4
public abstract class Expression {
//解析公式和数值,key是公式中的参数,value是具体的数值
public abstract int interpreter(HashMap<String,Integer> var);
}

2. 变量解析器

通过 interpreter() 方法从map中取出

1
2
3
4
5
6
7
8
9
10
11
public class VarExpression extends Expression{
private String key;

public VarExpression(String key){
this.key = key;
}
@Override
public int interpreter(HashMap<String, Integer> var) {
return var.get(this.key);
}
}

3. 抽象运算符号解析器

这里,每个运算符号都只和自己左右两个数字有关系,他们都要是Expression的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SymbolExpression extends Expression {
protected Expression left;
protected Expression right;

public SymbolExpression(Expression left,Expression right){
this.left = left;
this.right = right;
}

@Override
public int interpreter(HashMap<String, Integer> var) {
return 0;
}
}

4. 加法解析器

1
2
3
4
5
6
7
8
9
10
11
public class AddExpression extends SymbolExpression {

public AddExpression(Expression left, Expression right) {
super(left, right);
}

@Override
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) + super.right.interpreter(var);
}
}

5. 减法解析器

1
2
3
4
5
6
7
8
9
10
public class SubExpression extends SymbolExpression {
@Override
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) - super.right.interpreter(var);
}

public SubExpression(Expression left, Expression right) {
super(left, right);
}
}

6. 解析器封装类

使用Calculator构造函数传参,并解析封装。这里根据栈的“先进后出”来安排运算的先后顺序(主要用在乘除法,这里只写了加减法比较简单)。以加法为例,Calculator构造函数接收一个表达式,然后把表达式转换为char数组,并判断运算符号,如果是‘+’则进行加法运算,把左边的数(left变量)和右边的数(right变量)加起来即可。

例如a+b-c这个表达式,根据for循环,首先被压入栈中的是a元素生成的VarExpression对象,然后判断到加号时,把a元素的对象从栈中pop出来,与右边的数组b进行相加,而b是通过当前的数组游标下移一个单元格得来的(为了防止该元素被再次遍历,通过++i的方式跳过下一遍历)。减法运算同理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Calculator {
//定义表达式
private Expression expression;
//构造函数传参并解析
public Calculator(String expStr){
//安排运算先后顺序
Stack<Expression> stack = new Stack<>();
//表达式拆分为字符数组
char[] charArray = expStr.toCharArray();

Expression left = null;
Expression right = null;
for (int i = 0; i < charArray.length; i++) {
switch (charArray[i]){
case '+': //加法
left = stack.pop();
right = new VarExpression(String.valueOf(charArray[++i]));
stack.push(new AddExpression(left,right));
break;
case '-': //减法
left = stack.pop();
right = new VarExpression(String.valueOf(charArray[++i]));
stack.push(new SubExpression(left,right));
break;
default: //公式中的变量
stack.push(new VarExpression(String.valueOf(charArray[i])));
break;
}
}
this.expression = stack.pop();
}
//计算
public int run(HashMap<String,Integer> var){
return this.expression.interpreter(var);
}
}

7. 客户端

这里就比较简单了,通过getExpStr()方法获取表达式,再通过getValue()方法获取值的映射,最后再实例化Calculator类,通过run()方法获取最终的运算结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Client {
public static void main(String[] args) throws IOException {
String expStr = getExpStr();
HashMap<String, Integer> var = getValue(expStr);
Calculator calculator = new Calculator(expStr);
System.out.println("运算结果:" + expStr + "=" + calculator.run(var));
}
//获得表达式
public static String getExpStr() throws IOException {
System.out.println("请输入表达式:");
return (new BufferedReader(new InputStreamReader(System.in))).readLine();
}
//获得值映射
public static HashMap<String,Integer> getValue(String expStr) throws IOException {
HashMap<String,Integer> map = new HashMap<>();
for (char ch : expStr.toCharArray()) {
if(ch != '+' && ch != '-'){
if(!map.containsKey(String.valueOf(ch))){
System.out.println("请输入"+String.valueOf(ch)+"的值:");
String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
map.put(String.valueOf(ch),Integer.valueOf(in));
}
}
}
return map;
}
}

状态模式

定义:当一个对象的内在状态改变时允许改变其行为,这个对象看起来似乎是改变了其类

状态模式主要解决的是控制一个对象状态的条件表达式过于复杂时的情况,把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化

UML类图:

  1. 上下文环境(Context):它定义了客户程序需要的接口并维护一个具体状态角色的实例,将与状态相关的操作委托给当前的Concrete State对象来处理。

  2. 抽象状态(State):定义一个接口以封装使用上下文环境的的一个特定状态相关的行为。

  3. 具体状态(Concrete State):实现抽象状态定义的接口

举个栗子:

以电梯为例,它有四个状态:电梯门打开、电梯门关闭、电梯运行、电梯停止。

但是这四个状态之间的转换是有前提条件的,例如只有电梯门关闭后,电梯才能运行等等

这种情形下,就可以用状态模式去实现:

我们先声明一个上下环境类:Context ,里面什么都不写,后续再写

接下来写一个电梯接口类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class LiftState {
//定义一个环境角色,也就是封装状态的变换引起的功能变化
protected Context context;
public void setContext(Context context){
this.context = context;
}
//开启动作
public abstract void open();
//关闭动作
public abstract void close();
//运行动作
public abstract void run();
//停止动作
public abstract void stop();
}

抽象类比较简单,我们来先看一个具体的实现,门敞状态的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class OpeningState extends LiftState {
@Override
public void open() {
System.out.println("电梯门开启...");
}

//开启状态可以转换成关闭状态
@Override
public void close() {
//状态修改
super.context.setLiftState(Context.closingState);
//动作委托给closeState执行
super.context.getLiftState().close();
}

//开启状态不可以转换成运行状态
@Override
public void run() {
//do nothing
}

//开启状态不可以转换成停止状态
@Override
public void stop() {
//do nothing
}
}

这个类重写四个方法,Openning 状态是由open()方法产生的,因此这个方法中有一个具体的业务逻辑,我们是用print 来代替了;在Openning 状态下,电梯能过渡到其他什么状态呢?这就是我们的常识了,它只能转换成关闭状态,所以我们在 close() 方法里定义了状态变更,也委托了另一个关闭类的 close() 方法去执行,这个可能不好理解,我们再看看Context类就好理解一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Context {
//定义所有电梯的状态
public final static OpeningState openingState = new OpeningState();
public final static ClosingState closingState = new ClosingState();
public final static RunningState runningState = new RunningState();
public final static StoppingState stoppingState = new StoppingState();

//定义个当前电梯状态
private LiftState liftState;
public void setLiftState(LiftState liftState){
this.liftState = liftState;
//把当前的环境通知到各个实现类中
this.liftState.setContext(this);
}

public LiftState getLiftState() {
return liftState;
}
public void open(){
this.liftState.open();
}
public void close(){
this.liftState.close();
}
public void run(){
this.liftState.run();
}
public void stop(){
this.liftState.stop();
}
}

结合以上三个类,我们可以这样理解,Context是一个环境角色,它的作用是串联各个状态的过渡,在 LiftState 抽象类中我们定义了并把这个环境角色聚合起来,并传递到子类,也就是四个具体的实现类中根据各自的状态进行过渡与否,我们看看其他三个类,下面是关闭状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ClosingState extends LiftState {
//电梯门关闭状态可以变为打开状态
@Override
public void open() {
super.context.setLiftState(Context.openingState);
super.context.getLiftState().open();
}

@Override
public void close() {
System.out.println("电梯门关闭...");
}

//电梯门关闭状态之后,电梯可以运行
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.getLiftState().run();
}

////电梯门关闭状态之后,电梯可以停止不动
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.getLiftState().stop();
}
}

下面是运行状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class RunningState extends LiftState {
//电梯运行,电梯门不能打开
@Override
public void open() {
//do nothing
}

//电梯运行,门肯定是关的
@Override
public void close() {
//do nothing
}

@Override
public void run() {
System.out.println("电梯运行...");
}

//电梯运行,可以停止
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.getLiftState().stop();
}
}

下面是停止状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StoppingState extends LiftState {
//停止状态,可以开门
@Override
public void open() {
super.context.setLiftState(Context.openingState);
super.context.getLiftState().open();
}

//停止状态门本来就是关着的
@Override
public void close() {
//do nothing
}

//停止之后可以运行起来
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.getLiftState().run();
}

@Override
public void stop() {
System.out.println("电梯停止...");
}
}

最后来写客户端代码:

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.setLiftState(new ClosingState());
context.open();
context.run();//测试看开门状态电梯能不能运行,这行代码执行nothing
context.close();
context.run();
context.stop();
}
}

输出结果:

1
2
3
4
电梯门开启...
电梯门关闭...
电梯运行...
电梯停止...

状态模式的注意事项与细节:

  1. 代码有很强的可读性,状态模式将每个状态的行为封装到对应的一个类中
  2. 方便维护。将容易产生问题的if-else语句删除了,如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态,不但会产生很多if-else语句,而且还容易出错
  3. 符合“开闭原则”,容易删除状态
  4. 会产生很多类,每个状态都要有对应的一个类,当状态过多时,加大维护难度
  5. 当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为时,可以考虑使用状态模式

策略模式

基本介绍:

策略模式(Strategy Pattern)中,定义算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户

这算法体现了几个设计原则:

  1. 把变化的代码从不变的代码中分离出来;
  2. 针对接口编程而不是具体类(定义了策略接口);
  3. 多用组合/聚合,少用继承(客户通过组合方式使用策略)。

UML类图:

Context是上下文,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用

Strategy是策略类,用于定义所有支持算法的公共接口

ConcerteStrategy是具体策略类,封装了具体的算法或行为

我的理解:策略模式就是去分析项目变化的与不变的部分,不变的部分在超类中定义成抽象方法,子类各自去实现;变化的部分,就定义成接口,超类聚合这些接口,子类去实现即可。

继承是实现共性,减少代码的重复。接口是实现特性。

举个栗子:

鸭子问题,有一些鸭子,设计它们。

我们先简单分析出鸭子的一些特性:飞、叫、游泳、描述。

好的,以这四个特性为例进行分析,首先,描述这个方法我们肯定要定义成抽象方法,让子类继承;飞和叫我们可以定义成接口,聚合在父类中,子类按情况实现;游泳的话,就说所有的鸭子都会,所以直接在父类写就可。

好的,下面就是代码部分,先写接口与其实现类:

1
2
3
4
//飞方法的接口
public interface FlyBehavior {
void fly();
}
1
2
3
4
//叫方法的接口
public interface QuackBehavior {
void quack();
}
1
2
3
4
5
6
7
//有些鸭子不会飞,聚合这个类即可
public class BadFlyBehavior implements FlyBehavior {
@Override
public void fly() {
System.out.println("~~~~~~我不会飞~~~~~~");
}
}
1
2
3
4
5
6
7
//有些鸭子不会叫,聚合这个类即可
public class BadQuackBehavior implements QuackBehavior {
@Override
public void quack() {
System.out.println("~~~~~~~我不会叫~~~~~");
}
}

鸭子父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck(){

}
public void Quack(){
quackBehavior.quack();
}
public void Fly(){
flyBehavior.fly();
}
//交给子类实现
public abstract void display();
public void swim(){
System.out.println("~~~~~~我会游泳~~~~~~~");
}

public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}

public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
}

红头鸭、绿头鸭两种具体的鸭子:

1
2
3
4
5
6
7
8
9
10
11
public class GreenHeadDuck extends Duck {
//绿头鸭不会飞
public GreenHeadDuck(){
flyBehavior = new BadFlyBehavior();
}

@Override
public void display() {
System.out.println("我和你们不一样,我是绿色的头...");
}
}
1
2
3
4
5
6
7
8
9
10
public class RedHeadDuck extends Duck {
//红头鸭不会叫
public RedHeadDuck(){
quackBehavior = new BadQuackBehavior();
}
@Override
public void display() {
System.out.println("我是独一无二的,我是红头鸭...");
}
}

客户端调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client {
public static void main(String[] args) {
Duck redDuck = new RedHeadDuck();
Duck greenDuck = new GreenHeadDuck();

redDuck.display();
redDuck.swim();
redDuck.Quack();
redDuck.setQuackBehavior(new QuackBehavior() {
@Override
public void quack() {
System.out.println("~~~~~~我会叫~~~~~~");
}
});
redDuck.Quack();
}
}

输出结果:

1
2
3
4
我是独一无二的,我是红头鸭...
~~~~~~我会游泳~~~~~~~
~~~~~~~我不会叫~~~~~
~~~~~~我会叫~~~~~~

策略模式的注意事项和细节:

  1. 策略模式的关键是:分析项目中变化部分与不变部分
  2. 策略模式的核心思想是:多用组合/聚合、少用继承;用行为类组合,而不是行为的继承。更有弹性
  3. 体现了“对修改关闭,对扩展开放”原则,客户端增加行为不用修改原有代码,只要添加一种策略(或者行为)即可,避免了使用多重转移语句(if..else if..else)
  4. 提供了可以替换继承关系的办法:策略模式将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展
  5. 需要注意的是:每添加一个策略就要增加一个类,当策略过多是会导致类数目庞大

责任链模式

定义:责任链模式(Chain of Responsibility)使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象能够处理它。

发出这个请求的客户端不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配职责。

本质:分离职责,动态组合

设计意图:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

结构:

抽象处理器(Handler)角色:定义职责的接口,通常在这里定义处理请求的方法,如果需要接口可以定义一个方法以设定和返回对后继处理者的引用

具体处理者(ConcreteHandler)角色:实现职责的类,实现对它的职责范围内请求的处理,如果不处理,就继续转发请求给后继处理者

客户端(Client)角色:向链上的具体处理对象提交请求,让职责链负责处理

举个栗子:

聚餐费用报销情形,有三个领导:项目经理、部门经理、总经理

项目经理的报销范围为:申请人必须是小明,且费用需在500元之内;

部门经理的报销范围为:申请人必须是小明或者小红,且费用需在1000元之内;

总经理的报销范围为:申请人必须是小明或者小红,费用随意。

这样的情形就可以用责任链模式去实现,我们先写一个处理类:

1
2
3
4
5
6
7
8
9
10
11
public abstract class Handler {
//持有后继的处理器对象
protected Handler successor = null;

//设置setter方法
public void setSuccessor(Handler successor) {
this.successor = successor;
}
//处理聚餐费用的申请
public abstract String handleFeeReques(String user,double fee);
}

然后接着写这三个经理,即具体的处理者,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProjectManager extends Handler {
//项目经理处理请求,申请人必须是小明,且费用需在500元之内
@Override
public String handleFeeReques(String user, double fee) {
if (fee < 500 && user.equals("小明")){
//符合条件批准
return "项目经理批准" + user + " " + fee + "元的聚餐费用报销";
}else {
//不符合条件,传递得高的人处理
if(this.successor!=null){
return this.successor.handleFeeReques(user,fee);
}
}
return "";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DeptManager extends Handler {
//部门经理处理请求,申请人必须是小明或者小红,且费用需在1000元之内
@Override
public String handleFeeReques(String user, double fee) {
if(fee < 1000 && (user.equals("小红") || user.equals("小明"))){
return "部门经理批准" + user + " " + fee + "元的聚餐费用报销";
}else {
if(this.successor!=null){
return this.successor.handleFeeReques(user,fee);
}
}
return "";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GeneraManager extends Handler {
//总经理处理请求,申请人必须是小明、小红,费用随意
@Override
public String handleFeeReques(String user, double fee) {
if (user.equals("小明") || user.equals("小红")){
return "总经理批准" + user + " " + fee + "元的聚餐费用报销";
} else {
if(this.successor!=null){
return this.successor.handleFeeReques(user,fee);
}
}
return "";
}
}

最后客户端进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Client {
public static void main(String[] args) {
//生成对象
Handler projectManager = new ProjectManager();
Handler deptManager = new DeptManager();
Handler generaManager = new GeneraManager();

//组装责任链
projectManager.setSuccessor(deptManager);
deptManager.setSuccessor(generaManager);

//开始测试
String ret1 = projectManager.handleFeeReques("小菜", 100);
System.out.println("user: 小菜 fee: 100 的报销结果为:" + ret1);

String ret2 = projectManager.handleFeeReques("小明", 100);
System.out.println("user: 小明 fee: 100 的报销结果为:" + ret2);

String ret3 = projectManager.handleFeeReques("小红", 800);
System.out.println("user: 小红 fee: 800 的报销结果为:" + ret3);

String ret4 = projectManager.handleFeeReques("小红", 2000);
System.out.println("user: 小红 fee: 800 的报销结果为:" + ret4);
}
}

输出结果:

1
2
3
4
user: 小菜 fee: 100 的报销结果为:
user: 小明 fee: 100 的报销结果为:项目经理批准小明 100.0元的聚餐费用报销
user: 小红 fee: 800 的报销结果为:部门经理批准小红 800.0元的聚餐费用报销
user: 小红 fee: 2000 的报销结果为:总经理批准小红 2000.0元的聚餐费用报销

总结:

  1. 责任链模式与 if…else 相比,他的耦合性要低一些,因为它将条件判定分散到各个处理类中,并且这些处理类的优先处理顺序可以随意的设定,并且如果想要添加新的 handler 类也是十分简单的,这符合开放闭合原则。
  2. 责任链模式带来了灵活性,但是在设置处理类前后关系时,一定要避免在链中出现循环引用的问题。