软件体系结构

本文最后更新于:2024年10月7日 下午

一、软件设计模式基础概述

软件设计模式简介

软件设计模式类型

软件设计模式原则

开闭原则(Open Close Principle)

  • 扩展开放,对修改关闭
  • 再程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
  • 想要达到这样的效果,需要使用接口抽象类
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 class FruitShop {
public void sellFruit(Fruit fruit) {
// sell the fruit
fruit.sell();
}
}

public abstract class Fruit {
// 抽象类没有包含足够的信息来描绘一个具体的对象,不能实例化对象,所以抽象类必须被继承,才能被使用。
int fruit_type;

public abstract void sell();
}

// 苹果
public class Apple extends Fruit {
Apple() {
super.fruit_type = 1;
//“this.”是一个实例对象内部为了区分实例变量和局部变量。
//而“super.”是一个实例对象为了区分是子类的成员还是父类的成员。
//父类有,子类也有,子类想访问父类的,“super.”不能省略。
}

@Override
public void sell() {
System.out.println("卖出一斤苹果!");
}
}

// 香蕉
public class Banana extends Fruit {
Banana() {
super.fruit_type = 2;
}

@Override
public void sell() {
System.out.println("卖出一斤香蕉!");
}
}

// 西瓜
public class Watermelon extends Fruit {
Watermelon() {
super.fruit_type = 3;
}

@Override
public void sell() {
System.out.println("卖出一斤西瓜!");
}
}

里氏代换原则(Liskov Substitution Principle)

  • 任何基类可以出现的地方,子类一定可以出现。

  • LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。

  • 里氏代换原则是对开闭原则的补充。

  • 实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

例子:鸟不一定都会飞,所以鸟的基类里不要设置飞的方法,要设置跑的方法。

依赖倒转原则(Dependence Inversion Principle)

  • 这个原则是开闭原则的基础。

  • 具体内容:针对接口编程依赖于抽象不依赖于具体

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象

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
public class DependenceInversionPrinciple {
// 依赖倒置原则
// 1. 高层模块不应该依赖低层模块,二者都应该依赖其抽象。
// - 高层模块(如Person类)依赖于接口(IReceiver),而不是依赖于具体的实现类(如Email或WeChat)。
// 2. 抽象不应该依赖细节,细节应该依赖抽象。
// - IReceiver接口是抽象的,具体的实现类(Email和WeChat)依赖于这个抽象接口。
// 3. 依赖倒置的中心思想是面向接口编程,而不是依赖具体的实现类。
// - 这样做可以增强系统的扩展性和可维护性,增加新功能时无需修改现有代码。

public static void main(String[] args) {
// 客户端代码,无需修改,因为高层模块Person依赖于抽象的IReceiver接口
// 而不是具体的实现类,所以我们可以灵活替换不同的消息接收器
Person person = new Person();
// 使用Email类实现消息接收
person.receive(new Email());
// 使用WeChat类实现消息接收
person.receive(new WeChat());
}
}

interface IReceiver {
// 抽象方法,所有实现IReceiver接口的类都必须实现此方法
// 这个方法返回一个字符串,表示接收到的消息内容
public String getInfo();
}

class Email implements IReceiver {
// 实现IReceiver接口的getInfo方法
// 返回表示Email消息内容的字符串
@Override
public String getInfo() {
return "电子邮件信息:hello,Email";
}
}

class WeChat implements IReceiver {
// 实现IReceiver接口的getInfo方法
// 返回表示WeChat消息内容的字符串
@Override
public String getInfo() {
return "微信消息:hello,WeChat";
}
}

class Person {
// 接收一个IReceiver接口类型的参数
// 该方法根据传入的不同消息接收器,输出相应的消息内容
public void receive(IReceiver receiver) {
// 调用接口的getInfo方法,输出接收到的消息内容
System.out.println(receiver.getInfo());
}
}

接口隔离原则(Interface Segregation Principle)

  • 使用多个隔离的接口,比使用单个接口要好。

  • 客户端不应该依赖它 不需要的接口。降低类之间的耦合度

  • 建立单一接口,尽量细化接口,接口中的方法尽量少。

  • 注意适度原则,一定要适度,过大的话会增加耦合性,而过小的话会增加复杂性和开发成本。

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
public class InterfaceSegregationPrinciple {
// 使用多个隔离的接口,比使用单个接口要好
// 接口要小而专,不要让接口干这干那的,接口要干就一件事情
// 接口要尽量小,但是要有限度,一个接口只服务于一个子模块或业务逻辑
// 为依赖接口的类定制服务,只暴露给这些类需要的方法,不多也不少
// 不要为不用的接口去实现,不要为未来可能的功能去实现

public interface SwimAnimalAction {
void swim();
}

public interface FlyAnimalAction {
void fly();
}

public interface EatAnimalAction {
void eat();
}

public class Dog implements EatAnimalAction, SwimAnimalAction {
@Override
public void eat() {
System.out.println("Dog eat");
}

@Override
public void swim() {
System.out.println("Dog swim");
}
}
}

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 UnSegregation {
public interface AnimalAction {
void eat();

void sleep();

void fly();

void swim();
}

public class Dog implements AnimalAction {
@Override
public void eat() {
System.out.println("Dog eat");
}

@Override
public void sleep() {
System.out.println("Dog sleep");
}

@Override
public void fly() {
System.out.println("Dog can't fly");
}

@Override
public void swim() {

}
}
}

迪米特法则(Demeter Principle)(又称最少知道原则)

  • 一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
  • 降低系统的耦合度,使类与类之间保持松耦合状态。
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 DemeterPrinciple {
// 最少知道原则
public class Computer {
private void saveCurrentTask() {
// 保存当前任务
}

private void closeService() {
// 关闭服务
}

private void closeScreen() {
// 关闭屏幕
}

private void closePower() {
// 关闭电源
}

public void close() {
//只提供一个服务给用户访问,防止出现歧义,错误
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}

public class User {
private Computer computer;

public void clickCloseButton() {
computer.close();
//若不隔离,用户可能会先closePower(),再close(),引发错误
}
}
}

合成复用原则(Composite Reuse Principle)

  • 复用类我们可以通过继承合成两种方式来实现。
  • 尽量使用合成/聚合的方式,而不是使用继承。
    • 继承的优点:容易实现并且修改和扩展继承来的内容。
    • 继承的缺点:它最大的缺点就是增加了类之间的依赖,继承是属于白箱复用,父类对子类来说是透明的,这破坏了类的封装性
    • 合成复用存在的缺点就是在系统中会存在较多的对象需要管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//简单依赖
public class CompositeReusePrinciple {
public static void main(String[] args) {
B b = new B();
b.methodB(new A());
}
}

class A {
public void methodA() {
System.out.println("A类的方法执行了");
}
}

class B {
public void methodB(A a) {
System.out.println("B类的方法执行了");
a.methodA();
}
}
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
//聚合
public class Test2 {
public static void main(String[] args) {
B b = new B();
System.out.println("使用聚合的执行结果:");
b.setA(new A());
b.methodB();
}
}

class A {
public void methodA() {
System.out.println("A类的方法执行了");
}
}

class B {
private A a;

public A getA() {
return a;
}

public void setA(A a) {
this.a = a;
}

public void methodB() {
System.out.println("B类的方法执行了");
this.a.methodA();
}
}
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 Test3 {
public static void main(String[] args) {
B b = new B();
System.out.println("使用组合的执行结果:");
b.methodB();
}
}

class A {
public void methodA() {
System.out.println("A类的方法执行了");
}
}

class B {
private A a = new A();

public void methodB() {
System.out.println("B类的方法执行了");
this.a.methodA();
}
}
  • 聚合与组合的区别:
    • 关联(Association):指一个类的实例A使用另外一个类的实例B,这两个对象之间为关联关系,关联关系分为单项关联和双向关联。
    • 聚合(Aggregation)关系是关联关系的一种,耦合度强于关联,他们的代码表现是相同的,仅仅是在语义上有所区别:关联关系的对象间是相互独立的,而聚合关系的对象之间存在着包容关系,他们之间是“整体-个体”的相互关系。
    • 相比于聚合,组合(Compostion)是一种耦合度更强的关联关系。存在组合关系的类表示“整体-部分”的关联关系,“整体”负责“部分”的生命周期,他们之间是共生共死的;并且“部分”单独存在时没有任何意义。

二. 常用软件设计模式

创建型模式

单例模式(Singleton)

  • 单例模式提供了一种创建对象的最佳方式。

  • 单例模式涉及到单一的类:

    • 该类负责创建自己的对象,并确保只有单个对象被创建。
    • 该类提供了一种访问其唯一的对象的方式,可以直接访问不需要实例化该类的对象

To be noted:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。
  • 模式意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 解决问题:一个全局使用的类频繁地创建与销毁
  • 使用时机:当你想控制实例数目,节省系统资源的时候。
  • 解决方案:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  • 关键代码:构造函数是私有的
  • 应用实例:
      1. 一个班级只有一个班主任。
      1. 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

1. 懒汉式,线程不安全(最基础的实现方式)

  • Lazy初始化
  • 无多线程安全,在多线程不能正常工作
  • 没有加锁synchronized,严格意义上它并不算单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LazySingleton {
private static LazySingleton instance;

private LazySingleton() {

} // 核心---私有构造函数

public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
};
}

2. 懒汉式,线程安全

  • lazy初始化:第一次调用才初始化,避免内存浪费。
  • 多线程安全,能够在多线程中很好的工作,但效率很低,99%的情况下不需要同步。
  • 缺点:必须加锁synchronized才能保证单例,但加锁会影响效率
1
2
3
4
5
6
7
8
9
10
11
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();

private HungrySingleton() {

}

public static HungrySingleton getInstance() {
return instance;
}
}

3. 饿汉式

  • 无Lazy初始化,比较常用,类加载时就初始化,浪费内存,容易产生垃圾对象
  • 多线程安全,没有加锁,执行效率会提高。
  • 基于classloader机制避免了多线程的同步问题,不过,instance在类加载时就实例化。
1
2
3
4
5
6
7
8
9
10
11
12
public class HungrySingleton {
//类加载,初始化
private static HungrySingleton instance = new HungrySingleton();

private HungrySingleton() {

}

public static HungrySingleton getInstance() {
return instance;
}
}

4. 双检锁/双重校验锁(DCL,即double-checked locking)

  • JDK版本:JDK1.5起
  • Lazy初始化
  • 采用双锁机制,安全且在多线程下保持高性能
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 DCLSingleton {
// 使用volatile关键字修饰instance变量
private volatile static DCLSingleton instance;

// 私有构造函数,防止外部实例化
private DCLSingleton() {
}

// 获取单例实例的方法
public static DCLSingleton getInstance() {
// 第一次检查,避免不必要的同步
if (instance == null) {
// 同步块,确保线程安全
synchronized (DCLSingleton.class) {
// 第二次检查,防止多个线程同时通过第一次检查
if (instance == null) {
// 分配内存空间,初始化对象,将instance指向分配的内存地址
instance = new DCLSingleton();
}
}
}
return instance;
}
}

5. 登记式/静态内部类

  • Lazy初始化:利用了classloader机制来保证初始化instance时只有一个线程,而Singleton类被装载了,instance不一定被初始化。
  • SingletonHolder类没有被主动使用,只有通过显式调用getInstance方法时,才会显式装载SingletonHolder类,从而实例化instance。实现Singleton类延迟加载。
  • 多线程安全,达到双检锁的功效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
//静态内部类不会在加载外部类时被加载,而是在第一次使用时才被加载。
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

private Singleton() {

}

public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

final关键字的作用

  1. 修饰类:当一个类被声明为final时,它不能被继承。例如,final class Singleton {},这个类不能被其他类继承。
  2. 修饰方法:当一个方法被声明为final时,它不能被子类重写。例如,public final void method() {},这个方法不能在子类中被重写。
  3. 修饰变量:当一个变量被声明为final时,它的值不能被改变。对于基本数据类型,final变量是常量,一旦被初始化,就不能再被修改。对于引用类型,final变量引用的对象不能被改变,但对象本身的内容可以改变。

总结

  • 优点
      1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
      1. 避免对资源的多重占用(比如写文件操作)。
  • 缺点
    • 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
  • 使用场景:
      1. 要求生产唯一序列号
      1. WEB中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
      1. 创建的一个对象需要消耗的资源过多,比如I/O与数据库的连接。
  • 注意事项
    • getInstance方法需要使用同步锁synchronizedSingleton.class)防止多线程同时进入造成instance被多次实例化。

工厂模式(Factory)

  • 使用场景:
      1. 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
      1. 数据库访问,数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
      1. 设计一个连接服务器的框架,需要三个协议,POP3,IMAP,HTTP,可以把这三个作为产品类,共同实现一个接口。
  • 注意事项:
    • 作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。
    • 需要注意的地方就是复杂对象适合使用工程模式
    • 而简单对象,特别是只需要通过new就可以完成对象的创建的对象,无需使用工厂模式。
    • 如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度

抽象工厂模式

  • 抽象工厂模式是围绕一个超级工厂创建其他工厂。
  • 该超级工厂又称为其他工厂的工厂,提供了一种创建对象的最佳方式。
  • 在抽象工厂模式中,接口时负责创建一个相关对象的工厂,不需要显式指定他们的类。每个生产的工厂,都能按照工厂模式提供对象。
  • 模式意图:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
  • 解决问题:主要解决接口选择的问题。
  • 使用时机:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
  • 解决方案:在一个产品族里面,定义多个产品。
  • 关键代码:在一个工厂里聚合多个同类产品。

示例代码

  • 创建ShapeColor接口和实现这些接口的实体类。
1
2
3
4
5
6
public interface Shape {
void draw();
}
public interface Color {
void fill();
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}//省略其他形状
public class Red implements Color {
@Override
public void fill() {
System.out.println("Inside Red::fill() method.");
}
}//省略其他颜色
  • 创建抽象工厂类AbstractFactory
1
2
3
4
5
6
7
public abstract class AbstractFactory {

public abstract Color getColor(String color);

public abstract Shape getShape(String shape);

}
  • 定义工厂类ShapeFactoryColorFactory,均扩展了AbstractFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ShapeFactory extends AbstractFactory {
@Override
public Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
// equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
return new Rectangle();
} else if (shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}

@Override
public Color getColor(String color) {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ColorFactory extends AbstractFactory {

@Override
public Shape getShape(String shapeType) {
return null;
}

@Override
public Color getColor(String color) {
if (color == null) {
return null;
}
if (color.equalsIgnoreCase("RED")) {
return new Red();
} else if (color.equalsIgnoreCase("GREEN")) {
return new Green();
} else if (color.equalsIgnoreCase("BLUE")) {
return new Blue();
}
return null;
}
}
  • 创建一个工厂创造器/生成器类FactoryProducer
1
2
3
4
5
6
7
8
9
10
public class FactoryProducer {
public static AbstractFactory getFactory(String choice) {
if (choice.equalsIgnoreCase("SHAPE")) {
return new ShapeFactory();
} else if (choice.equalsIgnoreCase("COLOR")) {
return new ColorFactory();
}
return null;
}
}
  • AbstractFactoryPatternDemo类使用FactoryProducer来获取AbstractFactory对象。它将向AbstractFactory传递形状信息ShapeCIRCLE/RECTANGLE/SQUARE),以获取它所需要对象的类型。
  • 同时他还向AbstractFactory传递颜色信息ColorRED/GREEN/BLUE),以便获取它所需对象的类型。
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
public class AbstractFactoryPatternDemo {
public static void main(String[] args) {
// 获取形状工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");

// 获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("CIRCLE");

// 调用 Circle 的 draw 方法
shape1.draw();

// 获取形状为 Rectangle 的对象
Shape shape2 = shapeFactory.getShape("RECTANGLE");

// 调用 Rectangle 的 draw 方法
shape2.draw();

// 获取形状为 Square 的对象
Shape shape3 = shapeFactory.getShape("SQUARE");

// 调用 Square 的 draw 方法
shape3.draw();

// 获取颜色工厂
AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");

// 获取颜色为 Red 的对象
Color color1 = colorFactory.getColor("RED");

// 调用 Red 的 fill 方法
color1.fill();

// 获取颜色为 Green 的对象
Color color2 = colorFactory.getColor("GREEN");

// 调用 Green 的 fill 方法
color2.fill();

// 获取颜色为 Blue 的对象
Color color3 = colorFactory.getColor("BLUE");

// 调用 Blue 的 fill 方法
color3.fill();
}
}

总结

  • 优点
    • 当一个产品族的多个对象被设计成一起工作时,他能保证客户端始终只使用同一个产品族中的对象。
  • 缺点
    • 产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的Creator里加代码,又要在具体的里面加代码。
  • 使用场景
      1. QQ换皮肤,一整套一起换
      1. 生成不同操作系统的程序
  • 注意事项
    • 产品族难扩展,产品等级易扩展。

结构性模式

适配器模式(Adapter)

  • 适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
  • 适配器模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。
  • 例如:读卡器是作为内存卡和笔记本之间的适配器
  • 模式意图:将一个类的接口转换成客户希望的另一个接口。使原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  • 解决问题:解决在软件系统中,常常要将一些“现存的对象”放到新的环境中,而新环境要求的接口是现对象不能满足的。
  • 使用时机
      1. 系统需要使用现有的类,而此类的接口不符合系统的需要。
      1. 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
      1. 通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口)
  • 解决方案:继承或依赖(推荐)
  • 关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
  • 应用实例
      1. 美国电器110V,中国220V,就要有一个适配器将110V转化为220V。
      1. JAVA JDK 1.1提供了Enumeration接口,而在1.2中提供了Iterator接口,想要使用1.2的JDK,则要将以前系统的Enumeration接口转化为Iterator接口,这是就需要适配器模式。
      1. JAVA中的jdbc(Java DataBase Connectivity)。

示例代码

  • MediaPlayer接口和一个实现了MediaPlayer接口的实体类AudioPlayer。默认情况下,AudioPlayer可播放mp3格式的文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface MediaPlayer {
public void play(String audioType, String fileName);
}

public class AudioPlayer implements MediaPlayer {
@Override
public void play(String audioType, String fileName) {
// 内置对mp3支持
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file. Name: " + fileName);
}
// mediaAdapter提供了播放其他文件的支持
else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
MediaAdapter mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
  • 另一个接口AdvancedMediaPlayer和实现了AdvancedMediaPlayer接口的实体类。该类可以播放vlcmp4文件。
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 interface AdvancedMediaPlayer {
public void playVlc(String fileName);

public void playMp4(String fileName);
}

public class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: " + fileName);
}

@Override
public void playMp4(String fileName) {
// do nothing
}
}

public class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// do nothing
}

@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: " + fileName);
}
}
  • 如果要让AudioPlayer播放其他格式的音频文件。为了实现这个功能,我们需要创建一个实现了MediaPlayer接口的适配类MediaAdapter,并使用AdvancedMediaPlayer对象来播放所需格式。
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 MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMediaPlayer;
// AdvancedMediaPlayer是一个接口
// advancedMediaPlayer变量实际上是一个接口类型的引用
// 需要通过实现该接口的类来创建实例

public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMediaPlayer = new Mp4Player();
}
}

@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMediaPlayer.playMp4(fileName);
}
}
}
  • AudioPlayer使用适配类MediaPlayer传递所需的音频类型,不需要知道能播放所需格式音频的实际类。AdapterPatternDemo类使用AudioPlayer类来播放各种格式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AudioPlayer implements MediaPlayer {
@Override
public void play(String audioType, String fileName) {
// 内置对mp3支持
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file. Name: " + fileName);
}
// mediaAdapter提供了播放其他文件的支持
else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
MediaAdapter mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
1
2
3
4
5
6
7
8
9
10
public class AdapterPatternDemo {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();

audioPlayer.play("mp3", "beyond the horizon.mp3");
audioPlayer.play("mp4", "alone.mp4");
audioPlayer.play("vlc", "far far away.vlc");
audioPlayer.play("avi", "mind me.avi");
}
}

类适配器模式(继承或实现关系)

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
1
2
3
public interface Target {
public void request();
}
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
1
2
3
4
5
public class Adaptee {
public void specificRequest() {
System.out.println("适配者中的业务代码被调用!");
}
}
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者转换成目标接口,让客户按目标接口的格式访问适配者。
1
2
3
4
5
6
7
public class ClassAdapter extends Adaptee implements Target {
// 重写Target接口中的request方法,使其调用Adaptee类中的specificRequest方法
@Override
public void request() {
specificRequest();
}
}
1
2
3
4
5
6
7
8
9
//客户端代码
public class ClassAdapterTest {
public static void main(String[] args) {
// 创建一个目标对象,使用适配器模式
Target target = new ClassAdapter();
// 调用目标对象的方法
target.request();
}
}

双向适配器模式

  • 适配器模式(Adapter)可扩展为双向适配器模式(Two Way Adapter
  • 双向适配器类可实现适配者(Adaptee)接口和目标(Target)接口双向转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//目标接口及实现
public interface TwoWayTarget {
public void request();
}
public class TargetRealize implements TwoWayTarget {
@Override
public void request() {
System.out.println("目标代码被调用!");
}
}
//适配者接口及实现
public interface TwoWayAdaptee {
public void specificRequest();
}
public class AdapteeRealize implements TwoWayAdaptee {
@Override
public void specificRequest() {
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
//双向适配器
public class TwoWayAdapter implements TwoWayTarget, TwoWayAdaptee {// 双重实现
private TwoWayTarget target;
private TwoWayAdaptee adaptee;

// 根据传入参数构造
public TwoWayAdapter(TwoWayTarget target) {
this.target = target;
}

public TwoWayAdapter(TwoWayAdaptee adaptee) {
this.adaptee = adaptee;
}

// 调用彼此的方法
public void request() {
adaptee.specificRequest();
}

public void specificRequest() {
target.request();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//客户端代码
public class TwoWayAdapterTest {
public static void main(String[] args) {
System.out.println("Target通过双向适配器访问Adaptee:");
TwoWayAdaptee adaptee = new AdapteeRealize();
TwoWayTarget target = new TwoWayAdapter(adaptee);
target.request();// 依然调用自身的方法,但实际调用的是Adaptee的方法
System.out.println("------------------------------------------");
System.out.println("Adaptee通过双向适配器访问Target:");
target = new TargetRealize();
adaptee = new TwoWayAdapter(target);
adaptee.specificRequest();// 依然调用自身的方法,但实际调用的是Target的方法

}
}

总结

  • 优点
    • 可以让任何两个没有关联的类一起运行。
    • 提高了类的复用
    • 增加了类的透明度
    • 灵活性好
  • 缺点
    • 过多使用适配器,会让系统非常凌乱,不易整体把握
    • 比如,明明看到调用A接口,其实内部被是配成了B接口的实现
    • 一个系统如果出现太多这种情况,无异于一场灾难
    • 如果非必要,可以不使用适配器,而是直接对系统进行重构
  • 使用场景
    • 有动机地修改一个正常运行的系统的接口,这时应该考虑适配器模式
  • 注意事项
    • 适配器不是在详细设计时添加的,而是解决正在服役的项目的问题

桥接模式(Bridge)

  • 桥接模式(Bridge Pattern)通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
  • 桥接模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型可被结构化改变而互不影响。
  • 模式意图:将抽象部分与实现部分分离,使他们都可以独立的变化。
  • 解决问题:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
  • 使用时机:实现系统可能有多个角度分类,每一种角度都可能变化。
  • 解决方案:把这种多角度分类分离出来,让它们独立变化,减少他们的耦合。
  • 关键代码抽象类依赖实现类

示例代码

  • 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//实现化角色:颜色
public interface Color {
String getColor();
}
//具体实现化角色:黄色
public class Yellow implements Color {
@Override
public String getColor() {
return "yellow";
}
}
//具体实现化角色:红色
public class Red implements Color {
@Override
public String getColor() {
return "red";
}
}
  • 抽象化(Abstraction)角色:定义抽象,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法
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 Bag {
protected Color color; //组合,共生共死

public void setColor(Color color) {
this.color = color;
}

public abstract String getName();
}
//扩展抽象化角色:挎包
public class HandBag extends Bag {
@Override
public String getName() {
return color.getColor() + "HandBag";
}
}
//扩展抽象化角色:钱包
public class Wallet extends Bag {
@Override
public String getName() {
return color.getColor() + "Wallet";
}
}
1
2
3
4
5
6
7
public class BagManage {
public static void main(String[] args) {
Bag bag = new HandBag();
bag.setColor(new Red());
System.out.println(bag.getName());
}
}

总结

  • 优点
    • 抽象和实现的分离。
    • 优秀的扩展能力。
    • 实现细节对用户透明。
  • 缺点
    • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
  • 使用场景
    • 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
    • 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
    • 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
  • 注意事项
    • 对于两个独立变化的维度,使用桥接模式再适合不过了
    • 在软件开发中,有时桥接(Bridge)模式可与适配器模式联合使用。
    • 当桥接(Bridge)模式的实现化角色的接口与现有类的接口不一致时,可以在二者中间定义一个适配器将二者连接起来

代理模式(Proxy)

  • 代理模式(Proxy Pattern):由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介
  • 代理模式也叫做委托模式
  • 模式意图:为其他对象提供一种代理以控制对这个对象的访问。
  • 解决问题:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层
  • 使用时机:想在访问一个类时做一些控制。
  • 解决方案:增加中间层。
  • 关键代码:实现与被代理类组合

静态代理

  • 创建一个Image接口和实现了Image接口的实体类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Image {
void display();
}

public class RealImage implements Image {
private String filename;

public RealImage(String filename) {
this.filename = filename;
loadFromDisk(filename);
}

@Override
public void display() {
System.out.println("Displaying " + filename);
}

private void loadFromDisk(String filename) {
System.out.println("Loading " + filename);
}
}
  • ProxyImage是一个代理类,减少RealImage对象加载的内存占用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProxyImage implements Image {
private RealImage realImage;
private String filename;

public ProxyImage(String filename) {
this.filename = filename;
}

@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
  • ProxyPatternDemo类使用ProxyImage来获取要加载的Image对象,并按照需求进行显示。
1
2
3
4
5
6
7
8
9
public class ProxyPattrenDemo {
public static void main(String[] args) {
Image image = new ProxyImage("huge_image.jpg");
// 图像从磁盘加载
image.display();
// 图像不需要从磁盘加载
image.display();
}
}
  • 优点
    • 可以做到在不修改目标对象功能前提下,对目标功能扩展。
  • 缺点
    • 因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多。
    • 一旦接口增加方法,目标对象与代理对象都要维护。

JDK动态代理

  • JDK自带动态代理。
  • 其通过自己实现InvocationHandler来实现动态代理,真正的代理对象由JDK在运行时为我们动态的来创建。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Car {
void run();
}

class Benz implements Car {
@Override
public void run() {
System.out.println("奔驰车跑起来了");
}
}

class CarUtils {
public static void methodBefore() {
System.out.println("跑之前要点火。。。。。。");
}

public static void methodAfter() {
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
32
33
34
35
36
37
38
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

class MyInvocationHandler implements InvocationHandler {
private Object target;

public void setTarget(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
CarUtils.methodBefore();
method.invoke(target, args);
CarUtils.methodAfter();
return null;
}
}

// 生产代理对象的工厂
class MyProxyFactory {
public static Object getProxy(Object target) {
MyInvocationHandler handle = new MyInvocationHandler();
handle.setTarget(target);
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
handle);
return proxy;
}
}

public class ProxyTest {
public static void main(String[] args) {
Car car = new Benz();
Car carProxy = (Car) MyProxyFactory.getProxy(car);
carProxy.run();
}
}

JDK动态代理

  • 虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。
  • 但是JDK自带动态代理只能支持实现了Interface的类。

总结

  • 优点
    • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
    • 代理对象可以扩展目标对象的功能例如我们想给项目加入缓存、 日志这些功能, 我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
    • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度
  • 缺点
    • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
    • 增加了系统的复杂度;

行为型模式

中介者模式(Mediator)

  • 中介者模式(Mediator Pattern):定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则(最少知道)的典型应用
  • 中介者模式是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。
  • 模式意图:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  • 解决问题:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。
  • 使用时机:多个类相互耦合,形成了网状结构。
  • 解决方案:将网状结构分离为星型结构。
  • 关键代码:对象 Colleague 之间的通信封装到一个类中单独处理。

示例代码

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • 具体中介者(Concrete Mediator)角色实现中介者接口,定义一个List来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色
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 interface Mediator {
// 注册同事
public abstract void register(Colleague colleague);

public abstract void relay(Colleague c1);// 转发
}

// 具体中介者
class ConcreteMediator implements Mediator {
private List<Colleague> colleagues = new ArrayList<Colleague>();

// 注册同事
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMediator(this);
}
}

// 转发消息
public void relay(Colleague c1) {
for (Colleague ob : colleagues) {
if (!ob.equals(c1)) {
((Colleague) ob).receive();
}
}
}
}
  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
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 abstract class Colleague {
protected Mediator mediator; //保存中介者对象

public void setMediator(Mediator mediator) {
this.mediator = mediator;
}

public abstract void receive();

public abstract void send();
}

// 具体同事类
class ConcreteColleague1 extends Colleague {
@Override
public void receive() {
System.out.println("具体同事类1收到请求。");
}

@Override
public void send() {
System.out.println("具体同事类1发出请求。");
mediator.relay(this);// 请中介者转发
}
}

// 具体同事类
class ConcreteColleague2 extends Colleague {
@Override
public void receive() {
System.out.println("具体同事类2收到请求。");
}

@Override
public void send() {
System.out.println("具体同事类2发出请求。");
mediator.relay(this);// 请中介者转发
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class MediatorPattern {
public static void main(String[] args) {
Mediator md = new ConcreteMediator();
Colleague c1 = new ConcreteColleague1();
Colleague c2 = new ConcreteColleague2();
md.register(c1);
md.register(c2);
c1.send();
System.out.println("-------------");
c2.send();
}
}

中介+单例

  • 不定义中介者接着,把具体中介者对象实现成为单例。
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 SimpleMediator {
private static SimpleMediator smd = new SimpleMediator();
private List<SimpleColleague> colleagues = new ArrayList<SimpleColleague>();

private SimpleMediator() {
}

public static SimpleMediator getMediator() {
return smd;
}

public void register(SimpleColleague colleague) {
colleagues.add(colleague);
}

public void relay(SimpleColleague c1) {
for (SimpleColleague ob : colleagues) {
if (!ob.equals(c1)) {
ob.receive();
}
}
}
}
  • 同事对象不持有中介者,而是在需要的时候直接获取中介者对象并调用。
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
//抽象同事类
interface SimpleColleague {
void receive();

void send();
}

// 具体同事类
class SimpleConcreteColleague1 implements SimpleColleague {
SimpleConcreteColleague1() {
SimpleMediator smd = SimpleMediator.getMediator();
smd.register(this);
}

@Override
public void receive() {
System.out.println("具体同事类1收到请求");
}

@Override
public void send() {
System.out.println("具体同事类1发出请求");
SimpleMediator smd = SimpleMediator.getMediator();
smd.relay(this);// 请求中介者转发
}
}

class SimpleConcreteColleague2 implements SimpleColleague {
SimpleConcreteColleague2() {
SimpleMediator smd = SimpleMediator.getMediator();
smd.register(this);
}

@Override
public void receive() {
System.out.println("具体同事类2收到请求");
}

@Override
public void send() {
System.out.println("具体同事类2发出请求");
SimpleMediator smd = SimpleMediator.getMediator();
smd.relay(this);// 请求中介者转发
}
}

总结

  • 优点
    • 降低了类的复杂度,将一对多转化成了一对一。
    • 各个类之间的解耦。
    • 符合迪米特最小知道原则。
  • 缺点
    • 中介者会庞大,变得复杂难以维护
  • 使用场景
    • 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。
    • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
  • 注意事项
    • 不应当在职责混乱时使用。

观察者模式(Observer)

  • 又被称为发布-订阅/模型-视图模式,属于行为型设计模式的一种。
  • 是一个在项目中经常使用的模式。指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
  • 模式意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
  • 解决问题:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
  • 使用时机: 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
  • 解决方案:使用面向对象技术,可以将这种依赖关系弱化。
  • 关键代码:在抽象类里有一个ArrayList存放观察者们。

示例代码

  • 抽象目标(Subject)角色:提供一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
  • 具体目标(Concrete Subject)角色:实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
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
//抽象目标
abstract class Subject {
protected List<Observer> observers = new ArrayList<>();

// 增加观察者方法
public void add(Observer observer) {
observers.add(observer);
}

// 删除观察者方法
public void remove(Observer observer) {
observers.remove(observer);
}

public abstract void notifyObserver();// 通知观察者方法
}

// 具体目标
class ConcreteSubject extends Subject {
@Override
public void notifyObserver() {
System.out.println("具体目标发生改变...");
System.out.println("--------------");
for (Object observer : observers) {// 使用Object,实现“接口隔离”“依赖倒置”原则
((Observer) observer).response();
}
}
}
  • 抽象观察者(Observer)角色:是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
  • 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//抽象观察者
interface Observer {
void response();
}

// 具体观察者
class ConcreteObserver1 implements Observer {
@Override
public void response() {
System.out.println("具体观察者1作出反应!");
}
}

class ConcreteObserver2 implements Observer {
@Override
public void response() {
System.out.println("具体观察者2作出反应!");
}
}
1
2
3
4
5
6
7
8
9
10
public class ObserverPattern {
public static void main(String[] args) {
Subject subject = new ConcreteSubject();
Observer obs1 = new ConcreteObserver1();
Observer obs2 = new ConcreteObserver2();
subject.add(obs1);
subject.add(obs2);
subject.notifyObserver();
}
}

实现观察者模式时要注意具体目标对象和具体观察者对象之间不能直接调用,否则将使两者之间紧密耦合起来,这违反了面向对象的设计原则。

扩展(JDK9被废弃)

  • 在 Java 中,通过 java.util.Observable 类和java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。
    • Observable类:用 Vector 向量存所有要通知的观察者对象,包含3 个方法。
      • void addObserver(Observer o) 方法:将新的观察者对象添加到向量中。
      • void notifyObservers(Object arg) 方法:调用向量中的所有观察者对象的update方法,通知它们数据发生改变。
      • void setChange() 方法:用来设置一个 boolean 类型的内部标志位,注明目标对象发生了变化。当它为真时,notifyObservers() 才会通知观察者。
    • Observer 接口:监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 void update(Observable o,Object arg) 方法,进行相应的工作。

总结

  • 优点
    • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
    • 目标与观察者之间建立了一套触发机制。
  • 缺点
    • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
    • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
    • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
  • 使用场景
    • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
    • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度
    • 一个对象必须通知其他对象,而并不知道这些对象是谁。
    • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
  • 注意事项
    • JAVA 中已经有了对观察者模式的支持类。
    • 避免循环引用。
    • 如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。

访问者模式(Vistor)

  • 将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。
  • 将对数据的操作数据结构进行分离,是行为类模式中最复杂的一种模式。
  • 模式意图:主要将数据结构与数据操作分离。
  • 解决问题:稳定的数据结构和易变的操作耦合问题。
  • 使用时机:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,使用访问者模式将这些封装到类中。
  • 解决方案:在被访问的类里面加一个对外提供接待访问者的接口
  • 关键代码:在数据基础类里面有一个方法接受访问者将自身引用传入访问者

示例代码(抽象)

  • 抽象访问者(Vistor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作visit(),其参数类型表示了被访问的具体元素。
  • 具体访问者(Concrete Vistor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
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
//抽象访问者
interface Visitor {
// 根据参数访问不同的元素
void visit(ConcreteElementA element);

void visit(ConcreteElementB element);
}

// 具体访问者A类
class ConcreteVisitorA implements Visitor {
@Override
public void visit(ConcreteElementA element) {
System.out.println("具体访问者A访问-->" + element.operationA());
}

@Override
public void visit(ConcreteElementB element) {
System.out.println("具体访问者A访问-->" + element.operationB());
}
}

// 具体访问者B类
class ConcreteVisitorB implements Visitor {
@Override
public void visit(ConcreteElementA element) {
System.out.println("具体访问者B访问-->" + element.operationA());
}

@Override
public void visit(ConcreteElementB element) {
System.out.println("具体访问者B访问-->" + element.operationB());
}
}
  • 抽象元素(Element)角色:声明一个包含接受操作accept()接口,被接受的访问者对象作为accept()方法的参数
  • 具体元素(Concrete Element)角色:实现抽象元素角色提供的accept()操作,其方法体通常都是visitor.visit(this),另外具体元素中可能还包含本身业务逻辑的相关操作。
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
// 抽象元素类
interface Element {
void accept(Visitor visitor);
}

// 具体元素A类
class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

public String operationA() {
return "具体元素A的操作";
}
}

// 具体元素B类
class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

public String operationB() {
return "具体元素B的操作";
}
}
  • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中所有元素的方法,通常由List,Set,Map等聚合类实现。
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 ObjectStructure {
// 定义一个Element类型的List集合
private List<Element> list = new ArrayList<>();

// 接受Visitor访问者
public void accept(Visitor visitor) {
// for (Element element : list) {
// element.accept(visitor);
// }

// 下面这种方式更加灵活,因为你可以在遍历的过程中对集合进行修改(如删除元素),同时可以对迭代器进行更细粒度的控制。
Iterator<Element> i = list.iterator();
while (i.hasNext()) {
i.next().accept(visitor);
}
}

// 添加Element元素
public void add(Element element) {
list.add(element);
}

// 移除Element元素
public void remove(Element element) {
list.remove(element);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class VisitorPattern {
public static void main(String[] args) {
ObjectStructure os = new ObjectStructure();
os.add(new ConcreteElementA());
os.add(new ConcreteElementB());

Visitor visitor = new ConcreteVisitorA();
os.accept(visitor);
System.out.println("-------------------");
visitor = new ConcreteVisitorB();
os.accept(visitor);
}
}

示例代码(例子)

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 abstract class Staff {
public String name;
public int kpi;

public Staff(String name) {
this.name = name;
// 生成一个0到9之间的随机整数,并将其赋值给kpi变量
kpi = new Random().nextInt(10);
}

// 核心方法,接受Visitor的访问
public abstract void accept(Visitor visitor);
}

//工程师
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);
}
}

//经理
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);
}
}
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 interface Visitor {
// 访问工程师类型
void visit(Engineer engineer);

// 访问经理类型
void visit(Manager manager);
}

//CEO访问者
public class CEOVisitor implements Visitor {

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

@Override
public void visit(Manager manager) {
System.out.println("经理: " + manager.name + ", KPI:" + manager.kpi + ", 新产品数量: " + manager.getProducts());
}
}

//CTO访问者
public class CTOVisitor implements Visitor {

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

@Override
public void visit(Manager manager) {
System.out.println("经理:" + manager.name + " 产品数量:" + manager.getProducts());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//员工业务报表类
public class BusinessReport {
private List<Staff> mStaffs = new ArrayList<>();

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"));
}

// 为访问者展示报表
public void showReport(Visitor visitor) {
for (Staff staff : mStaffs) {
staff.accept(visitor);
}
}
}
1
2
3
4
5
6
7
8
9
10
public class Client {
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());
}
}

总结

  • 优点
    • 符合单一职责原则
    • 优秀的扩展性
    • 灵活性
  • 缺点
    • 具体元素对访问者公布细节,违反了迪米特原则
    • 具体元素变更比较困难。
    • 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
  • 使用场景
    • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
    • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
  • 注意事项
    • 访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。

软件体系结构
https://cdro.tech/notes/CS/software-architecture/
作者
k9Q6CK42
发布于
2024年9月8日
更新于
2024年10月7日
许可协议