设计模式的六大原则

开放封闭原则(Open Close Principle)

  • 原则思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化
  • 描述:一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。
  • 优点:单一原则告诉我们,每个类都有自己负责的职责,里氏替换原则不能破坏继承关系的体系。
    开闭原则(Open/Closed Principle,OCP)是面向对象设计的五大基本原则之一,由伯特兰·梅耶(Bertrand Meyer)在1988年提出。它指出:

软件实体(类、模块、函数等)应该

  1. 对扩展开放(Open for extension):可以通过增加新功能来进行扩展,而不影响现有系统的稳定性。
  2. 对修改关闭(Closed for modification):一旦设计完成,不应轻易修改已有代码,而是通过扩展的方式来实现新功能。

这个原则的主要目的是为了增强系统的灵活性和可维护性,避免因为修改现有代码而引入新的错误。遵循开闭原则可以使代码更容易扩展,减少维护成本。

例子

假设我们有一个几何图形类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
28
29
30
31
32
33
class Shape {
public double area() {
return 0;
}
}

class Circle extends Shape {
private double radius;

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

@Override
public double area() {
return Math.PI * radius * radius;
}
}

class Rectangle extends Shape {
private double width;
private double height;

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

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

如果我们需要增加一个新的形状,例如三角形,那么我们需要修改现有的代码,增加新的逻辑。

为了遵循开闭原则,我们可以引入一个接口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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
interface Shape {
double area();
}

class Circle implements Shape {
private double radius;

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

@Override
public double area() {
return Math.PI * radius * radius;
}
}

class Rectangle implements Shape {
private double width;
private double height;

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

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

class Triangle implements Shape {
private double base;
private double height;

public Triangle(double base, double height) {
this.base = base;
this.height = height;
}

@Override
public double area() {
return 0.5 * base * height;
}
}

在这种设计中,我们通过实现Shape接口来添加新的形状,而无需修改已有的代码。这种设计使得系统更容易扩展,并且符合开闭原则。

[!NOTE] 笔记
这两种类似的实现方式,区别就在于前者修改了Shape的area方法,违反了对修改关闭的定义。但是两者都实现了对扩展开放,通过重写。

里氏代换原则(Liskov Substitution Principle)

  • 原则思想:使用的基类可以在任何地方使用继承的子类,完美的替换基类。
  • 大概意思是:子类可以扩展父类的功能,但不能改变父类原有的功能。子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,子类中可以增加自己特有的方法。
  • 优点:增加程序的健壮性,即使增加了子类,原有的子类还可以继续运行,互不影响。

里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计的五大基本原则之一,由芭芭拉·里斯科夫(Barbara Liskov)在1987年提出。它指出:

“如果 S 是 T 的子类,那么所有使用 T 的地方必须能够透明地使用 S 的实例而不会产生错误。”

换句话说,在程序中如果使用一个基类的实例是正确的,那么使用它的子类实例也是正确的,并且不会导致程序的逻辑错误。

里氏替换原则的核心思想

  1. 子类型必须能够替换其基类型:子类应该可以替换基类,而不会影响程序的正确性。
  2. 行为一致性:子类在覆盖基类的方法时,应该保证行为的正确性和一致性,不应该违背基类的方法约定。
  3. 保证继承的可替换性:通过继承实现代码复用时,子类必须增强父类的功能,而不是削弱或改变父类的功能。

里氏替换原则示例

违反里氏替换原则的例子

假设我们有一个矩形类和一个正方形类,正方形是特殊的矩形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Rectangle {
private double width;
private double height;

public double getWidth() {
return width;
}

public void setWidth(double width) {
this.width = width;
}

public double getHeight() {
return height;
}

public void setHeight(double height) {
this.height = height;
}

public double getArea() {
return width * height;
}
}

class Square extends Rectangle {
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width);
}

@Override
public void setHeight(double height) {
super.setWidth(height);
super.setHeight(height);
}
}

在上述代码中,Square类继承了Rectangle类,但它重写了setWidthsetHeight方法,使得正方形的宽和高始终相等。这违反了里氏替换原则,因为Square的行为与Rectangle不同。如果我们在某个地方使用Rectangle的实例,但不小心传入了一个Square实例,程序可能会出现意想不到的错误。

符合里氏替换原则的例子

为了符合里氏替换原则,我们可以将正方形和矩形的共同部分提取到一个更通用的父类或接口中,而不是让正方形继承矩形:

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
abstract class Shape {
public abstract double getArea();
}

class Rectangle extends Shape {
private double width;
private double height;

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

public double getWidth() {
return width;
}

public void setWidth(double width) {
this.width = width;
}

public double getHeight() {
return height;
}

public void setHeight(double height) {
this.height = height;
}

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

class Square extends Shape {
private double side;

public Square(double side) {
this.side = side;
}

public double getSide() {
return side;
}

public void setSide(double side) {
this.side = side;
}

@Override
public double getArea() {
return side * side;
}
}

在这种设计中,RectangleSquare都继承自Shape类,并各自实现自己的getArea方法。这样我们就不会在继承过程中产生行为不一致的问题。

总结

里氏替换原则强调子类替换基类时行为的一致性,确保代码的健壮性和灵活性。遵循这一原则有助于实现更好的面向对象设计,提高代码的可维护性和可扩展性。

依赖倒转原则(Dependence Inversion Principle)

  • 依赖倒置原则的核心思想是面向接口编程.

  • 依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,

  • 这个是开放封闭原则的基础,具体内容是:对接口编程,依赖于抽象而不依赖于具体。
    依赖倒转原则(Dependency Inversion Principle,DIP)是面向对象设计的五大基本原则之一。它的主要思想是:

高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

依赖倒转原则的核心思想

  1. 高层模块和低层模块:高层模块(高层次的业务逻辑)和低层模块(具体实现细节)都应该依赖于抽象,而不是彼此依赖。这样可以减少模块之间的耦合,提高系统的灵活性和可维护性。
  2. 面向接口编程:通过接口或抽象类来定义模块之间的交互,而不是通过具体的实现类。这样可以实现模块的可替换性和独立性。

依赖倒转原则示例

违反依赖倒转原则的例子

假设我们有一个Keyboard类和一个Computer类,其中Computer类依赖于Keyboard类的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Keyboard {
public void type() {
System.out.println("Typing on the keyboard...");
}
}

class Computer {
private Keyboard keyboard;

public Computer() {
this.keyboard = new Keyboard(); // 直接依赖于具体的Keyboard类
}

public void type() {
keyboard.type();
}
}

public class Main {
public static void main(String[] args) {
Computer computer = new Computer();
computer.type(); // 输出: Typing on the keyboard...
}
}

在上述代码中,Computer类依赖于具体的Keyboard类。如果我们想要替换键盘的实现,例如使用无线键盘,我们需要修改Computer类的代码,这违反了依赖倒转原则。

符合依赖倒转原则的例子

为了符合依赖倒转原则,我们可以引入一个Keyboard接口,Computer类依赖于这个接口,而不是具体的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
interface Keyboard {
void type();
}

class WiredKeyboard implements Keyboard {
@Override
public void type() {
System.out.println("Typing on the wired keyboard...");
}
}

class WirelessKeyboard implements Keyboard {
@Override
public void type() {
System.out.println("Typing on the wireless keyboard...");
}
}

class Computer {
private Keyboard keyboard;

// 通过构造函数注入依赖
public Computer(Keyboard keyboard) {
this.keyboard = keyboard;
}

public void type() {
keyboard.type();
}
}

public class Main {
public static void main(String[] args) {
Keyboard wiredKeyboard = new WiredKeyboard();
Computer computer1 = new Computer(wiredKeyboard);
computer1.type(); // 输出: Typing on the wired keyboard...

Keyboard wirelessKeyboard = new WirelessKeyboard();
Computer computer2 = new Computer(wirelessKeyboard);
computer2.type(); // 输出: Typing on the wireless keyboard...
}
}

在这种设计中,Computer类依赖于Keyboard接口,而不是具体的Keyboard实现类。这样,当我们需要替换键盘的实现时,只需提供一个新的实现类,而不需要修改Computer类的代码。

总结

依赖倒转原则通过使高层模块和低层模块都依赖于抽象来降低耦合度,提高系统的灵活性和可维护性。通过引入接口或抽象类,我们可以实现模块之间的可替换性和独立性,避免了具体实现之间的紧密耦合。这是面向对象设计中的一个重要原则,有助于构建更加健壮和可扩展的系统。

接口隔离原则(Interface Segregation Principle)

  • 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
  • 例如:支付类的接口和订单类的接口,需要把这俩个类别的接口变成俩个隔离的接口

接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计的五大基本原则之一。它的主要思想是:

客户端不应该被迫依赖它不使用的方法。

换句话说,一个类对另一个类的依赖应该建立在最小的接口上。这个原则鼓励我们将臃肿的接口拆分为更小的、具体的接口,使得客户端只需要知道它们实际使用的方法。

接口隔离原则的核心思想

  1. 精简接口:接口应该尽量小,只包含客户端需要的方法。这可以防止接口变得庞大和臃肿。
  2. 分离职责:将接口按照不同的职责进行拆分,使得实现类可以选择实现不同的职责,而不是被迫实现一个庞大的接口。
  3. 避免臃肿接口:避免创建一个包含大量不相关方法的接口,这样会导致实现类需要实现一些它们不需要的方法。

接口隔离原则示例

违反接口隔离原则的例子

假设我们有一个用于各种打印任务的接口Printer,其中包含了多个打印相关的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Printer {
void printDocument();
void printPhoto();
void printReceipt();
}

class DocumentPrinter implements Printer {
@Override
public void printDocument() {
System.out.println("Printing document...");
}

@Override
public void printPhoto() {
// DocumentPrinter 不需要实现这个方法
}

@Override
public void printReceipt() {
// DocumentPrinter 不需要实现这个方法
}
}

在这个例子中,DocumentPrinter类被迫实现了Printer接口中的所有方法,即使它只需要printDocument()方法。这违反了接口隔离原则,因为DocumentPrinter依赖了它不需要的方法。

符合接口隔离原则的例子

为了符合接口隔离原则,我们可以将Printer接口拆分为更小的、具体的接口:

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
interface DocumentPrinter {
void printDocument();
}

interface PhotoPrinter {
void printPhoto();
}

interface ReceiptPrinter {
void printReceipt();
}

class DocumentPrinterImpl implements DocumentPrinter {
@Override
public void printDocument() {
System.out.println("Printing document...");
}
}

class PhotoPrinterImpl implements PhotoPrinter {
@Override
public void printPhoto() {
System.out.println("Printing photo...");
}
}

class ReceiptPrinterImpl implements ReceiptPrinter {
@Override
public void printReceipt() {
System.out.println("Printing receipt...");
}
}

在这种设计中,每个实现类只需要实现它们所需要的接口,而不会被迫实现不相关的方法。

总结

接口隔离原则鼓励我们将大接口拆分为更小、更具体的接口,使得客户端只依赖它们实际使用的方法。这有助于提高系统的灵活性和可维护性,避免了因为接口变更导致的连锁反应,减少了实现类的负担和复杂度。通过遵循接口隔离原则,我们可以构建更加健壮和可扩展的系统。

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

  • 原则思想:一个对象应当对其他对象有尽可能少地了解,简称类间解耦
  • 大概意思就是一个类尽量减少自己对其他对象的依赖,原则是低耦合,高内聚,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。
  • 优点:低耦合,高内聚。

    单一职责原则(Principle of single responsibility)

  • 原则思想:一个方法只负责一件事情。

  • 描述:单一职责原则很简单,一个方法 一个类只负责一个职责,各个职责的程序改动,不影响其它程序。 这是常识,几乎所有程序员都会遵循这个原则。
  • 优点:降低类和类的耦合,提高可读性,增加可维护性和可拓展性,降低可变性的风险。
    Q:java各种对象的.toString()方法是否违背了里氏替换
    重写 toString() 方法通常不会违背里氏替换原则,原因如下:
  1. 方法签名一致toString() 方法的签名在基类和子类中是一致的,它们都返回 String 类型。

  2. 行为一致性: 重写 toString() 方法不会改变对象的核心行为。无论是基类对象还是子类对象,它们都可以被正确地转换为字符串形式。这种字符串表示主要用于调试和日志记录,不会影响对象在程序中的使用和操作。

  3. 增强而非削弱: 重写 toString() 方法通常是为了提供更多的信息,并没有减少或改变基类的功能。例如,Employee 类在 Person 类的基础上增加了职位信息,这是对功能的增强,而不是削弱。
    里式替换原则的核心就是“约定”,父类与子类的约定。里氏替换原则要求子类在进行设计的时候要遵守父类的一些行为约定。这里的行为约定包括:函数所要实现的功能,对输入、输出、异常的约定,甚至包括注释中一些特殊说明等。

子类方法不能违背父类方法对输入输出异常的约定

1. 前置条件不能被加强

前置条件即输入参数是不能被加强的,就像上面Cache的示例,Redis子类对输入参数Key的要求进行了加强,此时在调用处替换父类对象为子类对象就可能引发异常。

也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。

2. 后置条件不能被削弱

后置条件即输出,假设我们的父类方法约定输出参数要大于0,调用父类方法的程序根据约定对输出参数进行了大于0的验证。而子类在实现的时候却输出了小于等于0的值。此时子类的涉及就违背了里氏替换原则

3. 不能违背对异常的约定

在父类中,某个函数约定,只会抛出 ArgumentNullException 异常, 那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。

创建型设计模式6

单例模式(Singleton pattern)

其目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例

全局访问点(Global Access Point)是指一种设计模式或机制,通过它可以从应用程序中的任何位置访问某个特定的资源或对象。在单例模式中,全局访问点通常是通过一个公共的静态方法来实现的,这个方法会返回单例类的唯一实例

应用场景:

  • 网站的计数器:多个实例难以同步,保证线程安全。
  • 日志:节省资源,保证一致性,
    • 关于一致性:
      • 如果日志记录器有多个实例,可能会导致日志记录不一致。例如,不同的实例可能会配置不同的日志格式、输出目的地等,导致日志信息分散在不同的地方,难以统一管理和分析。单例模式保证了日志记录器的配置和行为在整个应用程序中是一致的。
  • 多线程的线程池的设计一般也是采用单例模式,因为线程池要方便对池中的线程进行控制。
  • Windows的(任务管理器)和回收站就是很典型的单例模式,他们不能打开俩个。

    单例创建方式

  • 饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。,但如果实例占用资源多且未被使用,会造成资源浪费。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();

    private EagerSingleton() {
    // 私有化构造方法,防止外部实例化
    }

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

  • 懒汉式: 懒汉式在第一次使用时创建实例,适合延迟加载,但需要注意线程安全问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {
    // 私有化构造方法,防止外部实例化
    }

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

  • 静态内部方式: 结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载(静态内部类只有在其被使用时才会被加载),加载类是线程安全的.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {
    // 私有化构造方法,防止外部实例化
    }

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

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

  • 枚举单例:使用枚举实现单例模式 优点:实现简单、调用效率高,枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点是没有延迟加载。
    1
    2
    3
    4
    5
    6
    7
    public enum EnumSingleton {
    INSTANCE;

    public void doSomething() {
    System.out.println("Doing something...");
    }
    }
  • 双重检测锁方式: 在Java 5及以后的版本中,使用volatile关键字的双重检查锁定是安全且推荐的。对于Java 5之前的版本,由于没有volatile关键字的支持,双重检查锁定确实存在问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class DCLSingleton {
    private static volatile DCLSingleton instance;

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

    public static DCLSingleton getInstance() {
    if (instance == null) {
    synchronized (DCLSingleton.class) {
    if (instance == null) {
    instance = new DCLSingleton();
    }
    }
    }
    return instance;
    }
    }

工厂模式

工厂模式(Factory Pattern)是一种创建型设计模式,它提供了一种创建对象的方式,而无需在代码中显式指定具体类。这种模式将实例化对象的过程封装在工厂类中,从而使得代码更具灵活性和可维护性。
好处:

  • 工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。
  • 利用工厂模式可以降低程序的耦合性,为后期的维护修改提供了很大的便利。
  • 将选择实现类、创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。

    简单工厂(Simple Factory)

    简单工厂模式通过一个静态方法,根据传入的参数决定创建哪一种类的实例。
  1. 创建工厂
    1
    public interface Car { public void run(); }
  2. 创建工厂的产品(宝马)
    1
    2
    3
    4
    5
    public class Bmw implements Car {
    public void run() {
    System.out.println("我是宝马汽车...");
    }
    }
  3. 创建工另外一种产品(奥迪)
    1
    2
    3
    4
    5
    public class AoDi implements Car {
    public void run() {
    System.out.println("我是奥迪汽车..");
    }
    }
  4. 创建核心工厂类,由他决定具体调用哪产品
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class CarFactory {

    public static Car createCar(String name) {
    if ("".equals(name)) {
    return null;
    }
    if(name.equals("奥迪")){
    return new AoDi();
    }
    if(name.equals("宝马")){
    return new Bmw();
    }
    return null;
    }
    }
    使用示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Client01 {

    public static void main(String[] args) {
    Car aodi =CarFactory.createCar("奥迪");
    Car bmw =CarFactory.createCar("宝马");
    aodi.run();
    bmw.run();
    }
    }
  • 优点:简单工厂模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。明确区分了各自的职责和权力,有利于整个软件体系结构的优化。
  • 缺点:很明显工厂类集中了所有实例的创建逻辑,容易违反GRASPR的高内聚的责任分配原则

    创建型 - 工厂方法(Factory Method)

  • 工厂方法模式Factory Method,又称多态性工厂模式。在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。该核心类成为一个抽象工厂角色,仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节
    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
    // 产品接口(Product)
    public interface Car {
    public void run();
    }
    // 具体产品(ConcreteProduct)
    public class AoDi implements Car {
    public void run() {
    System.out.println("我是奥迪汽车..");
    }
    }

    public class Bmw implements Car {
    public void run() {
    System.out.println("我是宝马汽车...");
    }
    }
    // 工厂接口(Creator)
    public interface CarFactory {

    Car createCar();

    }
    // 具体工厂(ConcreteCreator)
    public class AoDiFactory implements CarFactory {

    public Car createCar() {

    return new AoDi();
    }
    }

    public class BmwFactory implements CarFactory {

    public Car createCar() {

    return new Bmw();
    }

    }
    使用:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Client {

    public static void main(String[] args) {
    Car aodi = new AoDiFactory().createCar();
    Car jili = new BmwFactory().createCar();
    aodi.run();
    jili.run();
    }
    }
    优点
  1. 分离接口与实现

    • 客户端代码通过抽象接口操作具体对象,而不关心具体实现类,符合依赖倒置原则(DIP)。
  2. 产品族一致性

    • 抽象工厂模式可以确保同一个产品族中的对象一起使用时是兼容的,防止产品之间的不一致性问题。
  3. 符合开闭原则

    • 增加新的产品族时,只需要增加具体的工厂类和产品类,而无需修改现有代码,符合开闭原则(OCP)。
  4. 高内聚

    • 工厂类封装了创建对象的逻辑,单一职责明确,使代码更加模块化和可维护。

    缺点

  5. 复杂性增加

    • 引入大量的类,增加了系统的复杂度和理解难度,特别是在产品族和产品种类较多时。
  6. 难以扩展新的产品等级结构

    • 抽象工厂模式很容易增加新的产品族,但不太容易增加新的产品等级结构(即增加新的产品种类)。如果需要增加新的产品种类,则需要修改抽象工厂接口及其所有子类。

      抽象工厂(Abstract Factory)

抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,它提供一个接口,用于创建一系列相关或互相依赖的对象,而无需指定它们的具体类。抽象工厂模式使得一个类的实例化过程延迟到子类中进行。

抽象工厂模式的核心思想

  1. 抽象工厂:定义创建一组相关对象的接口。
  2. 具体工厂:实现抽象工厂的接口,创建具体的产品对象。
  3. 抽象产品:为一组产品对象定义接口。
  4. 具体产品:实现抽象产品的接口。

抽象工厂模式的结构

  1. AbstractFactory:声明创建抽象产品对象的方法。
  2. ConcreteFactory:实现创建具体产品对象的方法。
  3. AbstractProduct:为产品对象声明接口。
  4. ConcreteProduct:定义一个将被相应的具体工厂创建的产品对象。

示例

假设我们需要创建一组相关的产品,比如按钮和文本框。我们将定义抽象工厂接口 GUIFactory,以及具体工厂 WindowsFactoryMacFactory 来创建不同平台的按钮和文本框。

代码示例

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
// 抽象产品A
interface Button {
void click();
}

// 具体产品A1
class WindowsButton implements Button {
@Override
public void click() {
System.out.println("Windows button clicked");
}
}

// 具体产品A2
class MacButton implements Button {
@Override
public void click() {
System.out.println("Mac button clicked");
}
}

// 抽象产品B
interface TextField {
void type();
}

// 具体产品B1
class WindowsTextField implements TextField {
@Override
public void type() {
System.out.println("Typing in Windows text field");
}
}

// 具体产品B2
class MacTextField implements TextField {
@Override
public void type() {
System.out.println("Typing in Mac text field");
}
}

// 抽象工厂
interface GUIFactory {
Button createButton();
TextField createTextField();
}

// 具体工厂1
class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}

@Override
public TextField createTextField() {
return new WindowsTextField();
}
}

// 具体工厂2
class MacFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacButton();
}

@Override
public TextField createTextField() {
return new MacTextField();
}
}

public class Main {
public static void main(String[] args) {
GUIFactory factory;

// 客户端代码,选择Windows平台
factory = new WindowsFactory();
Button button1 = factory.createButton();
TextField textField1 = factory.createTextField();
button1.click(); // 输出: Windows button clicked
textField1.type(); // 输出: Typing in Windows text field

// 客户端代码,选择Mac平台
factory = new MacFactory();
Button button2 = factory.createButton();
TextField textField2 = factory.createTextField();
button2.click(); // 输出: Mac button clicked
textField2.type(); // 输出: Typing in Mac text field
}
}

分析

  1. 抽象产品接口ButtonTextField 定义了产品的抽象接口。
  2. 具体产品类WindowsButtonMacButtonWindowsTextFieldMacTextField 实现了抽象产品接口。
  3. 抽象工厂接口GUIFactory 定义了创建产品的方法。
  4. 具体工厂类WindowsFactoryMacFactory 实现了 GUIFactory 接口,负责创建具体的产品对象。

优点

  1. 封装对象创建:将一组相关对象的创建逻辑封装在具体工厂中,客户端无需关心对象的创建细节。
  2. 提高可扩展性:可以方便地添加新的具体工厂和产品类,符合开闭原则。
  3. 一致的产品族:确保同一个具体工厂创建的一组对象是相互兼容的。

缺点

  1. 增加复杂性:增加了系统的复杂性,需要定义多个接口和类。
  2. 难以支持新种类的产品:如果需要添加一种新种类的产品,必须修改抽象工厂接口及其所有子类。

适用场景

  1. 系统需要与多个产品族中的多个产品交互
  2. 系统要求产品族的一致性,确保由同一个工厂创建的产品能够一起工作。
  3. 需要独立于产品的创建和具体实现的代码,将对象创建的细节封装在具体工厂中。

抽象工厂模式通过提供一组创建相关对象的接口,简化了客户端代码的使用,提高了系统的可扩展性和一致性。

生成器(Builder)

  • 建造者模式:是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的方式进行创建。

  • 工厂类模式是提供的是创建单个类的产品

  • 而建造者模式则是将各种产品集中起来进行管理,用来具有不同的属性的产品
    使用场景:

  1. 需要生成的对象具有复杂的内部结构。
  2. 需要生成的对象内部属性本身相互依赖。
  • 与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。

  • JAVA 中的 StringBuilder就是建造者模式创建的,他把一个单个字符的char数组组合起来

  • Spring不是建造者模式,它提供的操作应该是对于字符串本身的一些操作,而不是创建或改变一个字符串。

    原型模式(Prototype)

    原型模式(Prototype Pattern)是一种创建型设计模式,它允许通过复制现有对象的实例来创建新的对象,而不是通过类构造器来创建。这样可以减少创建对象的开销,尤其是在创建对象的过程复杂或昂贵时。原型模式通过实现一个原型接口(通常是 Cloneable 接口),来提供一个用于复制现有实例的方法。

    原型模式的核心要点

  1. 原型接口(Prototype Interface):通常定义一个 clone 方法用于复制对象。
  2. 具体原型类(Concrete Prototype Class):实现原型接口并提供实际的复制功能。
  3. 客户端(Client):使用原型实例来创建新的对象。

    使用步骤

  4. 定义一个原型接口,通常包括一个 clone 方法。

  5. 实现具体的原型类,实现 clone 方法。
  6. 使用原型实例创建新的对象。

demo:

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
// 定义原型接口
interface Prototype extends Cloneable {
Prototype clone();
}

// 具体的原型类
class ConcretePrototype implements Prototype {
private String name;

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

@Override
public Prototype clone() {
try {
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}

public String getName() {
return name;
}

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

// 客户端代码
public class PrototypePatternDemo {
public static void main(String[] args) {
// 创建一个原型实例
ConcretePrototype original = new ConcretePrototype("Original");

// 通过复制创建新实例
ConcretePrototype copy = (ConcretePrototype) original.clone();

// 修改复制的实例
copy.setName("Copy");

// 打印原型和复制实例的名称
System.out.println("Original Name: " + original.getName());
System.out.println("Copy Name: " + copy.getName());
}
}

[! 浅拷贝和深拷贝]

浅拷贝

浅拷贝会复制对象本身以及对象中包含的基本类型字段,但不会递归复制引用类型字段。引用类型字段的副本仍然指向原始对象中相同的内存地址。这意味着对这些引用类型字段的修改会影响原始对象和副本。

深拷贝

深拷贝不仅复制对象本身,还递归复制对象中包含的所有引用类型字段。这样,新副本就完全独立于原始对象,对副本的修改不会影响原始对象,反之亦然。

结构型设计模式7

外观模式(Facade pattern)

适配器模式(Adapter pattern)

适配器模式(Adapter Pattern)是一种结构型设计模式,它允许接口不兼容的对象之间进行协作。适配器模式通过将一个类的接口转换成客户端所期望的另一个接口,使得原本由于接口不兼容而无法在一起工作的类可以在一起工作。

适配器模式的核心要点

  1. 目标接口(Target Interface):客户端期望的接口。
  2. 需要适配的类(Adaptee):拥有不兼容接口的类。
  3. 适配器(Adapter):将 Adaptee 的接口转换为目标接口的类。
  4. 客户端(Client):使用目标接口与适配器交互。

    使用步骤

  5. 定义一个原型接口,通常包括一个 clone 方法。

  6. 实现具体的原型类,实现 clone 方法。
  7. 使用原型实例创建新的对象。
    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
    // 目标接口
    interface Target {
    void request();
    }

    // 需要适配的类
    class Adaptee {
    public void specificRequest() {
    System.out.println("Called specificRequest()");
    }
    }

    // 适配器类
    class Adapter implements Target {
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
    this.adaptee = adaptee;
    }

    @Override
    public void request() {
    adaptee.specificRequest();
    }
    }

    // 客户端代码
    public class AdapterPatternDemo {
    public static void main(String[] args) {
    Adaptee adaptee = new Adaptee();
    Target target = new Adapter(adaptee);

    // 客户端使用目标接口
    target.request();
    }
    }


桥接模式(Bridge pattern)

组合模式(composite pattern)

装饰者模式(decorator pattern)

享元模式(Flyweight Pattern)

代理模式(Proxy pattern)

代理模式(Proxy Pattern)是一种结构型设计模式,它提供了一个替代者或占位符,用来控制对某个对象的访问。代理模式可以在不修改原始对象的情况下,提供额外的功能,例如访问控制、延迟初始化、日志记录等。

代理模式的核心要点

  1. 主题接口(Subject Interface):定义了代理类和真实对象的共同行为。
  2. 真实对象(Real Subject):实现了主题接口,代表了代理所代理的实际对象。
  3. 代理对象(Proxy):也实现了主题接口,控制对真实对象的访问,并可以在此基础上增加额外的功能。

使用步骤

  1. 定义主题接口,声明业务方法。
  2. 实现真实对象类,完成实际的业务逻辑。
  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
    32
    33
    34
    35
    // 主题接口
    interface Subject {
    void request();
    }

    // 真实对象类
    class RealSubject implements Subject {
    @Override
    public void request() {
    System.out.println("RealSubject: Handling request.");
    }
    }

    // 代理对象类
    class Proxy implements Subject {
    private RealSubject realSubject;

    @Override
    public void request() {
    if (realSubject == null) {
    realSubject = new RealSubject();
    }
    System.out.println("Proxy: Logging request.");
    realSubject.request();
    }
    }

    // 客户端代码
    public class ProxyPatternDemo {
    public static void main(String[] args) {
    Subject proxy = new Proxy();
    proxy.request();
    }
    }

    行为型设计模式11

    责任链模式(Chain of responsibility pattern)

    策略模式(strategy pattern)

    模板方法模式(Template pattern)

    命令模式(Command pattern)

    观察者模式(observer pattern)

    访问者模式(visitor pattern)

    状态模式(State pattern)

    解释器模式(Interpreter pattern)

    迭代器模式(iterator pattern)

    中介者模式(Mediator pattern)

    备忘录模式(Memento pattern)