构建健壮软件系统的基石:深入解析面向对象设计七大原则

前言

大家好,这里是程序员阿亮!

大家平时肯定多少有接触到设计模式,像工厂模式、单例模式、代理模式...

这些设计模式,他们都是通过非常多的经验沉淀下来的可以复用的设计模板

而这些设计模式也是要遵循非常多的设计原则的,今天就给大家介绍一下这七大设计原则

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

1. 核心概念

一个类应该只有一个引起它变化的原因。

换句话说,一个类只负责一项职责。如果一个类承担了太多的职责,当其中某一个职责发生变化时,可能会影响到其他职责的正常运行,从而引入不必要的 Bug,同时也降低了类的可复用性。

2. 代码示例

违背原则的反例

下面的 User 类不仅负责管理用户数据,还负责将数据持久化到数据库,以及发送邮件通知。此时,数据库结构变化或邮件发送逻辑变化,都会导致该类被修改。

java 复制代码
public class User {
    private String name;
    private String email;

    // 职责 1:领域模型属性管理
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    //  职责 2:数据库持久化
    public void saveToDatabase() {
        System.out.println("将用户数据保存到数据库...");
    }

    //  职责 3:发送邮件通知
    public void sendEmailNotification() {
        System.out.println("向用户发送欢迎邮件...");
    }
}
遵循原则的正例

将不同的职责剥离到不同的类中,让每个类专注于自己的业务。

java 复制代码
// 仅作为数据载体
public class User {
    private String name;
    private String email;
    // getter/setter 省略...
}

// 专门负责数据持久化
public class UserRepository {
    public void save(User user) {
        System.out.println("将用户数据保存到数据库...");
    }
}

// 专门负责邮件发送
public class EmailService {
    public void sendEmail(User user, String content) {
        System.out.println("向 " + user.getEmail() + " 发送邮件...");
    }
}

二、开放封闭原则 (Open/Closed Principle, OCP)

1. 核心概念

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

当需求发生变化时,我们应该通过增加新代码 的方式来扩展系统的行为,而不是修改已有的代码 。这通常需要依赖于抽象

2. 代码示例

违背原则的反例

每当我们要新增一种优惠折扣类型时,都必须修改 DiscountCalculator 中的 calculate 方法。这很容易在修改时破坏原本已经测试通过的逻辑。

java 复制代码
public class DiscountCalculator {
    public double calculate(String type, double price) {
        if ("VIP".equals(type)) {
            return price * 0.8;
        } else if ("SuperVIP".equals(type)) {
            return price * 0.7;
        }
        return price; // 原价
    }
}
遵循原则的正例

通过接口进行抽象。当有新的折扣规则时,只需实现 DiscountStrategy 接口,无需修改 PriceCalculator 的核心代码。

java 复制代码
// 抽象折扣接口
public interface DiscountStrategy {
    double applyDiscount(double price);
}

// VIP 折扣实现
public class VIPDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.8;
    }
}

// 超级 VIP 折扣实现
public class SuperVIPDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.7;
    }
}

// 计算器类(对修改关闭,对扩展开放)
public class PriceCalculator {
    public double calculate(DiscountStrategy strategy, double price) {
        return strategy.applyDiscount(price);
    }
}

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

1. 核心概念

所有引用基类的地方,必须能够透明地使用其子类的对象,而程序不会产生任何异常或行为变化。

简而言之,子类可以扩展父类的功能,但不能改变父类原有的功能。子类不应该重写父类的非抽象方法,也不应抛出父类未声明的异常。

2. 代码示例

违背原则的反例

经典的"鸵鸟不是鸟"问题。Bird 类拥有 fly 方法,但鸵鸟 Ostrich 并不会飞,若在 fly 中抛出不支持的异常,便违反了里氏替换原则,因为子类无法完全替代父类的行为。

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

public class Ostrich extends Bird {
    @Override
    public void fly() {
        //  破坏了父类行为,客户端调用时可能因抛出异常而崩溃
        throw new UnsupportedOperationException("鸵鸟其实不会飞");
    }
}
遵循原则的正例

提取出更合理的基类或通过接口隔离不通用的行为。

java 复制代码
// 基类仅保留共同特征
public class Bird {
    public void eat() {
        System.out.println("鸟儿在吃东西");
    }
}

// 提取出可飞行的行为接口
public interface Flyable {
    void fly();
}

// 燕子既是鸟,也能飞
public class Swallow extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("燕子在飞翔");
    }
}

// 鸵鸟只是鸟,不实现 Flyable
public class Ostrich extends Bird {
    // 鸵鸟保留了 Bird 的 eat() 行为,且没有不合常理的 fly() 限制
}

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

1. 核心概念

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

其核心思想是:面向接口编程,而不是面向实现编程。通过引入抽象层,可以降低模块间的耦合度。

2. 代码示例

违背原则的反例

Driver 类直接强耦合了具体类 Benz(奔驰车)。如果有一天司机要改开 BMW(宝马车),就必须去修改 Driver 的代码。

java 复制代码
public class Benz {
    public void run() {
        System.out.println("奔驰车开动了...");
    }
}

public class Driver {
    private Benz car = new Benz(); //  直接依赖具体实现

    public void drive() {
        car.run();
    }
}
遵循原则的正例

将具体的汽车品牌抽象为 Car 接口,Driver 只与接口交互。未来无论接入什么新车,Driver 类的代码都无需改动。

java 复制代码
// 抽象接口
public interface Car {
    void run();
}

// 具体实现 1
public class Benz implements Car {
    @Override
    public void run() {
        System.out.println("奔驰车开动了...");
    }
}

// 具体实现 2
public class BMW implements Car {
    @Override
    public void run() {
        System.out.println("宝马车开动了...");
    }
}

// 司机只依赖抽象接口(构造函数注入)
public class Driver {
    private final Car car;

    public Driver(Car car) {
        this.car = car; // 依赖注入
    }

    public void drive() {
        car.run();
    }
}

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

1. 核心概念

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

尽量将庞大臃肿的接口拆分为更小、更具体的接口,避免让接口的实现类被迫去实现那些它们根本不需要、也无法合理实现的方法。

2. 代码示例

违背原则的反例

定义了一个多功能接口 MultiFunctionDevice,但是低端打印机 SimplePrinter 并不具备传真和扫描的功能,却被迫留空或抛出异常。

java 复制代码
public interface MultiFunctionDevice {
    void print();
    void scan();
    void fax();
}

public class SimplePrinter implements MultiFunctionDevice {
    @Override
    public void print() {
        System.out.println("打印文档...");
    }

    @Override
    public void scan() {
        // 无法实现,被迫留空或抛出异常
        throw new UnsupportedOperationException("不支持扫描");
    }

    @Override
    public void fax() {
        // 无法实现,被迫留空
    }
}
遵循原则的正例

将接口拆分成多个单一职责的接口。实现类按需组合实现。

java 复制代码
public interface Printer {
    void print();
}

public interface Scanner {
    void scan();
}

public interface Fax {
    void fax();
}

// 简易打印机只需实现 Printer 接口
public class SimplePrinter implements Printer {
    @Override
    public void print() {
        System.out.println("打印文档...");
    }
}

// 高端多功能一体机可以实现所有接口
public class SmartPrinter implements Printer, Scanner, Fax {
    @Override
    public void print() { System.out.println("打印中..."); }
    @Override
    public void scan() { System.out.println("扫描中..."); }
    @Override
    public void fax() { System.out.println("传真中..."); }
}

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

1. 核心概念

只与你直接的朋友交谈,不跟"陌生人"说话(最少知识原则)。

一个对象应该对其他对象有最少的了解。如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。在代码编写中,应尽量避免类似于 a.getB().getC().doSomething() 这样链式跨级调用的"火车车厢"式代码。

2. 代码示例

违背原则的反例

在学校里,校长(Principal)想要打印某个班级(Classroom)的学生名单。校长不需要亲自去拿到学生类(Student)并打印,班级里的学生对于校长来说应当是"陌生人"。

java 复制代码
public class Student {
    private String name;
    public String getName() { return name; }
}

public class Classroom {
    private List<Student> students = new ArrayList<>();
    public List<Student> getStudents() { return students; }
}

public class Principal {
    // 校长直接操作了 Classroom 内部的 Student 列表,形成了过度耦合
    public void printAllStudents(Classroom classroom) {
        List<Student> students = classroom.getStudents();
        for (Student student : students) {
            System.out.println(student.getName());
        }
    }
}
遵循原则的正例

校长只对直接朋友 Classroom 下达指令,让 Classroom 自己去处理打印逻辑。

java 复制代码
public class Classroom {
    private List<Student> students = new ArrayList<>();

    // 班级自己负责打印,校长不需要知道具体的 Student 细节
    public void printStudents() {
        for (Student student : students) {
            System.out.println(student.getName());
        }
    }
}

public class Principal {
    // 只与直接朋友 Classroom 交互
    public void printAllStudents(Classroom classroom) {
        classroom.printStudents();
    }
}

七、合成/聚合复用原则 (Composition/Aggregation Reuse Principle, CARP)

1. 核心概念

尽量使用对象组合或聚合,而不是继承来达到复用的目的。

继承(is-a)属于强耦合的"白箱复用",会向子类暴露父类的实现细节,若父类发生改变,子类也会被迫改变。而组合/聚合(has-a)属于"黑箱复用",耦合度低,能在运行时动态地改变关联的对象,更加灵活。

2. 代码示例

java 复制代码
public class DBConnection {
    public void getConnection() {
        System.out.println("获取数据库连接...");
    }
}

//  错误地使用了继承
public class OrderDao extends DBConnection {
    public void saveOrder() {
        super.getConnection(); // 调用父类方法
        System.out.println("保存订单...");
    }
}
遵循原则的正例

通过属性组合的方式引入 DBConnection。这样如果后续需要将 DBConnection 替换为其他连接池(如 HikariCP),只需在外部注入即可,无需修改类继承关系。

java 复制代码
public class OrderDao {
    // 使用组合关系
    private final DBConnection dbConnection;

    public OrderDao(DBConnection dbConnection) {
        this.dbConnection = dbConnection;
    }

    public void saveOrder() {
        dbConnection.getConnection();
        System.out.println("保存订单...");
    }
}

总结

在实际的软件架构设计中,这 7 大原则并不是孤立存在的,它们彼此交织、相互支撑:

  • 单一职责原则 告诉我们类要功能专注。

  • 里氏替换原则依赖倒置原则开放封闭原则 提供了实现的基石(通过抽象和继承规范,实现扩展不改动源码)。

  • 接口隔离原则 确保了我们定义的接口足够精准,不会引入累赘。

  • 迪米特法则 引导我们减少对象间的不必要纠缠。

  • 合成/聚合复用原则 则是提醒我们在复用代码时保持清醒,警惕继承带来的过度耦合。

相关推荐
杨了个杨89821 小时前
Dockerfile介绍及镜像制作
java·开发语言
AI科技星1 小时前
《数术工坊:无穷套娃录》 一部用数学套娃写成的“天书小说”
c语言·开发语言·网络·量子计算·agi
Dxy12393102161 小时前
Python 请求:为什么 Session 比直接请求快 10 倍?
开发语言·python
dongf20191 小时前
R 语言 逻辑斯蒂回归
开发语言·数据分析·回归·r语言
Irissgwe1 小时前
C++ STL unordered系列关联式容器详解
开发语言·c++·stl·关联式容器
m0_547486661 小时前
华南农业大学《C语言程序设计》期末试卷及答案2018-2025年PDF
c语言·开发语言·pdf·c语言程序设计
San813_LDD3 小时前
[C语言]《Dev-C++ 报错解决手册(Day0607 精华版)》
java·前端·javascript
fqbqrr10 小时前
2606C++,C++构的多态
开发语言·c++