三大类设计模式

  • 创建型模式
    将对象的创建和使用分离。包括单例,原型,工厂方法,抽象方法,建造者等5种。
  • 结构型模式
    描述如何将类或对象按某种布局组成更大的结构。包括代理,适配器,桥接,装饰,外观,享元,组合等7种。
  • 行为型模式
    描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。包括模板方法,策略,命令,职责链,状态,观察者,中介者,迭代器,访问者,备忘录,解释器等11种。

UML

UML包括用例图,类图,对象图,顺序图,协作图,状态图,活动图,时序图,协作图,构件图,部署图等。

类图

类图描述了系统中的类,类的内部结构以及类和类之间的关系。类图是静态的。

Employee
成员变量
-name:String
-age:int
成员方法
+work():void

其中,成员变量的表示格式为:可见性 名称:类型[可省 =默认值],其中-表示private,+表示public,#表示protected,不加表示默认权限。
成员方法的表示格式为:可见性 名称(参数列表)[可省 :返回值类型]。权限符号同上。

  • 类之间的表示
  • 关联关系:实线+箭头,表示一个类知道另一个类的存在,但是不一定知道对方的细节。
    另外,双向关联可以直接用直线表示。比如用户和订单,用户可以查看订单,订单也可以查看用户。
  • 泛化/继承关系:实线+空心三角形,表示一个类继承另一个类。是耦合度最大的一种关系。
  • 实现关系:虚线+空心三角形,表示一个类实现了一个接口。
  • 依赖关系:虚线+箭头,表示一个类使用了另一个类。比如司机使用汽车。**(常用)**
  • 聚合关系:实线+空心菱形+箭头,表示一个类包含另一个类,但是另一个类不是这个类的一部分。
    聚合关系与整体和部分的关系,比如学校和老师,学校包含老师,但是老师不是学校的一部分。(老师本身就是老师,不是学校的一部分)
  • 组合关系:实线+实心菱形+箭头,表示一个类包含另一个类,另一个类这个类的一部分。(少用)
    组合是加强的聚合,一旦整体不存在了,部分也不存在了。比如人和大脑,人包含大脑,大脑是人的一部分。

设计原则

开闭原则

对扩展开放,对修改关闭。。或者说,实现一个热插拔的效果,即在不修改原有代码的基础上,去增加新的功能。
具体实现方式为多使用抽象类和接口,而不是具体实现类。

Example:皮肤的切换
抽象类 - 皮肤类

1
2
3
public abstract class AbstractSkin {
public abstract void display();
}

原始实现类 - 默认皮肤

1
2
3
4
5
6
7
public class DefaultSkin extends AbstractSkin {
@Override
public void display() {
System.out.println("默认皮肤");
}
}

自定义皮肤

1
2
3
4
5
6
public class CustomSkin extends AbstractSkin {
@Override
public void display() {
System.out.println("自定义皮肤");
}
}

输入法类

1
2
3
4
5
6
7
8
9
10
11
public class InputMethod {
private AbstractSkin skin;

public void setSkin(AbstractSkin skin) {
this.skin = skin;
}

public void display() {
skin.display();
}
}

切换皮肤

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
InputMethod inputMethod = new InputMethod();
inputMethod.setSkin(new DefaultSkin());
inputMethod.display();
inputMethod.setSkin(new CustomSkin());
inputMethod.display();
}
}

里氏代换

子类可以扩展父类的功能,但不能改变父类原有的功能。子类尽量不要重写父类的方法。
比如正方形和长方形,正方形继承长方形。长方形有长和宽两个属性,正方形只有一个边长属性。正方形重写了父类长和宽的set方法,使得长和宽都是边长。这就违反了里氏代换原则。
相较于继承,更适合的应该是接口的实现。接口是一种规范,通过实现接口,来实现某种功能。比如长方形和正方形都实现了一个接口,接口中有一个方法,用来计算面积。长方形和正方形都实现了这个方法,但是实现的方式不同。这样就不会违反里氏代换原则。

依赖倒置

高层模块不应该依赖低层模块,二者都应该依赖其抽象。
比如,A类里声明了一个B类的对象,那么A类就依赖于B类,那么A类就是高层模块,B类就是低层模块。
就像组装电脑,我们需要组装CPU,显卡,内存,硬盘等等,这些都是低层模块,而我们组装的电脑就是高层模块,CPU等低层模块可以有多种选择。
依赖倒置原则的核心思想是:面向接口编程

接口隔离

客户端不应该依赖它不需要的接口。同时,类间的依赖关系应该建立在最小的接口上。
比如A类和B类,A类只需要B类的一个方法,但是B类有很多方法,那么A类就被迫实现了B类的所有方法,这就违反了接口隔离原则。
接口隔离原则的核心思想是:接口要小而专,不要大而全。那么这里的B类就应该拆分成多个接口,A类只需要实现自己需要的接口即可。

迪米特法则

一个软件实体应当尽可能少地与其他实体发生相互作用。
当前对象本身,当前对象的成员对象,当前对象创建的对象,当前对象的方法参数,这些对象同当前对象存在关联,聚合或者组合关系,可以直接访问这些对象的方法。
举例来说,明星,粉丝,媒体公司和经纪人,经纪人中声明了明星,粉丝和媒体公司的对象,那么经纪人就可以直接访问明星,粉丝和媒体公司的方法。而粉丝和媒体公司等之间没有关联,那么粉丝和媒体公司就不能直接访问对方的方法。

合成复用原则

尽量使用组成/聚合的方式,而不是使用继承。
为什么不使用继承?因为继承是一种强耦合的方式,父类的变化会影响到子类。而组合/聚合是一种弱耦合的方式,父类的变化不会影响到子类。

  • 继承破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,子类可以直接访问父类的实现细节。
  • 耦合度高。父类的实现细节一旦发生变化,子类也会发生变化。

而组合/聚合是一种弱耦合的方式,父类的变化不会影响到子类。

  • 维持了封装性。因为父类的实现细节不会暴露给子类。
  • 耦合度低。父类的实现细节发生变化,子类不会发生变化。可以在类的成员位置声明抽象。

比如汽车的分类,可以按照动力源分为汽油车和电动车,按照车身结构分为轿车和客车,按照颜色又可以分为白色,黑色等。
如果使用继承,那么就需要创建很多个类,汽车有子类汽油车和电动车,汽油车和电动车又有各自的子类白色汽油车,黑色汽油车,白色电动车,黑色电动车等等。
如果使用组合/聚合,那么就只需要创建汽车类,汽车类中有动力源,颜色。使用接口聚合来实现动力源和颜色。
聚合的意义就在于扩展,如果需要增加新的动力源,只需要实现动力源接口即可,不需要修改汽车类。如果需要增加新的颜色,只需要实现颜色接口即可,不需要修改汽车类。

创建者模式

创建者模式关注如何创建对象,将对象的创建和使用分离。主要分为五大类:

  • 单例模式
  • 工厂方法模式
  • 抽象工厂模式
  • 建造者模式
  • 原型模式

单例模式

单例模式是一种创建型模式,它保证一个类只有一个实例,并提供一个访问它的全局访问点。
单例模式包括一个单例类,一个访问类。

单例模式分为两种:

  • 饿汉式 在类加载的时候就创建对象
  • 懒汉式 在类加载的时候不创建对象,只有在第一次使用的时候才创建对象
1
2
3
4
5
6
7
8
9
10
// 饿汉式
public class Singleton {
//已经在类加载的时候创建了对象
private static Singleton instance = new Singleton();
//私有化构造方法
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}

也可以用静态代码块,在类加载的时候创建对象。

1
2
3
4
5
6
7
8
9
10
11
// 懒汉式
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

相较于饿汉式,懒汉式的优点是节省了内存,只有在第一次使用的时候才创建对象。但是缺点是线程不安全,如果有多个线程同时访问getInstance()方法,那么就会创建多个对象。
当然,可以通过加锁来解决线程安全问题,但是加锁会影响效率。所以可以使用双重检查锁来解决线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 双重检查锁
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
//第一次检查,如果instance为空,那么就进入同步代码块
if (instance == null) {
//同步代码块,只有一个线程可以进入
synchronized (Singleton.class) {
//第二次检查,如果instance为空,那么就创建对象
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

静态内部类也可以实现单例模式,静态内部类只有在第一次使用的时候才会加载,所以也是懒汉式的一种。

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

枚举也可以实现单例模式,枚举是线程安全的,并且只会加载一次。
枚举的单例模式是饿汉式的一种,是最推荐的单例实现。因为通过反射或者序列化可以破坏单例模式,但是枚举的单例模式是无法通过反射破坏的。

1
2
3
4
5
6
7
8
// 枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {}
}

//使用时
Singleton.INSTANCE.whateverMethod();

破坏单例模式

通过反射或者序列化可以破坏单例模式。枚举的单例模式除外

  • 序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 向文件中写入对象
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
    oos.writeObject(Singleton.getInstance());
    oos.close();
    // 从文件中读取对象
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
    Singleton s1 = (Singleton) ois.readObject();
    ois.close();
    // 比较两个对象
    System.out.println(s1 == Singleton.getInstance()); // false

在反序列化的时候,会通过反射调用无参构造方法创建一个新的对象。

  • 反射

    1
    2
    3
    4
    5
    6
    7
    8
    // 通过反射获取构造方法
    Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
    // 设置构造方法的访问权限
    c.setAccessible(true);
    // 通过构造方法创建对象
    Singleton s2 = c.newInstance();
    // 比较两个对象
    System.out.println(s2 == Singleton.getInstance()); // false

通过反射获取构造方法,然后通过构造方法创建对象。
通过反射可以获取私有的构造方法,所以可以通过反射创建对象。

可以通过一些方法来防止破坏单例模式:

  • 序列化
    在单例类中添加readResolve()方法,返回单例对象。这样在反序列化的时候,会调用readResolve()方法而不是无参构造,返回单例对象。
    1
    2
    3
    4
    5
    // 添加readResolve()方法
    private Object readResolve() throws ObjectStreamException {
    //返回单例对象
    return instance;
    }
  • 反射
    在单例类的构造方法中添加判断,如果已经存在单例对象,那么就抛出异常。
    1
    2
    3
    4
    5
    6
    // 添加判断
    private Singleton() {
    if (instance != null) {
    throw new RuntimeException();
    }
    }

JDK中的RunTime是一个单例类,具体上是一个饿汉式的单例类。

工厂方法模式

比如一个咖啡店系统中,包含咖啡店类、咖啡类、咖啡工厂类。
对于咖啡类,有子类美式咖啡、拿铁咖啡等,在咖啡店类中,需要根据用户的选择来创建不同的咖啡。

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 abstract class Coffee {
public abstract String getName();
public void addMilk() {
System.out.println("加牛奶");
}
public void addSugar() {
System.out.println("加糖");
}
}
// 美式咖啡
public class AmericanCoffee extends Coffee {
@Override
public String getName() {
return "美式咖啡";
}
}
// 拿铁咖啡
public class LatteCoffee extends Coffee {
@Override
public String getName() {
return "拿铁咖啡";
}
}

在点咖啡的时候,需要根据用户的选择来创建不同的咖啡,这时候就需要咖啡工厂类。
咖啡店和咖啡工厂之间依赖,和具体的咖啡对象解耦,生产不同的咖啡只需要修改咖啡工厂类即可,不需要修改咖啡店类。
或者说是把生产的过程交给了咖啡工厂类,咖啡店只需要调用咖啡工厂类的方法即可。

简单工厂模式

简单工厂模式又叫静态工厂模式,是工厂方法模式的一种特例。不属于23种设计模式。可以说简单工厂模式其实是一种编程习惯。

简单工厂模式包含三个角色

  • 抽象产品
    抽象产品是具体产品的父类,可以是接口或者抽象类。他定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品
    具体产品是抽象产品的子类,由一个具体的类来实现抽象产品中的抽象方法,生产出具体的产品。
  • 工厂类
    工厂类是简单工厂模式的核心,包含一个创建产品的方法,可以根据参数的不同返回不同的具体产品对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 工厂类
public class CoffeeFactory {
public static Coffee createCoffee(String type) {
Coffee coffee = null;
switch (type) {
case "american":
coffee = new AmericanCoffee();
break;
case "latte":
coffee = new LatteCoffee();
break;
default:
throw new RuntimeException("对不起,您点的咖啡没有");
}
return coffee;
}
}

显然,简单工厂模式的缺点是不符合开闭原则,如果需要添加新的咖啡,需要修改工厂类的代码。

静态工厂模式

静态工厂模式是简单工厂模式的一种特例,同样不属于23种设计模式。可以说静态工厂模式其实是一种编程习惯。
实际上就是把工厂类的方法改为静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 工厂类
public class CoffeeFactory {
public static Coffee createCoffee(String type) {
Coffee coffee = null;
switch (type) {
case "american":
coffee = new AmericanCoffee();
break;
case "latte":
coffee = new LatteCoffee();
break;
default:
throw new RuntimeException("对不起,您点的咖啡没有");
}
return coffee;
}
}

这样就不需要创建工厂类的对象了,直接调用工厂类的静态方法即可。

工厂方法模式

工厂方法模式是简单工厂模式的进一步抽象和推广。工厂方法模式是23种设计模式之一。他完美地符合了开闭原则

定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类
工厂方法模式包含四个角色:

  • 抽象产品
    抽象产品是具体产品的父类,可以是接口或者抽象类。他定义了产品的规范,描述了产品的主要特性和功能
  • 具体产品
    具体产品是抽象产品的子类,由一个具体的工厂创建。
  • 抽象工厂
    抽象工厂是具体工厂的父类,可以是接口或者抽象类(多态)。通过它来调用具体工厂中的方法创建产品。
  • 具体工厂
    具体工厂是抽象工厂的子类,由一个具体的类来实现抽象工厂中的抽象方法
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
// 抽象产品
public abstract class Coffee {
public abstract String getName();
public void addMilk() {
System.out.println("加牛奶");
}
public void addSugar() {
System.out.println("加糖");
}
}
// 具体产品 1
public class AmericanCoffee extends Coffee {
@Override
public String getName() {
return "美式咖啡";
}
}
// 具体产品 2
public class LatteCoffee extends Coffee {
@Override
public String getName() {
return "拿铁咖啡";
}
}
// 抽象工厂
public abstract class CoffeeFactory {
public abstract Coffee createCoffee();
}
// 具体工厂 1
public class AmericanCoffeeFactory extends CoffeeFactory {
@Override
public Coffee createCoffee() {
return new AmericanCoffee();
}
}
// 具体工厂 2
public class LatteCoffeeFactory extends CoffeeFactory {
@Override
public Coffee createCoffee() {
return new LatteCoffee();
}
}
// 测试类
public class Test {
public static void main(String[] args) {
CoffeeFactory factory = new AmericanCoffeeFactory();
Coffee coffee = factory.createCoffee();
System.out.println(coffee.getName());
coffee.addMilk();
coffee.addSugar();
}
}

这种方式很好地符合了开闭原则,同时在创建对象时,无需关注具体地创建过程,只需要关注所需产品对应的工厂即可。
缺点在于,每增加一个产品,就需要增加一个具体工厂类,增加了额外的开发量。

抽象工厂模式

抽象工厂模式是23种设计模式之一。他完美地符合了开闭原则。还解决了工厂方法模式中的缺点。

在工厂方法模式中,每种工厂只能创建一种产品。
而在抽象工厂模式中,每个工厂可以创建多种产品。抽象工厂考虑的是产品族的概念。一个工厂可以生产相同品牌的不同产品或者是同一类型的不同品牌的产品。

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
73
74
75
76
77
78
79
80
81
82
// 抽象产品
public abstract class Coffee {
public abstract String getName();
public void addMilk() {
System.out.println("加牛奶");
}
public void addSugar() {
System.out.println("加糖");
}
}
// 具体产品 1
public class AmericanCoffee extends Coffee {
@Override
public String getName() {
return "美式咖啡";
}
}
// 具体产品 2
public class LatteCoffee extends Coffee {
@Override
public String getName() {
return "拿铁咖啡";
}
}
// 抽象产品
public abstract class Dessert {
public abstract String getName();
}
// 具体产品 1
public class Cake extends Dessert {
@Override
public String getName() {
return "蛋糕";
}
}
// 具体产品 2
public class IceCream extends Dessert {
@Override
public String getName() {
return "冰淇淋";
}
}
// 抽象工厂
public abstract class AbstractFactory {
public abstract Coffee createCoffee();
public abstract Dessert createDessert();
}
// 具体工厂 1
public class AmericanDessertFactory extends AbstractFactory {
@Override
public Coffee createCoffee() {
return new AmericanCoffee();
}
@Override
public Dessert createDessert() {
return new Cake();
}
}
// 具体工厂 2
public class LatteDessertFactory extends AbstractFactory {
@Override
public Coffee createCoffee() {
return new LatteCoffee();
}
@Override
public Dessert createDessert() {
return new IceCream();
}
}
// 测试类
public class Test {
public static void main(String[] args) {
AbstractFactory factory = new AmericanDessertFactory();
// AbstractFactory factory = new LatteDessertFactory();
Coffee coffee = factory.createCoffee();
Dessert dessert = factory.createDessert();
System.out.println(coffee.getName());
System.out.println(dessert.getName());
coffee.addMilk();
coffee.addSugar();
}
}

产品族的概念使得工厂可以创建多种产品,当一个产品族被设计成一起工作后,他能够保证客户端始终只使用同一个产品族中的对象。
但是,当需要增加一个产品时,需要修改抽象工厂和所有的具体工厂,增加了额外的开发量。

在实际中,工厂方法模式的使用频率更高一些,抽象工厂模式的使用频率相对较低。

模式扩展

一种固定的套路:使用简单工厂和配置文件来解除共工厂和产品的耦合。
在配置文件中,定义类的类名和全类名,在工厂中,使用反射来创建对象。

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
// 配置文件
# 配置文件
# key = value
# key: 类名
# value: 类的全限定名
coffee=cn.itcast.factory.Coffee
dessert=cn.itcast.factory.Dessert
// 工厂类
public class CoffeeFactory{
//定义对象存储容器
private static HashMap<String, Coffee> map = new HashMap<>();
//静态代码块,类加载时执行,加载配置文件中的数据到map中
static {
Properties prop = new Properties();
try {
//加载配置文件
prop.load(CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties"));
for (Object key : prop.keySet()) {
String className = prop.getProperty(key.toString());
Class clazz = Class.forName(className);
//通过反射创建对象,强制转换为Coffee类型
Coffee coffee = (Coffee) clazz.newInstance();
map.put(key.toString(), coffee);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//提供根据key获取对象的方法
public static Coffee createCoffee(String key) {
return map.get(key);
}
}

简单来说,就是将工厂类和产品类的耦合关系解除,通过配置文件来维护工厂类和产品类的关系。在使用时,传入一个特定的key,在工厂中通过反射获取到对应的对象。

Collection.iterator()方法

什么情况下应该使用工厂方法模式?考虑以下情况:

  • 创建对象需要大量重复的代码。
  • 创建对象需要访问某些信息,而这些信息不应该包含在复合类中。
  • 创建对象的生命周期必须集中管理,以保证在整个程序中具有一致的行为。

java.util.Collection接口中定义了一个抽象的iterator()方法,该方法就是一个工厂方法。
Collection 接口就是一个根抽象工厂,下面还有 List 等接口作为抽象工厂,再往下有 ArrayList 等具体工厂。java.util.Iterator 接口是根抽象产品,下面有 ListIterator 等抽象产品,还有 ArrayListIterator 等作为具体的产品。简化后的 UML 图如下:
抽象工厂模式
ltr

原型模式

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
结构上原型模式包含以下角色:

  • 抽象原型类:规定了具体原型对象必须实现的接口。
  • 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象
  • 访问类:使用具体原型类中的 clone() 方法来复制新的对象。

实现上,原型模式可以实现浅克隆和深克隆。区别在于,浅克隆只能克隆基本数据类型,而引用数据类型只复制引用,也就是指向同一个地址
而深克隆可以克隆基本数据类型和引用数据类型,也就是在内存中存在多个引用对象而不是指向同一个地址

这里的原型模式指的是浅克隆。在Java中,Object类提供了一个clone()方法来实现浅克隆,
也就是说Cloneable接口就是所谓的抽象原型类,我们需要实现Cloneable接口并重写clone()方法来构造具体原型类。

浅克隆

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
// 实现Cloneable接口,构造具体原型类
public class Sheep implements Cloneable {
private String name;
private int age;
private String color;
public Sheep friend; //是对象, 克隆是会如何处理, 默认是浅拷贝

public Sheep(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}

//克隆该实例,使用默认的clone方法来完成
@Override
protected Object clone() {
Sheep sheep = null;
try {
//直接调用Object类的clone()方法,返回一个Object实例,强转为Sheep类型
sheep = (Sheep) super.clone();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return sheep;
}
}

// 测试
public class Client {
public static void main(String[] args) {
Sheep sheep = new Sheep("tom", 1, "白色");
sheep.friend = new Sheep("jack", 2, "黑色");
Sheep sheep2 = (Sheep) sheep.clone();
Sheep sheep3 = (Sheep) sheep.clone();
Sheep sheep4 = (Sheep) sheep.clone();
Sheep sheep5 = (Sheep) sheep.clone();
System.out.println("sheep2=" + sheep2 + "sheep2.friend=" + sheep2.friend.hashCode());
System.out.println("sheep3=" + sheep3 + "sheep3.friend=" + sheep3.friend.hashCode());
System.out.println("sheep4=" + sheep4 + "sheep4.friend=" + sheep4.friend.hashCode());
System.out.println("sheep5=" + sheep5 + "sheep5.friend=" + sheep5.friend.hashCode());
}
}

通过上面的测试,我们可以看到,克隆出来的对象和原对象的引用类型是一样的,也就是说,克隆出来的对象和原对象的引用类型指向同一个地址。

例如在奖状类中,除了获奖人姓名不同,其他都是一样的,这时候我们就可以使用原型模式来复制奖状类,只需要修改获奖人姓名即可。
相较于直接new一个新的对象,原型模式的效率更高,因为克隆出来的对象和原对象的引用类型指向同一个地址,不需要再次创建对象。

深克隆

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

深克隆的实现方式有很多,可以使用对象流的方式来实现深克隆。
```Java
//对象流-深克隆
public class DeepProtoType implements Serializable, Cloneable {
public String name; //String 属性
public DeepCloneableTarget deepCloneableTarget;// 引用类型

public DeepProtoType() {
super();
}

//深拷贝 - 方式 1 使用clone 方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object deep = null;
//这里完成对基本数据类型(属性)和String的克隆
deep = super.clone();
//对引用类型的属性,进行单独处理
DeepProtoType deepProtoType = (DeepProtoType) deep;
deepProtoType.deepCloneableTarget = (DeepCloneableTarget) deepCloneableTarget.clone();
return deepProtoType;
}

//深拷贝 - 方式2 通过对象的序列化实现 (推荐)
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);
DeepProtoType copyObj = (DeepProtoType) ois.readObject();
return copyObj;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
//关闭流
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e2) {
System.out.println(e2.getMessage());
}
}
}
}

第一种方式是通过重写clone()方法来实现深克隆,而第二种方式是通过对象流的方式来实现深克隆。对象流读取的是对象的二进制流,创建的是一个新的对象。

建造者模式

将一个复杂对象的构建与它的表示分离,使得同样地构建过程可以创建不同的表示

举例来说,同样的装配过程可以用来创建不同的产品,因为在装配的时候,相同步骤可以使用不同的组件。
建造者模式分离了对象子组件的单独构造(由Builder来负责)和装配(由Director负责)。从而可以构造出复杂的对象。
由于实现了构建和装配的解耦,不同的构建器可以相同地构建过程构建出不同的产品,也就是说不同的构建器可以用来构建不同的产品。

建造者模式的角色分为:

  • Product:产品类,由多个部件组成。
  • Builder:抽象建造者,定义了产品的创建和装配方法。
  • ConcreteBuilder:具体建造者,实现了抽象建造者的方法,并且返回一个组建好的对象。
  • Director:指挥者,调用具体建造者来创建产品对象,指挥者并不知道具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
  • Client:客户端,使用指挥者来创建产品对象。

整个过程即,客户端通过指挥者来创建产品对象,指挥者调用具体建造者来创建产品对象,具体建造者创建产品对象并返回给指挥者,指挥者将产品对象返回给客户端。

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

//产品类
public class Product {
private String buildA;
private String buildB;
private String buildC;
private String buildD;

public String getBuildA() {
return buildA;
}

public void setBuildA(String buildA) {
this.buildA = buildA;
}

public String getBuildB() {
return buildB;
}

public void setBuildB(String buildB) {
this.buildB = buildB;
}

public String getBuildC() {
return buildC;
}

public void setBuildC(String buildC) {
this.buildC = buildC;
}

public String getBuildD() {
return buildD;
}

public void setBuildD(String buildD) {
this.buildD = buildD;
}

@Override
public String toString() {
return "Product{" +
"buildA='" + buildA + '\'' +
", buildB='" + buildB + '\'' +
", buildC='" + buildC + '\'' +
", buildD='" + buildD + '\'' +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//抽象建造者,规定要实现复杂对象的哪些部分的创建和装配方法
public abstract class Builder {
//创建产品对象
protected Product product = new Product();

public abstract void buildA();

public abstract void buildB();

public abstract void buildC();

public abstract void buildD();

//返回产品对象
public Product getProduct() {
return product;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//具体建造者,实现抽象建造者的方法
public class ConcreteBuilder extends Builder {
@Override
public void buildA() {
product.setBuildA("建造A");
}

@Override
public void buildB() {
product.setBuildB("建造B");
}

@Override
public void buildC() {
product.setBuildC("建造C");
}

@Override
public void buildD() {
product.setBuildD("建造D");
}
}
1
2
3
4
5
6
7
8
9
10
11
//指挥者,调用具体建造者的方法来创建产品对象
public class Director {
//指挥具体的建造者完成产品的创建
public Product build(Builder builder) {
builder.buildA();
builder.buildB();
builder.buildC();
builder.buildD();
return builder.getProduct();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//客户端,使用指挥者来创建产品对象
public class Client {
public static void main(String[] args) {
//创建指挥者
Director director = new Director();
//创建具体的建造者
ConcreteBuilder concreteBuilder = new ConcreteBuilder();
//指挥者使用具体的建造者来创建产品对象
Product product = director.build(concreteBuilder);
System.out.println(product);
}
}

应用实例:

  1. 去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的“套餐”
  2. JAVA中的StringBuilder

优点: 建造者模式的封装性很好。使用建造者模式可以使客户端不必知道产品内部组成的细节。另外变化性页很好,如果需要增加一个产品类,只需要再创建一个具体的建造者就可以了。
缺点: 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,那么就不适合使用建造者模式了,因此其使用范围受到一定限制。

建造者模式的扩展

当一个类的构造函数参数过多时,可以使用建造者模式来解决这个问题。

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
//通过内部类实现建造者模式来解决构造函数参数过多的问题

public class Robot {
private String manufacturer;
private final String name;
private String brand;
private final long id;
private double weight;
private double width;
private double height;

private Robot(Builder builder) {
this.brand = builder.brand;
this.height = builder.height;
this.manufacturer = builder.manufacturer;
this.weight = builder.weight;
this.width = builder.width;
this.name = builder.name;
this.id = builder.id;
}
//内部类Builder,用来构建Robot对象
public static class Builder {
private String manufacturer;
private final String name;
private String brand;
private final long id;
private double weight;
private double width;
private double height;

public Builder(long id, String name) {
this.id = id;
this.name = name;
}
//通过return this来实现链式调用
public Builder manufacturer(String manufacturer) {
this.manufacturer = manufacturer;
return this;
}

public Builder brand(String brand) {
this.brand = brand;
return this;
}

public Builder height(double height) {
this.height = height;
return this;
}

public Builder width(double width) {
this.width = width;
return this;
}

public Builder weight(double weight) {
this.weight = weight;
return this;
}

public Robot build() {
return new Robot(this);
}
}
}

在Robot类中加入了静态类Builder,由Builder来构造Robot所需要的字段属性,默认id与name是必须传入的不可更改字段。然后再Robot中加入私有的构造器,传入的参数就是Builder对象。
当需要使用的时候,只需要调用Builder的build方法就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
Robot robot = new Robot.Builder(1, "robot1")
.brand("brand1")
.height(1.1)
.weight(2.2)
.width(3.3)
.manufacturer("manufacturer1")
.build();
System.out.println(robot);
}
}

在Lombok中,**@Builder**注解就是使用了建造者模式来解决构造函数参数过多的问题,原理与上面的代码相同。

创建者模式总结

  1. 工厂模式与建造者模式的区别:工厂模式关注的是创建单个产品,而建造者模式则关注创建符合对象,多个部分。
  2. 抽象工厂模式与建造者模式的区别:抽象工厂模式关注的是创建多个产品,而建造者模式则关注创建同一对象,多个部分。

结构型模式

结构型模式描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过简单积木的组合形成复杂的、功能更为强大的结构。
分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或者聚合来组合对象。其中对象结构型模式会更加灵活。

代理模式

访问对象不合适或者不能直接引用目标对象,代理对象在访问目标对象时引入一定程度的间接性。
代理模式分为静态代理和动态代理,静态代理类在编译时就已经实现,而动态代理类在运行时才实现。动态代理类又分为JDK动态代理和CGLIB动态代理。

比如,租房者,房东和中介。租房者需要租房,但是不能直接找房东,而是通过中介来找房东,中介就是代理对象。

代理模式分为三种角色:抽象角色、代理角色和真实角色。

  • 抽象角色:声明真实角色和代理角色的共同接口,这样一来在任何使用真实角色的地方都可以使用代理角色,客户端通常需要针对抽象角色进行编程。
  • 代理角色:代理角色内部含有对真实角色的引用,从而可以在任何时候操作真实角色对象;代理角色提供一个与真实角色相同的接口,以便在任何时候都可以替代真实角色;代理角色通常在将客户端调用传递给真实角色之前或者之后执行某些操作,而不是单纯地将调用传递给真实角色对象。
  • 真实角色:代理角色所代表的真实对象,是我们最终要引用的对象。
    对于中介的例子来说,抽象角色就是租房者和房东的共同接口,代理角色就是中介,真实角色就是房东。

静态代理

静态代理类在编译时就已经实现,代理类和真实类都实现同一个接口,代理类中持有真实类的引用,代理类的方法中调用真实类的方法。

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 interface Rent {
void rent();
}
//真实角色
public class Landlord implements Rent {
@Override
public void rent() {
System.out.println("房东出租房子");
}
}
//代理角色
public class Proxy implements Rent {
private final Landlord landlord;

public Proxy(Landlord landlord) {
this.landlord = landlord;
}

@Override
public void rent() {
System.out.println("中介带租客看房");
landlord.rent();
System.out.println("中介收取中介费");
}
}
//客户端
public class Client {
public static void main(String[] args) {
Landlord landlord = new Landlord();
Proxy proxy = new Proxy(landlord);
proxy.rent();
}
}

我们的抽象角色Rent分别被真实角色Landlord和代理角色Proxy实现,代理角色Proxy中持有真实角色Landlord的引用,代理角色Proxy的rent方法中调用真实角色Landlord的rent方法。

动态代理

相较于静态代理,动态代理类在运行时才实现,动态代理类又分为JDK动态代理和CGLIB动态代理。

  1. JDK动态代理

JDK动态代理是通过反射来实现的,需要实现InvocationHandler接口,重写invoke方法,invoke方法中调用真实角色的方法。
实现InvocationHandler,重写invoke方法的目的是

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
//抽象角色
public interface Rent {
void rent();
}
//真实角色
public class Landlord implements Rent {
@Override
public void rent() {
System.out.println("房东出租房子");
}
}
//代理工厂
public class ProxyFactory implements InvocationHandler {

//目标对象
private final Object target;

public ProxyFactory(Object target) {
this.target = target;
}

public Object getProxyInstance() {
//获取代理对象,需要三个参数:类加载器、接口、InvocationHandler
//代理类是在程序运行时产生的,需要类加载器加载到内存中
//参数二:目标对象实现的接口的字节码对象,使用泛型来确认类型
//参数三:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行的目标对象方法作为参数传入
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("中介带租客看房");
//执行目标对象的方法
Object result = method.invoke(target, args);
System.out.println("中介收取中介费");
return result;
}
}
//客户端
public class Client {
public static void main(String[] args) {
Landlord landlord = new Landlord();
ProxyFactory proxyFactory = new ProxyFactory(landlord);
Rent proxy = (Rent) proxyFactory.getProxyInstance();
proxy.rent();
}
}
  1. CGLIB动态代理

CGLIB是通过继承来实现的,需要实现MethodInterceptor接口,重写intercept方法,intercept方法中调用真实角色的方法。
相比于 JDK 动态代理的实现,CGLIB 动态代理不需要实现与目标类一样的接口,而是通过方法拦截的方式实现代理,代码实现如下,首先方法拦截接口 net.sf.cglib.proxy.MethodInterceptor。

1
2
3
4
5
6
7
8
9
10
11

public class TargetInterceptor implements MethodInterceptor {

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("CGLIB 调用前");
Object result = proxy.invokeSuper(obj, args);
System.out.println("CGLIB 调用后");
return result;
}
}

通过方法拦截接口调用目标类的方法,然后在该被拦截的方法进行增强处理,
实现方法拦截器接口的 intercept 方法里面有四个参数:

  • obj 代理类对象
  • method 当前被代理拦截的方法
  • args 拦截方法的参数
  • proxy 代理类对应目标类的代理方法

创建 CGLIB 动态代理类使用 net.sf.cglib.proxy.Enhancer 类进行创建,它是 CGLIB 动态代理中的核心类,首先创建个简单的代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CglibProxy {

public static Object getProxy(Class<?> clazz){
Enhancer enhancer = new Enhancer();
// 设置类加载
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
//TargetInterceptor - 代理实现的功能
enhancer.setCallback(new TargetInterceptor());
// 创建代理类
return enhancer.create();
}

}

设置被代理类的信息和代理类拦截的方法的回调执行逻辑,就可以实现一个代理类。 实现 CGLIB 动态代理调用:

1
2
3
4
5
6
7
8
public class Main {

@Test
public void dynamicProxy() throws Exception {
Animal cat = (Animal) CglibProxy.getProxy(Cat.class);
cat.call();
}
}

执行结果:

  • CGLIB 调用前
  • Cat call
  • CGLIB 调用后

CGLIB 动态代理简单应用就这样实现
但是 Enhancer 在使用过程中,常用且有特色功能还有回调过滤器 CallbackFilter 的使用,它在拦截目标对象的方法时,可以有选择性的执行方法拦截,也就是选择被代理方法的增强处理。使用该功能需要实现 net.sf.cglib.proxy.CallbackFilter 接口。 现在增加一个方法拦截的实现:

1
2
3
4
5
6
7
8
9
10
11

public class TargetInterceptor2 implements MethodInterceptor {

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("CGLIB 调用前 TargetInterceptor2");
Object result = proxy.invokeSuper(obj, args);
System.out.println("CGLIB 调用后 TargetInterceptor2");
return result;
}
}

在Cat中新增hobby方法

1
2
3
4
5
6
7
8
9
10
11
public class Cat implements Animal {

@Override
public void call() {
System.out.println("Cat call");
}

public void hobby(){
System.out.println("Cat hobby");
}
}
1
2
3
4
5
6
7
8
9
public class TargetCallbackFilter implements CallbackFilter {
@Override
public int accept(Method method) {
if ("hobby".equals(method.getName()))
return 1;
else
return 0;
}
}

通过这样的Filter,accept方法返回的值就是Callback数组的下标,
也就是说,如果accept方法返回0,那么就会执行Callback数组中下标为0的Callback即call,如果返回1,那么就会执行Callback数组中下标为1的Callback即hobby。

为演示调用不同的方法拦截器,在 Enhancer 设置中,
使用Enhancer#setCallbacks设置多个方法拦截器,参数是一个数组,
TargetCallbackFilter#accept返回的数字即为该数组的索引,决定调用的回调选择器。

1
2
3
4
5
6
7
8
9
10
11
12
public class CglibProxy {

public static Object getProxy(Class<?> clazz){
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(clazz.getClassLoader());
enhancer.setSuperclass(clazz);
enhancer.setCallbacks(new Callback[]{new TargetInterceptor(), new TargetInterceptor2()});
enhancer.setCallbackFilter(new TargetCallbackFilter());
return enhancer.create();
}

}
1
2
3
4
5
6
7
8
9
public class Main {

@Test
public void dynamicProxy() throws Exception {
Animal cat = (Animal) CglibProxy.getProxy(Cat.class);
cat.call();
cat.hobby();
}
}

执行结果:

  • CGLIB 调用前
  • Cat call
  • CGLIB 调用后
  • CGLIB 调用前 TargetInterceptor2
  • Cat hobby
  • CGLIB 调用后 TargetInterceptor2

代理模式的总结

和静态代理相比,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。
具体来说,接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。

代理模式主要有以下优点:

  1. 代理模式分离了调用者和被调用者,在一定程度上降低了系统的耦合度
  2. 代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了的作用和保护了目标对象的作用
  3. 代理对象可以扩展目标对象的功能

适配器模式

Adapter,类似于现实中,不同国家的插座规格不同,需要一个转换器来转换。

将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。主要分为两类,类适配器和对象适配器。
主要包含以下角色:

  • Target:目标抽象类,当前系统业务所期待的接口,它可以是抽象类或接口
  • Adapter:适配器类,它可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配。
  • Adaptee:适配者类,它是被适配的角色,它定义了一个已经存在的接口,这个接口需要适配。

类适配器

类适配器使用继承的方式,继承被适配类,实现目标接口,完成适配。

比如读卡器,读卡器可以读取TF卡,但是电脑只能读取SD卡,所以需要一个转换器,将TF卡转换成SD卡,这样电脑就可以读取TF卡了。

1
2
3
4
5
//TF
public interface TFCard {
String readTF();
void writeTF(String msg);
}
1
2
3
4
5
//SD
public interface SDCard {
String readSD();
void writeSD(String msg);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//TF实现类
public class TFCardImpl implements TFCard {
@Override
public String readTF() {
String msg = "TFCard read msg : hello word TFCard";
return msg;
}

@Override
public void writeTF(String msg) {
System.out.println("TFCard write msg : " + msg);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//SD实现类
public class SDCardImpl implements SDCard {
@Override
public String readSD() {
String msg = "SDCard read msg : hello word SDCard";
return msg;
}

@Override
public void writeSD(String msg) {
System.out.println("SDCard write msg : " + msg);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//适配器
public class SDAdapterTF extends TFCardImpl implements SDCard {
//重写接口方法
@Override
public String readSD() {
System.out.println("adapter read tf card ");
//调用父类方法
return readTF();
}
//重写接口方法
@Override
public void writeSD(String msg) {
System.out.println("adapter write tf card ");
//调用父类方法
writeTF(msg);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Main {

@Test
public void adapter() throws Exception {
//创建适配器对象
SDCard sdCard = new SDAdapterTF();
//读取SD卡
String msg = sdCard.readSD();
System.out.println(msg);
sdCard.writeSD("hello word");
}
}

这样就可以通过适配器,将TF卡转换成SD卡,实现了电脑读取TF卡的功能。

对象适配器

对象适配器使用组合的方式,将被适配类作为一个属性,实现目标接口,完成适配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SDAdapterTF implements SDCard {
//声明适配者类 区别于继承,这里直接持有一个被适配者类的对象实例
private TFCard tfCard;
//通过构造函数传入适配者类
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
//实现目标接口方法
@Override
public String readSD() {
System.out.println("adapter read tf card ");
return tfCard.readTF();
}
//实现目标接口方法
@Override
public void writeSD(String msg) {
System.out.println("adapter write tf card ");
tfCard.writeTF(msg);
}
}
1
2
3
4
5
6
7
8
9
10
public class Main {

@Test
public void adapter() throws Exception {
SDCard sdCard = new SDAdapterTF(new TFCardImpl());
String msg = sdCard.readSD();
System.out.println(msg);
sdCard.writeSD("hello word");
}
}

装饰着模式

比如以快餐店为例,快餐店有炒饭,炒面等等,每一种快餐都有一个基本价格,比如炒饭是10元,炒面是8元,但是顾客可以根据自己的需求,选择加鸡蛋,加肉等等,这样就会有不同的价格,比如加鸡蛋的炒饭是12元,加肉的炒饭是15元,这样就可以通过装饰者模式来实现。

如果使用继承的方式去实现,就会造成大量子类的产生,不利于扩展,而且每一种子类都会有大量重复的代码,不利于代码的复用。
而在装饰者模式中,只需要创建一个基本的快餐类,然后创建一个装饰者类,将基本的快餐类作为属性,然后在装饰者类中实现加鸡蛋,加肉等等的方法,这样就可以实现不同的快餐了。

一个装饰者模式中,主要包含以下角色:

  • Component:抽象构件,定义一个抽象接口,以规范准备接收附加责任的对象。 好比快餐店的快餐,有一个基本的价格。
  • ConcreteComponent:具体构件,实现抽象构件,通过装饰角色为其添加一些职责。 好比炒饭,炒面等等。
  • Decorator:装饰角色,持有一个抽象构件的引用,装饰对象接受所有客户端的请求,并把这些请求转发给真实的对象,这样就能在真实对象调用前后增加新的功能。
  • ConcreteDecorator:具体装饰角色,负责给构件对象增加新的责任。 好比加鸡蛋,加肉等等。

要求用户输入一段文字,比如 Hello Me,然后屏幕输出几个选项

  • 加密
  • 反转字符串
  • 转成大写
  • 转成小写
  • 扩展或者剪裁到10个字符,不足部分用!补充
    用户输入 任意组合,比如 1,3 表示先执行1的逻辑,再执行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
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
//组件对象的接口
public interface ICompoment {

String display(String str);
}

//具体的组件对象
public class DetailCompoment implements ICompoment {
@Override
public String display(String str) {
System.out.println("原来内容:"+str);
return str;
}
}

//所有装饰器的父类,实现了组件接口
public abstract class Decorator implements ICompoment{
//持有了一个组件对象
protected ICompoment compoment;

public Decorator(ICompoment compoment) {
this.compoment = compoment;
}

@Override
public String display(String str) {
return compoment.display(str);
}
//对组件对象进行装饰的抽象方法
public abstract String transform(String str);
}

//加密、解密工具类
public class EnDecodeUtil {

private static final char password='a';

public static String encodeDecode(String str){
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
chars[i] = (char) (chars[i] ^ password);
}
return new String(chars);
}
}

//加密装饰器
public class EncodeDecorator extends Decorator {

public EncodeDecorator(ICompoment compoment) {
super(compoment);
}

@Override
public String display(String str) {
String display = super.display(str);
return transform(display);
}

@Override
public String transform(String str) {
System.out.println("invoke EncodeDecorator....");
return EnDecodeUtil.encodeDecode(str);
}
}

//解密装饰器
public class DecodeDecorator extends Decorator {

public DecodeDecorator(ICompoment compoment) {
super(compoment);
}

@Override
public String display(String str) {
String display = super.display(str);
return transform(display);
}

@Override
public String transform(String str) {
System.out.println("invoke DecodeDecorator...");
return EnDecodeUtil.encodeDecode(str);
}
}

//反转 装饰器
public class ReverseDecorator extends Decorator {

public ReverseDecorator(ICompoment compoment) {
super(compoment);
}

@Override
public String display(String str) {
String display = super.display(str);
String transform = transform(display);
return transform;
}

@Override
public String transform(String str) {
System.out.println("invoke ReverseDecorator....");
StringBuilder sb = new StringBuilder(str);
return sb.reverse().toString();
}

}

//转为大写的装饰器
public class UpperDecorator extends Decorator {
public UpperDecorator(ICompoment compoment) {
super(compoment);
}

@Override
public String display(String str) {
String display = super.display(str);
String transform = transform(display);
return transform;
}

@Override
public String transform(String str) {
System.out.println("invoke UpperDecorator....");
return str.toUpperCase();
}
}

//转为大写的装饰器
public class UpperDecorator extends Decorator {
public UpperDecorator(ICompoment compoment) {
super(compoment);
}

@Override
public String display(String str) {
String display = super.display(str);
String transform = transform(display);
return transform;
}

@Override
public String transform(String str) {
System.out.println("invoke UpperDecorator....");
return str.toUpperCase();
}
}

//转为小写的装饰器
public class LowerDecorator extends Decorator{
public LowerDecorator(ICompoment compoment) {
super(compoment);
}

@Override
public String display(String str) {
String display = super.display(str);
String transform = transform(display);
return transform;
}

@Override
public String transform(String str) {
System.out.println("invoke lowerDecorator....");
return str.toLowerCase();
}
}

//裁剪、扩充装饰器
public class ExtendOrSplitDecorator extends Decorator {
public ExtendOrSplitDecorator(ICompoment compoment) {
super(compoment);
}

@Override
public String display(String str) {
String display = super.display(str);
String transform = transform(display);
return transform;
}

@Override
public String transform(String str) {
System.out.println("invoke ExtendOrSplitDecorator....");
if (str != null) {
if (str.length() > 10) {
return str.substring(0,10);
}else{
int repeatCount = 10 -str.length();
StringBuilder sb = new StringBuilder(str);
for (int i = 0; i < repeatCount; i++) {
sb.append("!");
}
return sb.toString();
}
}
return null;
}
}

//裁剪、扩充装饰器
public class ExtendOrSplitDecorator extends Decorator {
public ExtendOrSplitDecorator(ICompoment compoment) {
super(compoment);
}

@Override
public String display(String str) {
String display = super.display(str);
String transform = transform(display);
return transform;
}

@Override
public String transform(String str) {
System.out.println("invoke ExtendOrSplitDecorator....");
if (str != null) {
if (str.length() > 10) {
return str.substring(0,10);
}else{
int repeatCount = 10 -str.length();
StringBuilder sb = new StringBuilder(str);
for (int i = 0; i < repeatCount; i++) {
sb.append("!");
}
return sb.toString();
}
}
return null;
}
}

//测试代码
public static void main(String[] args) {
//将输入内容转为大写,再反转
ReverseDecorator reverseDecorator = new ReverseDecorator(new UpperDecorator(new DetailCompoment()));
String display = reverseDecorator.display("wo shi zhongguo ren.");
System.out.println(display);

//将输入内容转为小写,在裁剪或者扩展
ExtendOrSplitDecorator decorator = new ExtendOrSplitDecorator(new LowerDecorator(new DetailCompoment()));
String display1 = decorator.display("I Love");
System.out.println(display1);

//将输入内容转为小写,再反转,然后加密
EncodeDecorator decorator1 = new EncodeDecorator(new ReverseDecorator(new LowerDecorator(new DetailCompoment())));
String display2 = decorator1.display("顶级机密:1941年12月 日本偷袭珍珠港! 银行密码是:1234ADC");
System.out.println(display2);
System.out.println("++++++++++");
//将输入内容先反转、再转为小写,然后加密
EncodeDecorator decorator2 = new EncodeDecorator(new LowerDecorator(new ReverseDecorator(new DetailCompoment())));
String display3 = decorator2.display("顶级机密:1941年12月 日本偷袭珍珠港! 银行密码是:1234ADC");
System.out.println(display3);

System.out.println("============");
//对上面的加密内容,进行解密
DecodeDecorator decodeDecorator = new DecodeDecorator(decorator1);
String display4 = decodeDecorator.display("顶级机密:1941年12月 日本偷袭珍珠港! 银行密码是:1234ADC");
System.out.println(display4);
}

装饰者模式和代理模式

装饰者模式和代理模式的结构图是一样的,但是他们的目的是不一样的,装饰者模式是为了增强功能,而代理模式是为了增强访问控制。

桥接模式

桥接模式是一种结构型设计模式,可以将业务逻辑和实现逻辑相分离,使得二者可以独立变化。

试想,在一个有多种可能会变化的维度的系统中,
用继承方式会造成类爆炸,扩展起来不灵活。
每次在一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式。

桥接(Bridge)模式包含以下主要角色:

  • 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。

开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,
常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。

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
//视频文件
public interface VideoFile {
void decode(String fileName);
}

//avi文件
public class AVIFile implements VideoFile {
public void decode(String fileName) {
System.out.println("avi视频文件:"+ fileName);
}
}

//rmvb文件
public class REVBBFile implements VideoFile {

public void decode(String fileName) {
System.out.println("rmvb文件:" + fileName);
}
}

//操作系统版本
public abstract class OperatingSystemVersion {

protected VideoFile videoFile;

public OperatingSystemVersion(VideoFile videoFile) {
this.videoFile = videoFile;
}

public abstract void play(String fileName);
}

//Windows版本
public class Windows extends OperatingSystem {

public Windows(VideoFile videoFile) {
super(videoFile);
}

public void play(String fileName) {
videoFile.decode(fileName);
}
}

//mac版本
public class Mac extends OperatingSystemVersion {

public Mac(VideoFile videoFile) {
super(videoFile);
}

public void play(String fileName) {
videoFile.decode(fileName);
}
}

//测试类
public class Client {
public static void main(String[] args) {
OperatingSystem os = new Windows(new AVIFile());
os.play("战狼3");
}
}

桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。

如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化。

外观模式

外观模式是一种结构型设计模式,它为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用。

外观(Facade)模式包含以下主要角色:

  • 外观(Facade)角色 :为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色 :实现系统的部分功能,客户可以通过外观角色访问它。

外观模式的目的不是给子系统添加新的功能接口,而是为了让外部减少与子系统内多个模块的交互,松散耦合,从而让外部能够更简单地使用子系统。

【例】智能家电控制

小明的爷爷已经60岁了,一个人在家生活:
每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。
所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭

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
73
74
75
76
77
78
79
80
81
82
83
84
//灯类
public class Light {
public void on() {
System.out.println("打开了灯....");
}

public void off() {
System.out.println("关闭了灯....");
}
}

//电视类
public class TV {
public void on() {
System.out.println("打开了电视....");
}

public void off() {
System.out.println("关闭了电视....");
}
}

//控制类
public class AirCondition {
public void on() {
System.out.println("打开了空调....");
}

public void off() {
System.out.println("关闭了空调....");
}
}

//智能音箱
public class SmartAppliancesFacade {

private Light light;
private TV tv;
private AirCondition airCondition;

public SmartAppliancesFacade() {
light = new Light();
tv = new TV();
airCondition = new AirCondition();
}

public void say(String message) {
if(message.contains("打开")) {
on();
} else if(message.contains("关闭")) {
off();
} else {
System.out.println("我还听不懂你说的!!!");
}
}

//起床后一键开电器
private void on() {
System.out.println("起床了");
light.on();
tv.on();
airCondition.on();
}

//睡觉一键关电器
private void off() {
System.out.println("睡觉了");
light.off();
tv.off();
airCondition.off();
}
}

//测试类
public class Client {
public static void main(String[] args) {
//创建外观对象
SmartAppliancesFacade facade = new SmartAppliancesFacade();
//客户端直接与外观对象进行交互
facade.say("打开家电");
facade.say("关闭家电");
}
}

组合模式

组合模式是一种结构型设计模式,它将对象组合成树形结构,以表示“部分-整体”的层次结构。

在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。
可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。
但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,
但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。

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

组合模式主要包含三种角色:

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

我们在访问别的一些管理系统时,经常可以看到类似的菜单。
一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目)
也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,
我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。

不管是菜单还是菜单项,都应该继承自统一的接口,这里姑且将这个统一的接口称为菜单组件。

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
//菜单组件  不管是菜单还是菜单项,都应该继承该类
public abstract class MenuComponent {

protected String name;
protected int level;

//添加菜单
public void add(MenuComponent menuComponent){
throw new UnsupportedOperationException();
}

//移除菜单
public void remove(MenuComponent menuComponent){
throw new UnsupportedOperationException();
}

//获取指定的子菜单
public MenuComponent getChild(int i){
throw new UnsupportedOperationException();
}

//获取菜单名称
public String getName(){
return name;
}

public void print(){
throw new UnsupportedOperationException();
}
}

这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,
Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,
举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。
这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。

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
public class Menu extends MenuComponent {

private List<MenuComponent> menuComponentList;

public Menu(String name,int level){
this.level = level;
this.name = name;
menuComponentList = new ArrayList<MenuComponent>();
}

@Override
public void add(MenuComponent menuComponent) {
menuComponentList.add(menuComponent);
}

@Override
public void remove(MenuComponent menuComponent) {
menuComponentList.remove(menuComponent);
}

@Override
public MenuComponent getChild(int i) {
return menuComponentList.get(i);
}

@Override
public void print() {

for (int i = 1; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
for (MenuComponent menuComponent : menuComponentList) {
menuComponent.print();
}
}
}

Menu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MenuItem extends MenuComponent {

public MenuItem(String name,int level){
this.name = name;
this.level = level;
}

@Override
public void print() {
for (int i = 1; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
}
}

MenuItem类只需要实现print方法即可,因为MenuItem类不具有添加菜单,移除菜单和获取子菜单的功能。

在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。

透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 MenuComponent 声明了 add、remove 、getChild 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。
透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,
叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)

在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,
而是在树枝节点 Menu 类中声明并实现这些方法。
安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,
因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。

组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。

享元模式

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。

享元(Flyweight )模式中存在以下两种状态:

  1. 内部状态,即不会随着环境的改变而改变的可共享部分。
  2. 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

享元模式的主要有以下角色:

  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。
俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。

1
2
3
4
5
6
7
public abstract class AbstractBox {
public abstract String getShape();

public void display(String color) {
System.out.println("方块形状:" + this.getShape() + " 颜色:" + color);
}
}

定义具体的方块类,这里只定义了三类方块,实际上可以定义更多的方块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IBox extends AbstractBox {
@Override
public String getShape() {
return "I";
}
}

public class LBox extends AbstractBox {
@Override
public String getShape() {
return "L";
}
}

public class OBox extends AbstractBox {
@Override
public String getShape() {
return "O";
}
}

定义一个享元工厂,用来创建具体的方块对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BoxFactory {
private static Map<String, AbstractBox> map;

static {
map = new HashMap<>();
map.put("I", new IBox());
map.put("L", new LBox());
map.put("O", new OBox());
}

public static AbstractBox getShape(String key) {
return map.get(key);
}
}

客户端调用

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
AbstractBox box1 = BoxFactory.getShape("I");
box1.display("灰色");

AbstractBox box2 = BoxFactory.getShape("L");
box2.display("绿色");

AbstractBox box3 = BoxFactory.getShape("O");
box3.display("红色");
}
}

优点

  • 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
  • 享元模式中的外部状态相对独立,且不影响内部状态
    缺点
  • 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

一个系统有大量相同或者相似的对象,造成内存的大量耗费。
对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

行为型模式