

前言
大家好,这里是程序员阿亮!
大家平时肯定多少有接触到设计模式,像工厂模式、单例模式、代理模式...
这些设计模式,他们都是通过非常多的经验沉淀下来的可以复用的设计模板
而这些设计模式也是要遵循非常多的设计原则的,今天就给大家介绍一下这七大设计原则
一、单一职责原则 (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 大原则并不是孤立存在的,它们彼此交织、相互支撑:
单一职责原则 告诉我们类要功能专注。
里氏替换原则 和 依赖倒置原则 为 开放封闭原则 提供了实现的基石(通过抽象和继承规范,实现扩展不改动源码)。
接口隔离原则 确保了我们定义的接口足够精准,不会引入累赘。
迪米特法则 引导我们减少对象间的不必要纠缠。
合成/聚合复用原则 则是提醒我们在复用代码时保持清醒,警惕继承带来的过度耦合。
