【23种设计模式】七种设计原则:理论与 Java 实践

文章目录

23 种设计模式之七种设计原则:理论与 Java 实践

一、单一职责原则(SRP - Single Responsibility Principle)

(一)理论介绍

单一职责原则规定一个类应该只有一个引起它变化的原因,即一个类只负责一项职责。如果一个类承担了过多的职责,那么这些职责之间的耦合度会很高,一旦某个职责发生变化,可能会影响到整个类的其他职责相关的功能,导致代码的可维护性和可扩展性变差。

(二)Java 实现示例

假设我们有一个类来处理用户信息的存储和用户密码的加密。按照单一职责原则,我们将其拆分为两个类。

  1. 用户信息存储类
java 复制代码
public class UserInfoStorage {
    private String username;
    private String email;

    public void saveUserInfo(String username, String email) {
        this.username = username;
        this.email = email;
        // 这里可以添加将用户信息存储到数据库或文件的代码
        System.out.println("保存用户信息:用户名 - " + username + ",邮箱 - " + email);
    }

    public String getUsername() {
        return username;
    }

    public String getEmail() {
        return email;
    }
}
  1. 用户密码加密类
java 复制代码
public class UserPasswordEncryption {
    public String encryptPassword(String password) {
        // 简单的加密示例,实际中可能更复杂
        return password + "encrypted";
    }
}

(三)关键步骤

  • 首先确定类的主要职责,如 UserInfoStorage 专注于用户信息的存储操作,UserPasswordEncryption 专注于密码加密。
  • 在需要存储用户信息时,调用 UserInfoStoragesaveUserInfo 方法;在需要加密密码时,调用 UserPasswordEncryptionencryptPassword 方法。

(四)流程图

复制代码
开始
|
|-- 需要存储用户信息
|   |
|   |-- 调用 UserInfoStorage.saveUserInfo 方法
|   |   |
|   |   |-- 执行存储用户信息操作
|
|-- 需要加密密码
|   |
|   |-- 调用 UserPasswordEncryption.encryptPassword 方法
|   |   |
|   |   |-- 执行密码加密操作
结束

二、开闭原则(OCP - Open Closed Principle)

(一)理论介绍

开闭原则指出软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当有新的需求或功能变化时,应该通过扩展现有代码来实现,而不是直接修改已有的代码,这样可以减少对原有系统的影响,降低出错的风险,提高系统的稳定性和可维护性。

(二)Java 实现示例

以图形绘制为例,我们有一个绘制形状的接口,然后不同形状的类实现该接口。

  1. 图形绘制接口
java 复制代码
public interface Shape {
    void draw();
}
  1. 圆形绘制类
java 复制代码
public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}
  1. 矩形绘制类
java 复制代码
public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

如果要添加新的形状,如三角形,我们只需创建一个新的类实现 Shape 接口,而无需修改已有的 Shape 接口和 CircleRectangle 类。

java 复制代码
public class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("绘制三角形");
    }
}

(三)关键步骤

  • 定义抽象的接口或基类,如 Shape 接口。
  • 具体的类实现该接口或继承基类,如 CircleRectangle 等。
  • 当有新需求时,创建新的类实现相同的接口或继承相同的基类进行扩展。

(四)流程图

复制代码
开始
|
|-- 定义 Shape 接口
|   |
|   |-- Circle 类实现 Shape 接口
|   |   |
|   |   |-- 实现 draw 方法
|   |
|   |-- Rectangle 类实现 Shape 接口
|   |   |
|   |   |-- 实现 draw 方法
|   |
|   |-- 当添加新形状(如 Triangle)
|   |   |
|   |   |-- Triangle 类实现 Shape 接口
|   |   |   |
|   |   |   |-- 实现 draw 方法
结束

三、里氏替换原则(LSP - Liskov Substitution Principle)

(一)理论介绍

里氏替换原则表明子类型必须能够替换它们的父类型。这意味着在程序中任何使用父类型的地方,都应该能够透明地使用子类型对象,而不会导致程序的错误或异常行为。子类型应该在行为上与父类型保持一致,或者是父类型行为的扩展。

(二)Java 实现示例

假设有一个基类 Bird,它有一个 fly 方法。

java 复制代码
public class Bird {
    public void fly() {
        System.out.println("鸟在飞");
    }
}

然后有一个子类 Penguin,企鹅虽然是鸟类,但它不会飞。按照里氏替换原则,我们可以对 Penguin 类进行如下处理:

java 复制代码
public class Penguin extends Bird {
    @Override
    public void fly() {
        // 企鹅不会飞,这里可以抛出异常或者给出提示
        System.out.println("企鹅不会飞");
    }
}

这样在使用 Bird 类型的地方,如果传入的是 Penguin 对象,程序也能正常运行,只是会有相应的提示或异常处理,而不会导致系统崩溃。

(三)关键步骤

  • 首先定义父类及其方法,如 Bird 类的 fly 方法。
  • 子类继承父类,并根据自身特性重写父类方法,如 Penguin 类重写 fly 方法时考虑自身不会飞的情况。
  • 在使用父类对象的代码中,可以替换为子类对象进行测试和运行,确保程序的正确性。

(四)流程图

复制代码
开始
|
|-- 定义 Bird 类及 fly 方法
|   |
|   |-- Penguin 类继承 Bird 类
|   |   |
|   |   |-- 重写 fly 方法
|   |
|   |-- 在使用 Bird 类对象的地方
|   |   |
|   |   |-- 替换为 Penguin 类对象进行测试
|   |   |   |
|   |   |   |-- 检查程序运行情况
结束

四、依赖倒置原则(DIP - Dependency Inversion Principle)

(一)理论介绍

依赖倒置原则强调高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。通过引入抽象层,可以降低模块之间的耦合度,提高系统的灵活性和可维护性,使得系统更容易进行扩展和修改。

(二)Java 实现示例

以一个简单的日志记录系统为例。

  1. 日志记录抽象接口
java 复制代码
public interface Logger {
    void log(String message);
}
  1. 控制台日志记录类(低层模块)
java 复制代码
public class ConsoleLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("控制台日志:" + message);
    }
}
  1. 应用程序类(高层模块)
java 复制代码
public class Application {
    private Logger logger;

    public Application(Logger logger) {
        this.logger = logger;
    }

    public void doSomething() {
        logger.log("应用程序执行了某个操作");
    }
}

这样,Application 类不依赖于具体的 ConsoleLogger 类,而是依赖于 Logger 抽象接口。如果以后要添加文件日志记录功能,只需要创建一个实现 Logger 接口的 FileLogger 类,然后在创建 Application 类时传入 FileLogger 对象即可。

(三)关键步骤

  • 定义抽象接口或抽象类,如 Logger 接口。
  • 具体的低层模块类实现该抽象,如 ConsoleLogger 类。
  • 高层模块类通过构造函数或其他方式依赖于抽象,如 Application 类依赖 Logger 接口,并在运行时传入具体的实现类对象。

(四)流程图

复制代码
开始
|
|-- 定义 Logger 接口
|   |
|   |-- ConsoleLogger 类实现 Logger 接口
|   |   |
|   |   |-- 实现 log 方法
|   |
|   |-- Application 类依赖 Logger 接口
|   |   |
|   |   |-- 通过构造函数接收 Logger 对象
|   |   |   |
|   |   |   |-- 在 doSomething 方法中使用 Logger 对象进行日志记录
|   |
|   |-- 若添加 FileLogger 类
|   |   |
|   |   |-- FileLogger 类实现 Logger 接口
|   |   |   |
|   |   |   |-- 实现 log 方法
|   |   |
|   |   |-- 在创建 Application 类时传入 FileLogger 对象
结束

五、接口隔离原则(ISP - Interface Segregation Principle)

(一)理论介绍

接口隔离原则指出客户端不应该被迫依赖于它不使用的方法。一个类对另一个类的依赖应该建立在最小的接口上,而不是大而全的接口。这样可以避免因接口中包含过多不必要的方法而导致类之间的耦合度过高,当接口发生变化时,减少对不必要依赖该接口的类的影响。

(二)Java 实现示例

假设有一个 Shape 接口,它包含了计算面积和计算周长以及绘制形状的方法。

java 复制代码
public interface Shape {
    double calculateArea();
    double calculatePerimeter();
    void draw();
}

对于一些只需要计算面积和周长的类,如 CircleRectangle,绘制形状的方法可能是不必要的。我们可以将 Shape 接口拆分为两个更小的接口。

  1. 几何形状计算接口
java 复制代码
public interface GeometricShape {
    double calculateArea();
    double calculatePerimeter();
}
  1. 可绘制形状接口
java 复制代码
public interface DrawableShape {
    void draw();
}

然后 CircleRectangle 类可以只实现 GeometricShape 接口,而对于一些需要绘制的形状类可以同时实现 DrawableShape 接口和 GeometricShape 接口。

(三)关键步骤

  • 分析接口中的方法,确定哪些方法是相关的,哪些是不相关的。
  • 根据方法的相关性将大接口拆分为多个小接口,如将 Shape 接口拆分为 GeometricShapeDrawableShape 接口。
  • 具体的类根据自身需求选择实现合适的小接口,如 Circle 类实现 GeometricShape 接口。

(四)流程图

复制代码
开始
|
|-- 分析 Shape 接口方法
|   |
|   |-- 拆分 Shape 接口为 GeometricShape 和 DrawableShape 接口
|   |   |
|   |   |-- Circle 类实现 GeometricShape 接口
|   |   |   |
|   |   |   |-- 实现 calculateArea 和 calculatePerimeter 方法
|   |   |
|   |   |-- 若有可绘制形状类
|   |   |   |
|   |   |   |-- 实现 DrawableShape 接口和 GeometricShape 接口
|   |   |   |   |
|   |   |   |   |-- 实现相应方法
结束

六、迪米特法则(LoD - Law of Demeter)

(一)理论介绍

迪米特法则也称为最少知识原则,它强调一个对象应该对其他对象有最少的了解,只和朋友通信,不和陌生人说话。一个类应该尽量减少对其他类的直接依赖,通过引入中间类或方法来降低类之间的耦合度,使得系统的结构更加清晰,易于维护和扩展。

(二)Java 实现示例

假设有一个 Order 类,它包含了 Customer 信息和 Product 信息。

java 复制代码
public class Order {
    private Customer customer;
    private Product product;

    public Order(Customer customer, Product product) {
        this.customer = customer;
        this.product = product;
    }

    public double calculateTotalPrice() {
        // 这里不直接访问 Customer 类的内部信息,而是通过 Customer 类提供的方法获取相关信息
        return product.getPrice() * customer.getDiscountFactor();
    }
}

Customer 类提供了 getDiscountFactor 方法来返回折扣因子,而不是 Order 类直接访问 Customer 类的内部数据来计算总价。

(三)关键步骤

  • 确定类之间的依赖关系,如 Order 类依赖 Customer 类和 Product 类。
  • 对于被依赖类,提供合适的方法来返回其他类需要的信息,如 Customer 类的 getDiscountFactor 方法。
  • 依赖类通过调用被依赖类的方法来获取信息并进行相关操作,如 Order 类通过 customer.getDiscountFactor() 获取折扣因子。

(四)流程图

复制代码
开始
|
|-- Order 类依赖 Customer 类和 Product 类
|   |
|   |-- Customer 类提供 getDiscountFactor 方法
|   |
|   |-- Order 类调用 customer.getDiscountFactor() 和 product.getPrice() 方法
|   |   |
|   |   |-- 计算订单总价
结束

七、组合复用原则(CRP - Composite Reuse Principle)

(一)理论介绍

组合复用原则提倡优先使用组合或聚合关系来实现复用,而不是继承关系。组合或聚合可以使系统更加灵活,因为组合对象的内部实现可以独立变化,而不会影响到其他使用该组合对象的类。继承关系则相对固定,子类对父类有较强的依赖,父类的变化可能会影响到子类。

(二)Java 实现示例

假设有一个 Car 类,它可以包含 EngineWheel 等部件。

java 复制代码
public class Car {
    private Engine engine;
    private Wheel[] wheels;

    public Car(Engine engine, Wheel[] wheels) {
        this.engine = engine;
        this.wheels = wheels;
    }

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

这里 Car 类通过组合 EngineWheel 类来实现功能,而不是继承某个 Vehicle 类并包含这些部件。如果 EngineWheel 类有改进或变化,只要它们的接口不变,Car 类不需要进行大量修改。

(三)关键步骤

  • 确定需要复用的功能或部件,如 Car 类确定需要 EngineWheel 类的功能。
  • 使用组合或聚合关系将这些部件组合到目标类中,如 Car 类通过构造函数接收 EngineWheel 类对象。
  • 在目标类中调用组合对象的方法来实现自身功能,如 Car 类的 start 方法调用 Enginestart 方法和 Wheelrotate 方法。

(四)流程图

复制代码
开始
|
|-- 确定 Car 类需要 Engine 和 Wheel 类功能
|   |
|   |-- 通过构造函数组合 Engine 和 Wheel 类对象到 Car 类
|   |   |
|   |   |-- 在 Car 类的 start 方法中
|   |   |   |
|   |   |   |-- 调用 Engine.start 方法和 Wheel.rotate 方法
结束

通过对这七种设计原则的理解和实践,在 Java 编程中能够设计出更加灵活、可维护和可扩展的软件系统,这些原则是构建高质量软件架构的重要基础,并且在 23 种设计模式中也多有体现和应用。

相关推荐
未秃头的程序猿3 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
java·后端·面试
ZJPRENO3 小时前
吃透软件开发六大设计原则,告别烂代码
设计模式
用户298698530143 小时前
Word 文档文本查找与替换的 Java 实现方案
java·后端
阿哉3 小时前
Nacos 服务发现源码:藏在背后的两套事件机制,90%的人只讲了一半
java
咖啡八杯4 小时前
GoF设计模式——命令模式
java·设计模式·架构
AI人工智能_电脑小能手4 小时前
【大白话说Java面试题 第125题】【并发篇】第25题:说说 Java 线程的中断机制
java·后端·面试
Java内核笔记4 小时前
Spring Security 源码解析(六)无状态 JWT 实践:Session 共享与自定义过滤器
java·后端
荣码4 小时前
LangGraph多Agent协作:3个Agent干活比1个强,但我踩了4个坑
java·python
唐青枫5 小时前
Java 虚拟线程实战指南:从 Thread API 到 Spring Boot 高并发应用
java
用户83562907805119 小时前
Python 操作 PDF 附件:添加、查看与管理指南
后端·python