设计模式

86 阅读28分钟

设计模式

1 概述

1.1 设计模式是什么?

  • 设计模式是一种为常见的面向对象设计问题提供解决方案的通用模板,这些模式总结了开发人员在软件设计过程中积累的最佳实践和经验;设计模式并不是具体的代码,而是描述了一种特定的设计问题及其解决方案的结构。

1.2 设计模式主要用于解决什么方面的问题?

  • 1)如何组织代码结构:提供类和对象的组织方式,使系统更加模块化和灵活;
  • 2)如何处理对象之间的关系:描述对象之间的交互和依赖关系,帮助减少耦合度,提高系统的可扩展性;
  • 3)如何创建对象:提供创建对象的模式,避免直接使用类的构造函数,增强代码的可维护性和扩展性;
  • 4)如何分配职责:确定不同类或对象的职责,以实现高内聚、低耦合的设计。

1.3 设计模式的目的是什么?

  • 1)提高代码的可重用性:设计模式提供了通用的解决方案,使开发人员能够在不同项目中重复使用这些模式。这不仅节省了开发时间,还提高了代码的一致性和质量。
  • 2)增强代码的灵活性和扩展性:设计模式鼓励松耦合的设计,使系统的各个部分能够独立变化。这使得添加新功能或修改现有功能时,对其他部分的影响最小。
  • 3)提高代码的可维护性:通过应用设计模式,代码变得更加结构化和模块化,使得代码的理解、调试和扩展更加容易。这有助于降低维护成本,并使得团队可以更容易地适应新需求。
  • 4)促进设计最佳实践的传播:设计模式基于经过验证的经验和最佳实践。它们帮助开发人员避免常见的设计错误和陷阱,促进了高质量代码的编写。
  • 5)提供通用的设计语言:设计模式为开发人员提供了一个通用的术语和框架,方便团队成员之间的沟通和协作。使用设计模式的名称和概念,可以快速传达设计意图和解决方案。
  • 6)简化复杂问题的解决:设计模式提供了处理复杂设计问题的成熟方案,帮助开发人员避免从头开始设计系统或功能。
  • 通过上述方式最终让程序具有更好的代码重用性可读性(编程规范性,便于后期维护和理解)、可扩展性(当需要增加新需求时,非常方便)、可靠性(增加新功能后,对原功能么有影响)、使程序呈现高内聚,低耦合的特性。

2 设计模式的七大原则

2.1 单一职责原则(Single Responsibility Principle, SRP)

  • 定义
  • 单一职责原则(SRP)又称为单一功能原则,主张一个类应当只负责一项特定的职责或功能。通过这样设计,可以显著提高代码的可维护性、可读性和复用性。
  • 规范
  • 1)降低类的复杂度:每个类只应专注于一个功能,使得类更加简洁。
  • 2)提高类的可读性和可维护性:通过将不同的职责分离,使得代码更容易理解和修改。
  • 3)减少变更引发的风险:当职责明确时,对一项功能的更改不会影响到其他功能,降低了引入新错误的风险。
  • 4)遵循原则:通常情况下,应严格遵守单一职责原则。然而,当逻辑足够简单时,可以在代码级别略微违反此原则;在类的方法数量较少时,可以在方法上保持单一职责原则。
  • 示例场景:用户发送邮件系统
  • 假设我们有一个应用程序需要处理用户信息并发送电子邮件通知。为了遵循单一职责原则,我们可以将这些功能分解到不同的类中:一个类负责用户信息管理,另一个类负责电子邮件发送。
  • 不遵循单一职责原则的设计
// 用户类,即负责用户数据的管理,又负责发送邮件
class User {
  private String name;
  private String email;

  public User(String name, String email) {
    this.name = name;
    this.email = email;
  }

  public String getName() {
    return name;
  }

  public String getEmail() {
    return email;
  }

  public void sendEmail(String message) {
    // 发送邮件的逻辑
    System.out.println("Sending email to " + email + " with message: " + message);
  }
}

// 使用示例
public class SRPViolationDemo {
  public static void main(String[] args) {
    User user = new User("John Doe", "john@example.com");

    user.sendEmail("Welcome to our service!");
  }
}
  • 在这个设计中,User 类不仅负责存储用户信息,还负责发送电子邮件。这违反了单一职责原则,因为它有两个原因会导致类发生变化:用户信息和电子邮件发送的变化。
  • 遵循单一职责原则的设计:
  • 我们可以将不同的职责分开,创建两个类:一个类负责用户信息管理,另一个类负责电子邮件发送。
// 用户类,只负责用户数据的管理
class User {
  private String name;
  private String email;

  public User(String name, String email) {
    this.name = name;
    this.email = email;
  }

  public String getName() {
    return name;
  }

  public String getEmail() {
    return email;
  }
}

// 邮件服务类,只负责发送邮件
class EmailService {
  public void sendEmail(String email, String message) {
    // 发送邮件的逻辑
    System.out.println("Sending email to " + email + " with message: " + message);
  }
}

// 使用示例
public class SRPDemo {
  public static void main(String[] args) {
    User user = new User("John Doe", "john@example.com");
    EmailService emailService = new EmailService();

    emailService.sendEmail(user.getEmail(), "Welcome to our service!");
  }
}
  • 代码解析:
  • 1)User 类:只负责用户信息的管理。它包含了用户的基本信息如姓名和电子邮件。
  • 2)EmailService 类:负责处理电子邮件的发送。这类包含了发送邮件的逻辑,并且与用户数据的管理无关。
  • 这种设计遵循了单一职责原则,使得每个类只有一个变化的原因。User 类如果有变化,通常是因为用户数据的变化。而 EmailService 类的变化则是因为邮件发送逻辑的变化。
  • 通过这种分离,我们可以更容易地维护和扩展代码。例如,如果需要更改邮件发送的方式,只需修改 EmailService 类,而不影响用户数据的管理逻辑。

2.2 接口隔离原则(Interface Segregation Principle, ISP)

  • 定义
  • 接口隔离原则(ISP)强调,一个类不应依赖它不使用的接口。换句话说,我们应该设计小而专一的接口,而不是设计一个大而全的接口供所有客户端使用。这可以避免不必要的依赖和不需要的方法暴露。
  • 规范
  • 1)满足单一职责原则:在使用接口隔离原则之前,首先要确保每个接口符合单一职责原则。
  • 2)接口高内聚:接口应该是高内聚的,尽量减少对外暴露 public 方法,只提供必要的操作。
  • 3)定制服务:为不同的类提供专属的接口,以满足它们的特定需求。这通常意味着将一个大接口拆分为多个小接口。
  • 4)合理设计接口粒度:接口设计的粒度应适中。过大的接口会导致类实现过多不必要的方法,而过小的接口则会导致类之间的交互过于复杂。
  • 示例场景:文档处理系统
  • 假设我们设计一个文档处理系统,该系统支持多种文档类型的操作,如读取、保存和打印。不同类型的文档具有不同的功能需求,例如:文本文件(TextFile)可以读取、保存和打印,而图像文件(ImageFile)只能读取和打印。
  • 不遵循接口隔离原则的设计
  • 在不遵循接口隔离原则的情况下,我们可能会创建一个包含所有操作的大接口。
// 文档读取、保存、打印操作接口
interface Document {
  void read();

  void save();

  void print();
}

// 文本文件类
class TextFile implements Document {
  @Override
  public void read() {
    System.out.println("Reading text file...");
  }

  @Override
  public void save() {
    System.out.println("Saving text file...");
  }

  @Override
  public void print() {
    System.out.println("Printing text file...");
  }
}

// 图像文件类
class ImageFile implements Document {
  @Override
  public void read() {
    System.out.println("Reading image file...");
  }

  @Override
  public void save() {
    throw new UnsupportedOperationException("Save operation not supported for image files.");
  }

  @Override
  public void print() {
    System.out.println("Printing image file...");
  }
}

// 文档处理器类
class DocumentProcessor {
  public void processRead(Document document) {
    document.read();
  }

  public void processSave(Document document) {
    document.save();
  }

  public void processPrint(Document document) {
    document.print();
  }
}

// 使用示例
public class ISPViolationDemo {
  public static void main(String[] args) {
    TextFile textFile = new TextFile();
    ImageFile imageFile = new ImageFile();

    DocumentProcessor processor = new DocumentProcessor();

    // 处理文本文件
    processor.processRead(textFile);
    processor.processSave(textFile);
    processor.processPrint(textFile);

    // 处理图像文件
    processor.processRead(imageFile);
    processor.processPrint(imageFile);
    // 注意:processor.processSave(imageFile); 这种调用是非法的,
    // 因为 ImageFile 类不支持保存操作,所以会抛出异常。
  }
}
  • 在这种设计中,Document 接口强制所有实现类提供 read()save()print() 方法,即使某些方法对某些文档类型并不适用。例如:ImageFile 实现 save() 方法会抛出 UnsupportedOperationException,因为该操作对图像文件不适用。
  • 遵循接口隔离原则的设计
  • 为了遵循接口隔离原则,我们可以将接口拆分为多个更专一的接口,每个接口只包含与其相关的操作。
// 读取操作接口
interface Readable {
  void read();
}

// 保存操作接口
interface Saveable {
  void save();
}

// 打印操作接口
interface Printable {
  void print();
}

// 文本文件类
class TextFile implements Readable, Saveable, Printable {
  @Override
  public void read() {
    System.out.println("Reading text file...");
  }

  @Override
  public void save() {
    System.out.println("Saving text file...");
  }

  @Override
  public void print() {
    System.out.println("Printing text file...");
  }
}

// 图像文件类
class ImageFile implements Readable, Printable {
  @Override
  public void read() {
    System.out.println("Reading image file...");
  }

  @Override
  public void print() {
    System.out.println("Printing image file...");
  }
}

// 文档处理器类
class DocumentProcessor {
  public void processRead(Readable readable) {
    readable.read();
  }

  public void processSave(Saveable saveable) {
    saveable.save();
  }

  public void processPrint(Printable printable) {
    printable.print();
  }
}

// 使用示例
public class ISPDemo {
  public static void main(String[] args) {
    TextFile textFile = new TextFile();
    ImageFile imageFile = new ImageFile();

    DocumentProcessor processor = new DocumentProcessor();

    // 处理文本文件
    processor.processRead(textFile);
    processor.processSave(textFile);
    processor.processPrint(textFile);

    // 处理图像文件
    processor.processRead(imageFile);
    processor.processPrint(imageFile);
    // 注意:processor.processSave(imageFile); 这种调用是非法的,
    // 因为 ImageFile 不实现 Saveable 接口
  }
}
  • 代码解析
  • 1)Readable 接口:定义 read() 方法,用于读取文档内容。
  • 2)Saveable 接口:定义 save() 方法,用于保存文档内容。
  • 3)Printable 接口:定义 print() 方法,用于打印文档内容。
  • 优点
  • 1)灵活性:客户端可以根据需要选择依赖特定的接口。例如,处理器可以选择只关心 ReadablePrintable 接口,而忽略 Saveable 接口。
  • 2)减少冗余:避免了实现类强制实现不必要的方法。比如 ImageFile 类不再需要实现 save() 方法,这使得代码更加简洁和清晰。
  • 3)解耦:减少了类之间的耦合,提高了系统的可维护性和可扩展性。
  • 通过这种设计,系统可以根据具体需求灵活组合功能接口,从而提高了系统的灵活性和可维护性。这就是接口隔离原则的核心:通过定义小而专一的接口,减少不必要的依赖和冗余,实现更好的解耦。

2.3 依赖倒置原则(Dependency Inversion Principle, DIP)

  • 定义
  • 依赖倒置原则(DIP)建议高层模块不应该依赖于低层模块,两者都应该依赖于抽象。换句话说,抽象(接口或抽象类)不应该依赖于细节(实现类),而细节应该依赖于抽象。这一原则旨在通过减少模块之间的耦合来提高系统的灵活性和可维护性。
  • 规范
  • 1)高层模块不依赖于低层模块:两者都应依赖于抽象,使得高层模块与具体的实现细节解耦。
  • 2)抽象不依赖于细节:实现类依赖于接口或抽象类,而不是相反。
  • 3)面向接口编程:通过定义接口来规范模块之间的交互,而不关心具体实现,从而实现模块的可替换性。
  • 4)稳定的架构:相对于具体实现,抽象(接口和抽象类)更稳定。因此,以抽象为基础搭建的架构更加稳健和易于扩展。
  • 5)使用接口或抽象类:目的是定义规范,具体的操作由实现类完成。
  • 示例场景:消息通知系统
  • 假设我们有一个消息通知系统,它可以通过多种方式发送消息,如电子邮件和短信。我们希望这个系统能够灵活地扩展和修改,而不影响高层模块的代码。
  • 不遵循依赖倒置原则的设计
  • 在不遵循 DIP 的设计中,高层模块(如 NotificationService)直接依赖于低层模块(如 EmailSenderSmsSender)。这会导致代码难以维护和扩展。
// 具体的电子邮件发送
class EmailSender {
  public void sendEmail(String message) {
    System.out.println("Sending email: " + message);
  }
}

// 具体的短信发送
class SmsSender {
  public void sendSms(String message) {
    System.out.println("Sending SMS: " + message);
  }
}

// 高层模块
class NotificationService {
  private EmailSender emailSender;
  private SmsSender smsSender;

  public NotificationService() {
    emailSender = new EmailSender();
    smsSender = new SmsSender();
  }

  // 通过电子邮件发送通知
  public void sendEmailNotification(String message) {
    emailSender.sendEmail(message);
  }

  // 通过短信发送通知
  public void sendSmsNotification(String message) {
    smsSender.sendSms(message);
  }
}

// 使用示例
public class DIPViolationDemo {
  public static void main(String[] args) {
    // 使用电子邮件发送通知
    NotificationService emailNotification = new NotificationService();
    emailNotification.sendEmailNotification("This is an email notification.");

    // 使用短信发送通知
    NotificationService smsNotification = new NotificationService();
    smsNotification.sendSmsNotification("This is an SMS notification.");
  }
}
  • 在上述设计中,NotificationService 直接依赖 EmailSenderSmsSender 的具体实现。如果我们想添加新的通知方式(比如推送通知),或者更改现有的发送逻辑,就需要修改 NotificationService 类。
  • 遵循依赖倒置原则的设计
  • 为了遵循依赖倒置原则,我们可以引入抽象层。通过定义一个抽象的消息发送接口(MessageSender),让高层模块依赖这个抽象,而不是具体实现。
// 抽象的消息发送接口
interface MessageSender {
  void sendMessage(String message);
}

// 具体的电子邮件发送实现
class EmailSender implements MessageSender {
  @Override
  public void sendMessage(String message) {
    System.out.println("Sending email: " + message);
  }
}

// 具体的短信发送实现
class SmsSender implements MessageSender {
  @Override
  public void sendMessage(String message) {
    System.out.println("Sending SMS: " + message);
  }
}

// 高层模块
class NotificationService {
  private MessageSender messageSender;

  // 通过构造函数注入依赖
  public NotificationService(MessageSender messageSender) {
    this.messageSender = messageSender;
  }

  public void sendNotification(String message) {
    messageSender.sendMessage(message);
  }
}

// 使用示例
public class DIPDemo {
  public static void main(String[] args) {
    // 使用电子邮件发送通知
    MessageSender emailSender = new EmailSender();
    NotificationService emailNotification = new NotificationService(emailSender);
    emailNotification.sendNotification("This is an email notification.");

    // 使用短信发送通知
    MessageSender smsSender = new SmsSender();
    NotificationService smsNotification = new NotificationService(smsSender);
    smsNotification.sendNotification("This is an SMS notification.");
  }
}
  • 代码解析
  • 1)接口 MessageSender:这是一个抽象层,定义了 sendMessage(String message) 方法,所有消息发送实现都必须实现这个方法。
  • 2)具体实现类EmailSender 实现了 MessageSender 接口,负责发送电子邮件。SmsSender 实现了 MessageSender 接口,负责发送短信。
  • 3)高层模块 NotificationService:依赖 MessageSender 接口,而不是具体的实现类。通过构造函数注入 MessageSender 的实现,这种方式被称为依赖注入(Dependency Injection),实现了对抽象的依赖。
  • 在这个例子中,MessageSender 接口定义了发送消息的方法,而 EmailSenderSmsSender 分别实现了具体的电子邮件和短信发送功能。高层模块 NotificationService 依赖于 MessageSender 接口,而不是具体的实现类。这种设计允许我们轻松替换或扩展消息发送方式,而无需修改高层模块的代码。
  • 优点
  • 1)可扩展性:添加新的消息发送方式(如推送通知)时,只需实现 MessageSender 接口,不需要修改 NotificationService
  • 2)可维护性:高层模块与低层模块解耦,修改低层模块实现不会影响高层模块。
  • 3)灵活性:可以轻松更改消息发送方式,甚至在运行时也可以更改(如果通过工厂模式或依赖注入框架实现)。
  • 通过遵循依赖倒置原则,我们能够构建更加灵活、可扩展的系统架构,避免高层模块直接依赖具体实现类的缺陷。

2.4 里氏替换原则(Liskov Substitution Principle, LSP)

  • 定义
  • 里氏替换原则(LSP)要求,在程序中使用基类的地方,必须能够透明地使用其子类的对象。换句话说,子类应该能够替换基类并且程序的行为不会改变。子类可以扩展父类的功能,但不能改变父类原有的功能和行为。
  • 规范
  • 1)保持行为一致性:子类的方法不应该改变父类方法的预期行为或输出。
  • 2)遵循合同:子类不能违背父类所设定的合同(例如,方法签名、输入参数的合法性、输出结果的合理性等)。
  • 3)支持多态性:子类的对象应能够替代基类的对象,且程序逻辑保持正确。
  • 示例场景:形状计算
  • 假设我们有一个简单的几何图形系统,需要计算矩形和正方形的面积。我们使用一个基类 Rectangle 来表示矩形,并且有一个子类 Square 来表示正方形。按理说,正方形是一种特殊的矩形,因此可以继承矩形类。
  • 不遵循里氏替换原则的设计
  • 在不遵循 LSP 的设计中,Square 子类重写了 Rectangle 类的某些方法,导致子类行为与父类的预期行为不一致。
class Rectangle {
  protected int width;
  protected int height;

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

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

  public int getWidth() {
    return width;
  }

  public int getHeight() {
    return height;
  }

  public int calculateArea() {
    return width * height;
  }
}

class Square extends Rectangle {
  @Override
  public void setWidth(int width) {
    this.width = width;
    this.height = width; // 保证正方形的特性:边长相等
  }

  @Override
  public void setHeight(int height) {
    this.width = height;
    this.height = height; // 保证正方形的特性:边长相等
  }
}

// 客户端代码
public class LSPViolationDemo {
  public static void main(String[] args) {
    Rectangle rect = new Rectangle();
    rect.setWidth(5);
    rect.setHeight(10);
    System.out.println("Rectangle area: " + rect.calculateArea());

    Rectangle square = new Square();
    square.setWidth(5);
    System.out.println("Square area with setWidth: " + square.calculateArea());

    square.setHeight(6);
    System.out.println("Square area with setHeight: " + square.calculateArea());
  }
}
  • 在上述代码中,Square 类重写了 setWidth()setHeight() 方法,以保证正方形的特性:边长相等。然而,这违反了里氏替换原则,因为 Square 子类的行为与 Rectangle 父类的行为不一致。例如,当我们设置宽度和高度时,预期的矩形面积计算可能不再适用,这导致了潜在的错误。
  • 遵循里氏替换原则的设计
  • 为了解决这个问题,我们可以考虑重新设计我们的类结构,避免让 Square 继承 Rectangle。一种方法是通过组合来代替继承。
// 抽象的形状类
abstract class Shape {
  public abstract int calculateArea();
}

// 矩形类
class Rectangle extends Shape {
  protected int width;
  protected int height;

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

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

  public int getWidth() {
    return width;
  }

  public int getHeight() {
    return height;
  }

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

// 正方形类
class Square extends Shape {
  private int side;

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

  public int getSide() {
    return side;
  }

  @Override
  public int calculateArea() {
    return side * side;
  }
}

// 客户端代码
public class LSPDemo {
  public static void main(String[] args) {
    Rectangle rect = new Rectangle();
    rect.setWidth(5);
    rect.setHeight(10);
    System.out.println("Rectangle area: " + rect.calculateArea());

    Square square = new Square();
    square.setSide(5);
    System.out.println("Square area: " + square.calculateArea());
  }
}
  • 代码解析
  • 1)Shape 类:抽象基类,定义了 calculateArea() 方法,所有形状类都必须实现这个方法。
  • 2)Rectangle 类:继承自 Shape,表示一个矩形。具有 setWidth()setHeight() 方法,以及计算面积的实现。
  • 3)Square 类:继承自 Shape,表示一个正方形。具有 setSide() 方法,以及计算面积的实现。
  • 优点:
  • 1)遵循 LSP:子类(Square)没有改变父类(Shape)的预期行为,任何时候都可以替代父类的对象。
  • 2)清晰的结构:正方形和矩形被视为两种不同的形状,消除了因继承关系不当引起的混淆。
  • 3)灵活性:在不影响其他形状类的情况下,可以自由扩展新的形状类型。
  • 通过这种设计,我们确保了 Square 类完全遵循 Shape 的契约,没有破坏 Rectangle 的行为,这符合里氏替换原则。

2.5 开闭原则(Open/Closed Principle, OCP)

  • 定义
  • 开闭原则强调软件实体(如类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在设计时,应该使软件模块能够通过扩展来添加新功能,而不需要修改现有的代码。
  • 规范
  • 1)基础原则:开闭原则是编程中最基础、最重要的设计原则之一。
  • 2)对扩展开放:软件实体应允许通过新增代码来扩展其功能,而无需修改现有的功能。
  • 3)对修改关闭:不应对已有代码进行修改,以避免引入新错误。
  • 4)面向抽象:使用抽象类和接口来定义可扩展的框架,通过具体实现类来扩展功能。
  • 5)遵循其他原则和设计模式:遵循其他设计原则和使用设计模式的目的之一是实现开闭原则。
  • 示例场景:报表生成系统
  • 假设我们有一个报表生成系统,它可以生成不同格式的报表,如 PDF 和 Excel。我们希望能够轻松添加新的报表格式,而不需要修改现有的报表生成代码。
  • 不遵循开闭原则的设计
// 报表生成服务
class ReportService {
  public void generateReport(String type, String data) {
    if (type.equals("PDF")) {
      generatePdfReport(data);
    } else if (type.equals("Excel")) {
      generateExcelReport(data);
    } else {
      System.out.println("Unsupported report type: " + type);
    }
  }

  private void generatePdfReport(String data) {
    System.out.println("Generating PDF report with data: " + data);
  }

  private void generateExcelReport(String data) {
    System.out.println("Generating Excel report with data: " + data);
  }
}

// 使用示例
public class OCPViolationDemo {
  public static void main(String[] args) {
    ReportService reportService = new ReportService();
    reportService.generateReport("PDF", "PDF Report Data");
    reportService.generateReport("Excel", "Excel Report Data");
  }
}
  • 问题
  • 1)不遵循OCP:如果要添加新的报表格式(如 HTML),必须修改 ReportService 类中的 generateReport 方法,这违反了 OCP。
  • 2)不易维护:随着报表格式的增加,generateReport 方法会变得越来越复杂,难以维护。
  • 3)低内聚:报表生成的逻辑集中在一个类中,不同格式的报表生成方法相互干扰。
  • 遵循开闭原则的设计
  • 为了遵循 OCP,我们可以使用一个抽象的报表生成器接口,每种报表格式都有其具体的实现类。这样,添加新的报表格式不需要修改现有的代码,只需扩展新的实现类。
// 报表生成器接口
interface ReportGenerator {
  void generateReport(String data);
}

// 生成 PDF 报表的具体实现
class PdfReportGenerator implements ReportGenerator {
  @Override
  public void generateReport(String data) {
    System.out.println("Generating PDF report with data: " + data);
  }
}

// 生成 Excel 报表的具体实现
class ExcelReportGenerator implements ReportGenerator {
  @Override
  public void generateReport(String data) {
    System.out.println("Generating Excel report with data: " + data);
  }
}

// 报表生成服务
class ReportService {
  private ReportGenerator reportGenerator;

  // 通过构造函数注入具体的报表生成器
  public ReportService(ReportGenerator reportGenerator) {
    this.reportGenerator = reportGenerator;
  }

  public void createReport(String data) {
    reportGenerator.generateReport(data);
  }
}

// 使用示例
public class OCPDemo {
  public static void main(String[] args) {
    String reportData = "Report Data";

    // 使用 PDF 报表生成器
    ReportGenerator pdfGenerator = new PdfReportGenerator();
    ReportService pdfService = new ReportService(pdfGenerator);
    pdfService.createReport(reportData);

    // 使用 Excel 报表生成器
    ReportGenerator excelGenerator = new ExcelReportGenerator();
    ReportService excelService = new ReportService(excelGenerator);
    excelService.createReport(reportData);
  }
}
  • 代码解析
  • 1)ReportGenerator 接口:定义了生成报表的行为,所有具体的报表生成器类都必须实现这个接口。
  • 2)PdfReportGenerator 类:实现了 ReportGenerator 接口,提供生成 PDF 报表的具体实现。
  • 3)ExcelReportGenerator 类:实现了 ReportGenerator 接口,提供生成 Excel 报表的具体实现。
  • 4)ReportService 类:高层模块,依赖于 ReportGenerator 接口,而不是具体的实现类。它通过构造函数注入具体的报表生成器,实现报表的生成。
  • 扩展性
  • 如果我们需要添加一个新的报表格式,比如 HTML 报表,只需创建一个新的类 HtmlReportGenerator 实现 ReportGenerator 接口,而不需要修改现有的代码。
// 生成 HTML 报表的具体实现
class HtmlReportGenerator implements ReportGenerator {
  @Override
  public void generateReport(String data) {
    System.out.println("Generating HTML report with data: " + data);
  }
}

// 使用示例
public class OCPDemo {
  public static void main(String[] args) {
    String reportData = "Report Data";

    // 使用 HTML 报表生成器
    ReportGenerator htmlGenerator = new HtmlReportGenerator();
    ReportService htmlService = new ReportService(htmlGenerator);
    htmlService.createReport(reportData);
  }
}
  • 优点
  • 1)对扩展开放:可以通过添加新的 ReportGenerator 实现类来支持新的报表格式。
  • 2)对修改封闭:不需要修改 ReportService 类或其他已有的代码,减少了引入新错误的风险。
  • 3)高内聚低耦合:通过接口和抽象类分离高层模块和底层模块,增强了系统的可维护性和可扩展性。
  • 通过遵循开闭原则,我们能够创建灵活、可扩展的软件系统,从而减少维护成本并提高开发效率。

2.6 迪米特法则(Law of Demeter, LoD)

  • 定义
  • 迪米特法则,又称为最少知识原则,要求一个对象应该对其他对象有最少的了解。换句话说,一个对象不应直接与其他对象的内部结构交互,而应通过适当的接口进行通信。简而言之,只与直接的朋友通信,不与“陌生人”说话。
  • 规范
  • 1)对象之间的通信应尽可能少:一个对象应尽量少地了解其他对象的细节。
  • 2)使用中介方法:通过引入中介方法,避免对象之间直接交互,从而降低耦合度。
  • 3)封装内部实现:对象应通过公共接口与外界通信,而不暴露内部实现细节。
  • 示例场景:顾客与订单管理系统
  • 假设我们有一个顾客与订单管理系统,顾客可以下订单,订单中包含多个商品。订单可以计算总价,而顾客可以查看自己的订单信息。
  • 不遵循迪米特法则的设计
  • 在这种设计中,Customer 类直接与 OrderProduct 类的内部结构交互。
// 商品类
class Product {
  private String name;
  private double price;

  public Product(String name, double price) {
    this.name = name;
    this.price = price;
  }

  public double getPrice() {
    return price;
  }
}

// 订单类
class Order {
  private List<Product> products;

  public Order() {
    this.products = new ArrayList<>();
  }

  public void addProduct(Product product) {
    products.add(product);
  }

  public List<Product> getProducts() {
    return products;
  }
}

// 客户类
class Customer {
  private String name;
  private Order order;

  public Customer(String name) {
    this.name = name;
    this.order = new Order();
  }

  public void addProductToOrder(Product product) {
    order.addProduct(product);
  }

  public double calculateOrderTotal() {
    double total = 0;
    for (Product product : order.getProducts()) {
      total += product.getPrice();
    }
    return total;
  }
}

// 使用示例
public class LoDViolationDemo {
  public static void main(String[] args) {
    Customer customer = new Customer("John Doe");
    customer.addProductToOrder(new Product("Laptop", 1200));
    customer.addProductToOrder(new Product("Mouse", 25));
    System.out.println("Order Total: $" + customer.calculateOrderTotal());
  }
}
  • 问题
  • 1)高耦合Customer 类直接依赖 OrderProduct 类的内部结构。这违反了迪米特法则,使得系统难以维护和扩展。
  • 2)不必要的了解Customer 类知道 Order 类的内部实现(产品列表)和 Product 类的价格细节。
  • 遵循迪米特法则的设计
  • 为了遵循迪米特法则,我们可以引入一些中介方法,让 Customer 类通过 Order 类的方法来操作订单和获取信息,而不直接访问 OrderProduct 类的内部结构。
// 商品类
class Product {
  private String name;
  private double price;

  public Product(String name, double price) {
    this.name = name;
    this.price = price;
  }

  public double getPrice() {
    return price;
  }
}

// 订单类
class Order {
  private List<Product> products;

  public Order() {
    this.products = new ArrayList<Product>();
  }

  public void addProduct(Product product) {
    products.add(product);
  }

  public double calculateTotal() {
    double total = 0;
    for (Product product : products) {
      total += product.getPrice();
    }
    return total;
  }
}

// 客户类
class Customer {
  private String name;
  private Order order;

  public Customer(String name) {
    this.name = name;
    this.order = new Order();
  }

  public void addProductToOrder(Product product) {
    order.addProduct(product);
  }

  public double calculateOrderTotal() {
    return order.calculateTotal();
  }
}

// 使用示例
public class LoDDemo {
  public static void main(String[] args) {
    Customer customer = new Customer("John Doe");
    customer.addProductToOrder(new Product("Laptop", 1200));
    customer.addProductToOrder(new Product("Mouse", 25));
    System.out.println("Order Total: $" + customer.calculateOrderTotal());
  }
}
  • 代码解析
  • 1)Product 类:表示商品,包含商品名称和价格。
  • 2)Order 类:表示订单,包含商品列表。新增了 calculateTotal() 方法,计算订单的总价格。客户不再直接访问商品列表,而是通过订单来获取总价。
  • 3)Customer 类:表示客户。客户不再直接处理订单中的商品列表,而是通过 Order 类的方法进行操作。客户通过调用 order.calculateTotal() 来获取订单总价,而不是直接遍历产品列表。
  • 优点
  • 1)松耦合Customer 类不再依赖 OrderProduct 的内部实现。订单的计算逻辑被封装在 Order 类中,客户只需要调用接口方法。
  • 2)易于维护:由于客户类与订单和产品类之间的耦合减少,系统更容易维护和扩展。例如,如果订单的计算逻辑发生变化,只需要修改 Order 类,而不影响 Customer 类。
  • 3)减少知识暴露Customer 类只需要知道 Order 类的公共接口,而不需要知道订单的内部实现细节。
  • 通过这样的设计,我们可以使系统更加模块化,减少类之间的依赖性,从而提高系统的灵活性和可维护性。这就是迪米特法则的核心价值所在。

2.7 合成/聚合复用原则(Composite/Aggregate Reuse Principle, CARP)

  • 定义
  • 合成/聚合复用原则提倡尽量使用对象的组合/聚合来实现代码复用,而不是通过继承来建立一个脆弱的基类。组合和聚合提供了一种更灵活的方式来构建系统,使得各个组件更易于复用和维护。
  • 规范
  • 1)优先使用组合:在设计时,优先选择组合或聚合,而不是继承。通过组合,可以将行为分解为独立的部分,以便于灵活地组合这些行为。
  • 2)模块化设计:将不同的功能封装在独立的模块中,这些模块通过组合来实现复杂的功能。
  • 3)避免继承滥用:继承只能表示“是一个”的关系,不适合表示“有一个”的关系。在许多情况下,组合比继承更适合描述对象的行为和属性。
  • 示例场景:图形处理系统
  • 假设我们有一个图形处理系统,可以处理多种图形,如圆形和矩形。每种图形都有绘制和计算面积的功能。我们可以使用组合方式而不是继承来实现这些功能。
  • 不遵循合成复用原则的设计
  • 在这种设计中,我们可能会通过继承的方式来复用代码。这种设计虽然在一定程度上实现了代码复用,但会导致类之间的耦合过于紧密,灵活性较差。
// 图形基类
class Shape {
  public void draw() {
    // 默认实现
  }

  public double calculateArea() {
    return 0;
  }
}

// 圆形类
class Circle extends Shape {
  private double radius;

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

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

  @Override
  public double calculateArea() {
    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 void draw() {
    System.out.println("Drawing a rectangle.");
  }

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

// 使用示例
public class CARPViolationDemo {
  public static void main(String[] args) {
    Shape circle = new Circle(5);
    circle.draw();
    System.out.println("Circle area: " + circle.calculateArea());

    Shape rectangle = new Rectangle(4, 6);
    rectangle.draw();
    System.out.println("Rectangle area: " + rectangle.calculateArea());
  }
}
  • 问题
  • 1)类层次耦合:如果需要添加新的功能,如计算周长,必须修改 Shape 基类以及所有子类。
  • 2)继承的限制:继承只能表示“是一个”的关系,不适合表示“有一个”的关系。例如,如果图形具有颜色属性,那么所有图形类都需要继承一个具有颜色属性的基类,这不灵活。
  • 遵循合成复用原则的设计
  • 为了解决上述问题,我们可以使用组合的方式,将各个功能分离成独立的组件,然后通过组合来构建复杂的对象。
// 绘制接口
interface Drawable {
  void draw();
}

// 计算面积接口
interface AreaCalculable {
  double calculateArea();
}

// 具体的绘制实现
class CircleDrawing implements Drawable {
  @Override
  public void draw() {
    System.out.println("Drawing a circle.");
  }
}

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

// 具体的面积计算实现
class CircleAreaCalculator implements AreaCalculable {
  private double radius;

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

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

class RectangleAreaCalculator implements AreaCalculable {
  private double width;
  private double height;

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

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

// 图形类,通过组合实现功能
class Shape {
  private Drawable drawable;
  private AreaCalculable areaCalculable;

  public Shape(Drawable drawable, AreaCalculable areaCalculable) {
    this.drawable = drawable;
    this.areaCalculable = areaCalculable;
  }

  public void draw() {
    drawable.draw();
  }

  public double calculateArea() {
    return areaCalculable.calculateArea();
  }
}

// 使用示例
public class CARPDemo {
  public static void main(String[] args) {
    Drawable circleDrawing = new CircleDrawing();
    AreaCalculable circleArea = new CircleAreaCalculator(5);
    Shape circle = new Shape(circleDrawing, circleArea);
    circle.draw();
    System.out.println("Circle area: " + circle.calculateArea());

    Drawable rectangleDrawing = new RectangleDrawing();
    AreaCalculable rectangleArea = new RectangleAreaCalculator(4, 6);
    Shape rectangle = new Shape(rectangleDrawing, rectangleArea);
    rectangle.draw();
    System.out.println("Rectangle area: " + rectangle.calculateArea());
  }
}
  • 代码解析
  • 1)接口 Drawable 和 AreaCalculable:定义绘制和计算面积的功能。
  • 2)具体实现类:CircleDrawing 和 RectangleDrawing 实现 Drawable 接口,提供具体的绘制实现。CircleAreaCalculator 和 RectangleAreaCalculator 实现 AreaCalculable 接口,提供具体的面积计算实现。
  • 3)Shape 类:通过组合 Drawable 和 AreaCalculable 接口的实现,实现绘制和计算面积的功能。
  • 优点
  • 1)高内聚低耦合:各个功能模块独立,修改或扩展某个功能模块不会影响其他模块。
  • 2)易于扩展:可以轻松添加新的绘制方式或计算方式,只需实现相应的接口。
  • 3)灵活性:通过组合不同的功能模块,可以创建复杂的对象,而不需要依赖继承。
  • 通过这种组合的设计,我们可以更好地遵循合成复用原则,避免不必要的类层次耦合,从而提高系统的灵活性和可维护性。

3 设计模式分类

  • 设计模式通常分为三大类
  • 1)创建型模式:专注于对象的创建过程,旨在提供对象的创建机制,以增加对象的灵活性和复用性。主要的创建型模式包括:单例模式工厂方法模式抽象工厂模式建造者模式原型模式
  • 2)结构型模式:这些模式关注类和对象的组合,帮助确保系统结构更加灵活和高效。主要的结构型模式包括:适配器模式桥接模式组合模式装饰器模式外观模式享元模式代理模式
  • 3)行为型模式:专注于类和对象的组合,确保系统的结构变得更灵活和高效;主要的行为型模式包括::策略模式模板方法模式观察者模式迭代器模式责任链模式命令模式备忘录模式状态模式访问者模式中介者模式解释器模式
  • 常见的 23 种设计模式常见的23重设计模式.png

4 结论

  • 遵循设计模式的七大原则可以帮助开发者创建更加健壮、灵活和可维护的代码。这些原则是设计高质量软件的基础。
  • 设计模式是一套方法论和设计思想,强调高内聚、低耦合的设计。开发者可以在这些原则的指导下自由发挥,甚至设计出自己的一套设计模式。
  • 然而,学习设计模式或在工程中实践设计模式,必须深入特定的业务场景,结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。脱离具体业务逻辑的学习或使用设计模式是没有意义的。

5 参考文献