一、设计原则

1.接口隔离原则

核心思想:将接口拆分成多个小的、专门的接口,避免大而全的接口导致实现类必须实现无用的方法,从而提高代码的灵活性、可扩展性和可维护性。

解析:即接口要分割的足够小,功能尽量高内聚,不要太多冗余方法

以下是一个满足接口隔离原则的正例。假设我们有一个图形类Shape,它有一个计算面积的方法calculateArea()和一个计算周长的方法calculatePerimeter()。我们可以将这两个方法拆分成两个接口AreaCalculatablePerimeterCalculatable,然后让实现Shape类的类只实现它们需要的接口。这个设计可以避免实现一个不需要的接口。

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 interface AreaCalculatable {
double calculateArea();
}

public interface PerimeterCalculatable {
double calculatePerimeter();
}

public class Rectangle implements AreaCalculatable, PerimeterCalculatable {
private double width;
private double height;

public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}

@Override
public double calculateArea() {
return width * height;
}

@Override
public double calculatePerimeter() {
return 2 * (width + height);
}
}

反例,假设有一个动物接口,它定义了一系列方法,包括吃、喝、跑和飞等:

1
2
3
4
5
6
public interface Animal {
void eat();
void drink();
void run();
void fly();
}

然而实际上,一个动物很可能并不具有所有的这些能力,比如企鹅不会飞,老鹰也不一定会游泳,这里违背了接口隔离原则,需要接口功能更加细化

2.迪米特法则

核心思想:也叫作最少知识原则(Least Knowledge Principle,LKP),是一种面向对象设计原则,它要求一个对象应当对其他对象有尽可能少的了解,也就是说一个对象应当尽可能少地与其他对象发生相互作用。迪米特法则的核心思想是“不和陌生人说话”,它的目的是降低类之间的耦合度,从而提高系统的灵活性和可维护性。

解析:主要是为了降低类之间的耦合度,便于后期维护与阅读,同时满足高内聚低耦合的设计思想,具体优点如下

  • 降低耦合度:迪米特法则可以降低类之间的耦合度,避免不必要的依赖关系,从而提高系统的灵活性和可维护性。
  • 提高可重用性:迪米特法则可以将类的内部实现和外部接口进行分离,使得类的实现更加独立和可重用。
  • 易于测试:迪米特法则可以降低类之间的依赖关系,使得单元测试更加容易进行。
  • 易于扩展:迪米特法则可以降低类之间的耦合度,使得系统更加容易扩展和维护。

下面是一个违反迪米特法则的反例。假设有一个电视类,它需要调用遥控器类的turnOn和turnOff方法来控制电视的开关状态:

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 TV {
private RemoteControl remoteControl;

public TV(RemoteControl remoteControl) {
this.remoteControl = remoteControl;
}

public void turnOn() {
remoteControl.turnOn();
}

public void turnOff() {
remoteControl.turnOff();
}
}

public class RemoteControl {
public void turnOn() {
System.out.println("Remote control is turning on");
}

public void turnOff() {
System.out.println("Remote control is turning off");
}
}

这个设计看起来很简单,但是如果在实际应用中,遥控器类还包含其他方法,比如换台、调音量等,而电视类只需要控制开关状态,这时候就违反了迪米特法则。因为电视类并不需要知道遥控器类的所有方法,但是它的构造函数却依赖于遥控器类的实例,这就增加了类之间的耦合度,使得系统更加脆弱和难以维护。

一个更好的设计是将电视类与遥控器类解耦,电视类只需要定义自己的方法,而遥控器类也只需要定义自己的方法,两者之间通过一个控制器类来进行连接

3.里氏替换原则

核心思想:是面向对象设计的一个重要原则,由Barbara Liskov和Jeannette Wing提出。该原则指出,任何基类可以出现的地方,子类一定可以出现,且替换后程序不会出错。

解析:里氏替换原则的使用场景包括

  • 多态场景:在多态场景中,一个方法可以接受基类的实例,但实际上会传入子类的实例,这时就需要保证子类能够替换基类,不影响程序的正确性。
  • 继承场景:在继承场景中,子类继承了父类的属性和方法,但子类还可以增加自己的属性和方法,这时需要保证子类与父类的替换性,不破坏原有的逻辑。
  • 接口场景:在接口场景中,一个类实现了一个接口,就需要保证它的子类也能够实现这个接口,保证替换性和扩展性。

实际的使用案例,如集合框架中,List是一个接口,ArrayList和LinkedList是它的两个实现类。由于它们都实现了List接口,所以在使用List的地方,可以用ArrayList或者LinkedList来代替。这里就保证了子类的替换性。

在Spring框架的AOP(面向切面编程)中,切面(Aspect)需要实现Advice接口,Spring提供了很多Advice的实现类,例如BeforeAdvice、AfterAdvice、AroundAdvice等等。用户可以根据自己的需要来选择实现哪个Advice接口,然后把它作为切面的一部分来使用,这里也保证了子类的替换性和扩展性。

4.合成复用原则

核心思想:是面向对象设计的一个重要原则,它强调要优先使用对象组合(Composition)和聚合(Aggregation)关系来复用代码,而不是通过继承关系来复用代码。

解析:该原则的基本思想是,在设计时要优先考虑对象的组合关系而不是继承关系,通过将多个对象组合在一起来实现功能的复用。这种方法比继承更加灵活、可靠,同时也减少了代码的耦合度,提高了代码的可维护性和可扩展性。

假设有一个汽车类(Car),它有多个组成部分,例如引擎(Engine)、车轮(Wheel)等等。我们可以使用合成复用原则来实现汽车类的设计,将组成部分作为汽车的属性来组合起来,而不是通过继承来实现。

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
public class Car {
private Engine engine;
private Wheel[] wheels;

public Car() {
engine = new Engine();
wheels = new Wheel[4];
for (int i = 0; i < 4; i++) {
wheels[i] = new Wheel();
}
}

public void start() {
engine.start();
}

public void stop() {
engine.stop();
}

public void run() {
for (Wheel wheel : wheels) {
wheel.rotate();
}
}
}

public class Engine {
public void start() {
System.out.println("Engine start.");
}

public void stop() {
System.out.println("Engine stop.");
}
}

public class Wheel {
public void rotate() {
System.out.println("Wheel rotate.");
}
}

5.开闭原则

核心思想:一个软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着,当需要对一个软件实体进行改动时,应该尽量通过扩展来实现新功能,而不是通过修改已有的代码来实现。

解析:开闭原则是java里面经常提到的设计原则,简单来说就是对扩展开放,对修改关闭。当我们需要新增或者修改功能时,尽量使用新的类或方法去实现新功能,而不是在已存在的类上去修改,因为这样会导致系统稳定性变差,并且容易造成功能冗余,新的修改也不确定会不会造成新的问题,降低软件风险。

策略模式就是符合开闭原则的很好实现,它是一种行为型设计模式,它定义了一系列算法,将每个算法都封装起来,并且使它们之间可以互换。通过使用策略模式,可以在运行时动态地选择算法,而不需要在代码中显式地指定。这样,当需要添加新的算法时,只需要编写新的策略类并且注册到上下文中即可,而不需要修改原有的代码。

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 interface SortStrategy {
void sort(int[] nums);
}

public class BubbleSort implements SortStrategy {
@Override
public void sort(int[] nums) {
// 冒泡排序算法的具体实现
}
}

public class QuickSort implements SortStrategy {
@Override
public void sort(int[] nums) {
// 快速排序算法的具体实现
}
}

public class SortContext {
private SortStrategy strategy;

public void setStrategy(SortStrategy strategy) {
this.strategy = strategy;
}

public void sort(int[] nums) {
strategy.sort(nums);
}
}

6..依赖倒置原则

核心思想:依赖倒置原则(Dependency Inversion Principle,DIP)是面向对象设计中的一个重要原则,它提倡高层模块不应该依赖低层模块,二者都应该依赖于抽象接口。同时,抽象接口不应该依赖于具体实现,具体实现应该依赖于抽象接口。这个原则可以简单地表述为“依赖于抽象而不是具体实现”。

解析:依赖倒置原则核心就在“依赖于抽象而不是具体实现”这句话上,依赖于抽象的设计有助于简历松耦合的模块关系,从而使系统更容易扩展、修改和维护。同时,在使用依赖倒置原则的时候,我们可以更容易地进行模块的替换,例如我们可以通过更换实现某个接口的具体类来实现模块的替换,而不需要修改其他模块的代码。

下面来看一下不使用依赖倒置原则和使用依赖倒置原则来优化的案例,假设我们有一个图形库,其中包括三个类:Circle、Rectangle和Graphics,它们的代码如下所示:

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 Circle {
public void draw() {
System.out.println("Drawing a circle");
}
}

public class Rectangle {
public void draw() {
System.out.println("Drawing a rectangle");
}
}

public class Graphics {
public void drawCircle() {
Circle circle = new Circle();
circle.draw();
}

public void drawRectangle() {
Rectangle rectangle = new Rectangle();
rectangle.draw();
}
}

在这个图形库中,我们有两个具体的形状类:Circle和Rectangle,它们都有一个draw()方法来绘制它们的形状。我们还有一个Graphics类,它负责绘制图形。

Graphics类直接依赖于Circle和Rectangle类,因为它在它的方法中创建了一个Circle和Rectangle对象来进行绘制。

现在,我们将应用依赖倒置原则,通过引入抽象接口来解耦Graphics类和具体形状类之间的依赖关系。

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 interface Draw {
void draw();
}

public class Circle implements Darw {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}

public class Rectangle implements Darw {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}

public class Graphics {
public void drawGraph() {
Draw circle = new Circle();
circle.draw();

Draw rectangle = new Rectangle();
rectangle.draw();
}
}

7.单一职责原则

核心思想:单一职责原则是一种设计模式,它建议将一个类或者函数的职责限制在一个单一的领域或者任务上。简而言之,一个类或者函数只应该有一个改变的理由。

解析:单一职责原则猛一看和接口隔离原则有些相似,下面对比下两种原则的相似与不同之处

相似之处:

  • 都是为了提高代码的可维护性、可读性和可扩展性。
  • 都要求将职责限制在一个单一的领域或者任务上。

不同之处:

  • 单一职责原则注重的是职责的单一性,即一个类或者函数只应该有一个职责,而接口隔离原则注重的是接口的独立性,即接口应该只包含客户端所需的方法。
  • 单一职责原则针对的是类或者函数的设计,而接口隔离原则针对的是接口的设计。
  • 单一职责原则更多地关注代码内部的组织和设计,而接口隔离原则更多地关注代码之间的交互和通信。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Circle {
private double radius;

public Circle(double radius) {
this.radius = radius;
}

public double getRadius() {
return radius;
}

public double getArea() {
return Math.PI * radius * radius;
}

public double getCircumference() {
return 2 * Math.PI * radius;
}
}

这个代码示例中,Circle 类只有一个职责,即表示一个圆形,提供圆形半径、圆形面积和周长的获取方法。该类的职责是单一的,没有包含其他不相关的逻辑或功能。

设计原则小结

通过上面的详解,可以发现这些设计模式原则,核心思想就是为了使得系统实现高内聚、低耦合的状态,尽量减少各模块之间耦合的同时提高系统的可维护性,可扩展性;

设计模式只是一种思想,并不是说在写代码时要完全符合设计模式原则去做才行,很多情况下我们写出来的代码并不是那么符合设计模式原则,这就是在业务实现上的一种取舍,就像mysql数据库表设计的时候经常会出现一些反范式设计的情况,具体如何去做还要看实际情况。

设计模式(原则)只是帮助我们将代码写的更加灵活规范,但是过于追求符合设计模式也会导致出现新的问题,比如适配器模式过渡使用,就会导致出现大量类,使得代码可读性变差,类关系也变得更加复杂,系统性能也有可能会被影响,因此设计模式要在实际项目中灵活使用。

二、设计模式

1.简单工厂类(不属于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
public class CarFactory {
public static Car createCar(String carType) {
if (carType.equalsIgnoreCase("Sedan")) {
return new SedanCar();
} else if (carType.equalsIgnoreCase("SUV")) {
return new SUVCar();
} else {
throw new IllegalArgumentException("Invalid car type: " + carType);
}
}
}

interface Car {
void drive();
}

class SedanCar implements Car {
public void drive() {
System.out.println("Driving a sedan car...");
}
}

class SUVCar implements Car {
public void drive() {
System.out.println("Driving an SUV car...");
}
}

2.工厂方法模式

定义:是指定义一个创建对象的接口,反让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类种进行,属于创建型设计模式。

主要解决扩展问题

缺点:子类过多

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 interface Logger {
void log(String message);
}

public class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Logging message to file: " + message);
}
}

public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Logging message to console: " + message);
}
}

public interface LoggerFactory {
Logger createLogger();
}

public class FileLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new FileLogger();
}
}

public class ConsoleLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new ConsoleLogger();
}
}

核心思想就是不同的类使用不同的工厂来创建,需要的工厂较多时就会导致出现大量子类

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
// 抽象工厂接口,定义了创建产品的方法
public interface AbstractFactory {
Button createButton();
TextField createTextField();
}

// 具体工厂实现类1,用于创建Windows风格的控件
public class WindowsFactory implements AbstractFactory {
public Button createButton() {
return new WindowsButton();
}
public TextField createTextField() {
return new WindowsTextField();
}
}

// 具体工厂实现类2,用于创建Mac风格的控件
public class MacFactory implements AbstractFactory {
public Button createButton() {
return new MacButton();
}
public TextField createTextField() {
return new MacTextField();
}
}

// 抽象产品接口,定义了产品的方法
public interface Button {
void click();
}

// 具体产品实现类1,用于创建Windows风格的按钮
public class WindowsButton implements Button {
public void click() {
System.out.println("Windows Button Clicked.");
}
}

// 具体产品实现类2,用于创建Mac风格的按钮
public class MacButton implements Button {
public void click() {
System.out.println("Mac Button Clicked.");
}
}

// 抽象产品接口,定义了产品的方法
public interface TextField {
void input();
}

// 具体产品实现类1,用于创建Windows风格的文本框
public class WindowsTextField implements TextField {
public void input() {
System.out.println("Windows TextField Input.");
}
}

// 具体产品实现类2,用于创建Mac风格的文本框
public class MacTextField implements TextField {
public void input() {
System.out.println("Mac TextField Input.");
}
}

// 客户端代码,通过传入不同的工厂对象创建不同风格的控件
public class Client {
public static void main(String[] args) {
// 创建Windows风格的控件
AbstractFactory factory1 = new WindowsFactory();
Button button1 = factory1.createButton();
button1.click();
TextField textField1 = factory1.createTextField();
textField1.input();

// 创建Mac风格的控件
AbstractFactory factory2 = new MacFactory();
Button button2 = factory2.createButton();
button2.click();
TextField textField2 = factory2.createTextField();
textField2.input();
}
}

工厂和产品实现均需要接口定义,接口内定义了这些抽象接口之间的关系,因此实现这些的工厂和产品也具有接口定义的关系

4.单例模式

定义:是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点

隐藏其所有的构造方法

属于创建者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 缺点:能够被反射破坏----构造器黎明抛异常解决
public class LazyStaticInnerClassSingleton{
private LazyStaticInnerClassSingleton(){
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许非法访问");
}
}

private static LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}

private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}

注册式单例-枚举-解决反射破坏单例问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum EnumSingleton{
INSTANCE;

private Object data;

public Object getData(){
return data;
}

public void setData(Object data){
this.data = data;
}

public static EnumSingleton getInstance(){
return INSTANCE;
}
}

注册式单例-容器-线程不安全型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ContainerSingleton{
private ContainerSingleton(){}

private static Map<String,Object> ioc = new HashMap<>();

public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)){
try{
instance = Class.forName(className).newInstance();
ioc.put(className,instance);
} catch(Exception e){
e.printStackTrace();
}
return instance;
}else{
return ioc.get(className);
}
}
}

思考:当前容器方式下,如何解决线程不安全问题?

chatGPT版答案:

这段代码实现了一个容器单例模式,即将对象的创建和管理委托给一个容器来处理,通过类名获取对象的实例,如果容器中没有该实例则创建并存储在容器中,否则直接从容器中获取。不过,这段代码存在以下问题:

  1. 线程不安全:在多线程环境下,存在并发访问和更新 ioc 容器的情况,可能导致数据不一致或者出现线程安全问题。
  2. 可能会导致内存泄漏:容器中存储了对象的实例,如果这些实例没有被及时清理或移除,可能会导致内存泄漏。
  3. 使用反射:在getInstance方法中使用Class.forName(className).newInstance()创建对象,这种方式会带来性能开销,且容易出现异常。建议改用其他创建对象的方式,比如工厂方法模式等。
  4. 只能通过类名获取实例:如果需要传递参数来创建对象,那么就需要修改getInstance方法的实现,这增加了代码的复杂性。

针对以上问题,可以考虑使用线程安全的容器来存储对象实例,如ConcurrentHashMap;对于内存泄漏问题,可以考虑在适当的时候清理容器中的对象实例;对于反射和只能通过类名获取实例的问题,可以使用工厂方法等方式来创建对象。

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
/****线程安全,懒加载的单例****/
public class ContainerSingleton {
private Map<String, Object> ioc = new ConcurrentHashMap<>();

private ContainerSingleton() {}

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

public Object getService(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
synchronized (ioc) {
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
} else {
instance = ioc.get(className);
}
return instance;
}

private static class SingletonHolder {
private static final ContainerSingleton INSTANCE = new ContainerSingleton();
}
}

至于序列化和反序列化不一致问题,可以采用如下解决方案:

单例里面重写readResolve()方法,返回单例对象即可解决序列化单例不一致问题,因为在反序列化时会判断有没有写readResolve()方法,有的话就调用这个方法返回instance

5.代理模式

应用场景:保护目标对象,增强目标对象

静态代理:显示声明代理对象,只能代理指定类型对象

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 interface IUserDao {
void save();
}

// 目标对象:实现了接口
public class UserDao implements IUserDao {
public void save() {
System.out.println("用户保存成功!");
}
}

// 代理对象:也实现了接口
public class UserDaoProxy implements IUserDao {
private UserDao userDao;

public UserDaoProxy(UserDao userDao) {
this.userDao = userDao;
}

public void save() {
System.out.println("代理对象执行开始");
userDao.save(); // 调用目标对象的方法
System.out.println("代理对象执行结束");
}
}

// 客户端代码:使用代理对象调用目标对象的方法
public class Client {
public static void main(String[] args) {
IUserDao userDao = new UserDao();
UserDaoProxy userDaoProxy = new UserDaoProxy(userDao);
userDaoProxy.save();
}
}

动态代理:不论目标是啥,都能进行代理,主要分为jdk和CGLib两种动态代理方式

区别是,jdk的动态代理需要知道被代理类实现的接口,而cglib能够不需要知道

CGLib采用继承的方式,覆盖父类的方法

JDK采用的实现的方式们必须要求代理的目标对象一定要实现一个接口

思想:都是通过生成字节码,重组成一个新的类

其中,JDK Proxy对于用户而言,依赖更强,调用也更复杂

CGLib对于目标则没有任何的要求

CGLib底层没有用到反射,效率和性能更高

JDK Proxy生成逻辑较为简单,执行效率要低,每次都要用反射

注意,CGLib不能有fina修饰的方法,这一种方法会被忽略

Spring种的代理选择原则

  • 当Bean有实现接口时,Spring就会用JDK的动态代理
  • 当Bean没有实现接口时,Spring选择CGLib
  • Spring可以通过配置强制使用CGLib,只需要在Spring的配置文件中加入声明

那么JDK和CGLib到底都是如何实现动态代理的呢?首先看下JDK动态代理实现流程

  1. 定义一个接口和实现该接口的类。
  2. 创建一个 InvocationHandler 对象,并实现 invoke 方法,该方法将在代理对象上调用方法时被调用。
  3. 调用 Proxy.newProxyInstance 方法创建代理对象,并将实现了 InvocationHandler 接口的对象传递给它。
  4. 调用代理对象的方法,代理对象会调用 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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DynamicProxyExample {

public interface Animal {
void say();
}

public static class Dog implements Animal {
@Override
public void say() {
System.out.println("I'm a dog.");
}
}

public static class AnimalInvocationHandler implements InvocationHandler {
private Object target;

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

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After method " + method.getName());
return result;
}
}

public static void main(String[] args) {
Animal dog = new Dog();
AnimalInvocationHandler handler = new AnimalInvocationHandler(dog);
Animal proxy = (Animal) Proxy.newProxyInstance(
dog.getClass().getClassLoader(),
dog.getClass().getInterfaces(),
handler);
proxy.say();
}
}

上述示例中,我们定义了一个接口 Animal 和一个实现该接口的类 Dog。然后,我们创建了一个实现了 InvocationHandler 接口的类 AnimalInvocationHandler,它可以代理 Animal 接口的任何实现类。

main 方法中,我们首先创建了一个 Dog 实例 dog,然后创建了一个 AnimalInvocationHandler 实例 handler,将 dog 传入构造方法。最后,我们使用 Proxy.newProxyInstance 方法创建了一个 Animal 接口的代理对象 proxy,并将 handler 传入其中。

当我们调用 proxy.say() 时,AnimalInvocationHandler.invoke 方法会被调用,并在调用 dog.say() 方法前后输出了一些信息,以此实现了代理的效果。

CGLib实现

  1. 引入 cglib 依赖。
  2. 定义一个类和继承该类的代理类。
  3. 创建一个 MethodInterceptor 对象,并实现 intercept 方法,该方法将在代理对象上调用方法时被调用。
  4. 使用 Enhancer 类创建代理对象,并将实现了 MethodInterceptor 接口的对象传递给它。
  5. 调用代理对象的方法,代理对象会调用 MethodInterceptor.intercept 方法,并将方法名、参数和代理对象传递给它。

引入依赖

1
2
3
4
5
6
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

接下来,定义一个需要被代理的类 UserDao 和一个代理类 UserDaoProxy。这里 UserDao 类只有一个方法 save()UserDaoProxy 类是一个代理类,继承自 UserDao 并重写了 save() 方法。在重写的方法中,我们可以添加一些代理逻辑,比如在调用被代理对象的方法之前或之后进行一些额外的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class UserDaoProxy extends UserDao implements MethodInterceptor {

public Object createProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserDao.class);
enhancer.setCallback(this);
return enhancer.create();
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method invocation");
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method invocation");
return result;
}
}

6.建造者模式

定义:是一个将复杂对象的构建与他的表示分离,使得同样的构建过程可以创建不同的表示

特征:用户只需指定需要建造的类型就可以获得对象,建造过程及细节不需要了解

把复杂对象的创建和使用分离

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
public class Person {
private String firstName;
private String lastName;
private int age;
private String address;

public Person() {}

public Person withFirstName(String firstName) {
this.firstName = firstName;
return this;
}

public Person withLastName(String lastName) {
this.lastName = lastName;
return this;
}

public Person withAge(int age) {
this.age = age;
return this;
}

public Person withAddress(String address) {
this.address = address;
return this;
}

public String toString() {
return "Person{ firstName='" + firstName + "', lastName='" + lastName + "', age=" + age + ", address='" + address + "' }";
}
}

public class Client {
public static void main(String[] args) {
Person person = new Person()
.withFirstName("John")
.withLastName("Doe")
.withAge(30)
.withAddress("123 Main Street");

System.out.println(person);
}
}

7.原型模式

定义:是指原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象

调用者不需要知道任何创建细节,不调用构造函数

通俗讲法就是如何快速构建对象的方法总结,简单工厂将getter、setter封装到某个方法中

JDK提供的实现Cloneable接口,实现快速复制

适用场景

1.类初始化消耗资源较多

2.new产生的一个对象需要非常繁琐的过程

3.构造函数比较复杂

4.循环体中产生大量对象时

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 PrototypeExample implements Cloneable {
private String name;

public PrototypeExample(String name) {
this.name = name;
}

public String getName() {
return name;
}

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

@Override
public PrototypeExample clone() throws CloneNotSupportedException {
return (PrototypeExample) super.clone();
}
}

// 在客户端代码中使用原型模式来创建新对象
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
PrototypeExample prototype = new PrototypeExample("example");
PrototypeExample clone = prototype.clone();

System.out.println("Original object name: " + prototype.getName());
System.out.println("Cloned object name: " + clone.getName());
}
}

原型模式主要是通过类实现cloneable接口,覆写clone接口来实现拷贝,但是这里默认实现的还是浅拷贝,如果想实现深拷贝,需要手动指明非基本类型的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person implements Cloneable {
private String name;
private int age;
private Date birthday;

// 构造函数和getter/setter方法省略

@Override
public Person clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.birthday = (Date) this.birthday.clone();
return cloned;
}
}

8.适配器模式

定义:又叫变压器模式,它的功能是将一个类的接口编程客户端所期待的一种接口,从而使原本因接口不匹配而导致无法在一起工作的两个类能够一起工作

属于结构型设计模式

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 LegacyRectangle {
public int calculateArea() {
int length = 5;
int width = 10;
return length * width;
}
}

public class RectangleAdapter implements Shape {
private LegacyRectangle adaptee;

public RectangleAdapter(LegacyRectangle adaptee) {
this.adaptee = adaptee;
}

@Override
public int getArea() {
return adaptee.calculateArea();
}
}

public class Main {
public static void main(String[] args) {
LegacyRectangle legacyRectangle = new LegacyRectangle();
RectangleAdapter adapter = new RectangleAdapter(legacyRectangle);

int area = adapter.getArea();
System.out.println("Area of rectangle: " + area);
}
}

在这个示例中,我们首先创建了一个LegacyRectangle对象,并将其传递给RectangleAdapter适配器类的构造函数。然后,我们使用适配器对象的getArea()方法来计算长方形面积,并输出结果。

9.桥接模式

定义:是一种结构型设计模式,它将抽象部分与实现部分分离开来,使它们可以独立地变化。桥接模式的核心思想是通过组合的方式,将一个类的实现细节委托给另一个类来完成。

桥接模式的主要目的是将抽象和实现解耦,使得它们可以独立地变化。抽象部分定义了系统的接口和一些高层次的操作,而实现部分负责具体的实现细节。通过将抽象和实现分离开来,桥接模式使得系统更加灵活,易于扩展和维护。

假设我们要设计一个绘图程序,它可以绘制不同类型的图形,例如圆形和矩形。我们可以使用桥接模式来将图形的类型(抽象部分)和绘制图形的具体方式(实现部分)分离开来。

首先,我们定义一个Shape抽象类,它包含一个DrawAPI成员变量,表示绘制图形的实现部分:

1
2
3
4
5
6
7
8
9
public abstract class Shape {
protected DrawAPI drawAPI;

public Shape(DrawAPI drawAPI) {
this.drawAPI = drawAPI;
}

public abstract void draw();
}

然后,我们定义一个DrawAPI接口,表示绘制图形的实现部分:

1
2
3
4
public interface DrawAPI {
void drawCircle(int x, int y, int radius);
void drawRectangle(int x, int y, int width, int height);
}

接着,我们定义两个具体实现类,分别实现DrawAPI接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RedCircle implements DrawAPI {
@Override
public void drawCircle(int x, int y, int radius) {
System.out.println("Drawing red circle at (" + x + "," + y + "), radius " + radius);
}

@Override
public void drawRectangle(int x, int y, int width, int height) {
// do nothing
}
}

public class GreenRectangle implements DrawAPI {
@Override
public void drawCircle(int x, int y, int radius) {
// do nothing
}

@Override
public void drawRectangle(int x, int y, int width, int height)

10.委派模式

定义:又叫委托模式,它的基本作用就是负责任务的调度和任务分配,将任务的分配和执行分离开来,可以看做是一种特殊情况下的静态代理的全权代理。

不属于23种GOF设计模式之一

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
public interface Task {
void run();
}

public class TaskA implements Task {
@Override
public void run() {
System.out.println("Running Task A");
}
}

public class TaskB implements Task {
@Override
public void run() {
System.out.println("Running Task B");
}
}

public class TaskDelegator {
private Map<String, Task> taskMap = new HashMap<>();

public TaskDelegator() {
taskMap.put("TaskA", new TaskA());
taskMap.put("TaskB", new TaskB());
}

public void doTask(String taskName) {
Task task = taskMap.get(taskName);
if (task != null) {
task.run();
} else {
System.out.println("Unknown task: " + taskName);
}
}
}

public class DelegationPatternDemo {
public static void main(String[] args) {
TaskDelegator taskDelegator = new TaskDelegator();
taskDelegator.doTask("TaskA");
taskDelegator.doTask("TaskB");
taskDelegator.doTask("TaskC");
}
}

11.模版模式

定义:是一种行为型设计模式,它定义了一个算法的骨架,将一些步骤延迟到子类中实现,从而使得子类可以改变算法的某些特定步骤,而不改变算法的结构。

解析:模版模式是在框架和日常开发之中使用比较多的一种设计模式,适合具有大量重复操作的步骤,将这些重复内容抽离出来放进模版之中,后续的开发只需要继承模版并实现个性化部分即可

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
public abstract class Game {

abstract void initialize();
abstract void startPlay();
abstract void endPlay();

// 模板方法
public final void play(){
// 初始化游戏
initialize();

// 抽取出的公共部分方法
doSomething1();

// 开始游戏
startPlay();

// 抽取出的公共部分方法
doSomething2();

// 结束游戏
endPlay();
}

protect void doSomething1(){
System.out.println("模版内固定方法1");
}

protect void doSomething2(){
System.out.println("模版内固定方法2");
}
}

public class Cricket extends Game {

@Override
void endPlay() {
System.out.println("Cricket Game Finished!");
}

@Override
void initialize() {
System.out.println("Cricket Game Initialized! Start playing.");
}

@Override
void startPlay() {
System.out.println("Cricket Game Started. Enjoy the game!");
}
}

public class Football extends Game {

@Override
void endPlay() {
System.out.println("Football Game Finished!");
}

@Override
void initialize() {
System.out.println("Football Game Initialized! Start playing.");
}

@Override
void startPlay() {
System.out.println("Football Game Started. Enjoy the game!");
}
}

public class GameTemplatePatternDemo {

public static void main(String[] args) {

Game game = new Cricket();
game.play();
System.out.println();

game = new Football();
game.play();
}
}

12.策略模式

定义:又叫政策模式,它是将定义的算法家族,分别封装起来,让他们之间可以相互替换,从而让算法的变化不会影响到使用算法的用户。

可以避免多重的if-else语句,使用map或者数据库配置

属于行为型模式

以下是一个简单的示例,演示如何使用策略模式计算两个数字的和。该示例包括一个接口Strategy,它定义了一个doOperation方法,以及三个实现类:AddStrategySubtractStrategyMultiplyStrategy,它们分别实现了doOperation方法,用于执行加法、减法和乘法运算。

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
// 定义策略接口
interface Strategy {
int doOperation(int num1, int num2);
}

// 实现加法策略
class AddStrategy implements Strategy {
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}

// 实现减法策略
class SubtractStrategy implements Strategy {
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}

// 实现乘法策略
class MultiplyStrategy implements Strategy {
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}

// 上下文类,负责选择和执行具体策略
class Context {
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}

// 测试代码
public class StrategyDemo {
public static void main(String[] args) {
Context context = new Context(new AddStrategy());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

context = new Context(new SubtractStrategy());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

context = new Context(new MultiplyStrategy());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}