文章目录
- [零 五大设计原则](#零 五大设计原则)
- [一 单一职责原则(SRP)](#一 单一职责原则(SRP))
-
- [1.1 单一职责原则](#1.1 单一职责原则)
- [1.2 Java中的单一职责原则](#1.2 Java中的单一职责原则)
-
- [1.2.1 反例:违反 SRP 的类](#1.2.1 反例:违反 SRP 的类)
- [1.2.2 正确做法:拆分职责](#1.2.2 正确做法:拆分职责)
- [1.3 四 SRP 的"边界"](#1.3 四 SRP 的“边界”)
- [1.4 初学者常见误区](#1.4 初学者常见误区)
- [1.5 思考与延伸](#1.5 思考与延伸)
- [二 开闭原则(OCP)](#二 开闭原则(OCP))
-
- [2.1 开闭原则](#2.1 开闭原则)
- [2.2 Java中的开闭原则](#2.2 Java中的开闭原则)
-
- [2.2.1 初始实现(未遵守开闭原则)](#2.2.1 初始实现(未遵守开闭原则))
- [2.2.2 改进的实现(遵守开闭原则)](#2.2.2 改进的实现(遵守开闭原则))
- [2.2.3 关键点分析](#2.2.3 关键点分析)
- [2.3 深度探究](#2.3 深度探究)
- [2.4 初学者建议](#2.4 初学者建议)
- [三 里氏替换原则(LSP)](#三 里氏替换原则(LSP))
-
- [3.1 里氏替换原则](#3.1 里氏替换原则)
- [3.2 Java中的里氏替换原则](#3.2 Java中的里氏替换原则)
-
- [3.2.1 初始实现(未遵守LSP)](#3.2.1 初始实现(未遵守LSP))
- [3.2.2 改进的实现(遵守LSP)](#3.2.2 改进的实现(遵守LSP))
- [3.3 深度探究](#3.3 深度探究)
- [四 接口隔离原则(ISP)](#四 接口隔离原则(ISP))
-
- [4.1 接口隔离原则](#4.1 接口隔离原则)
- [4.2 Java中的接口隔离原则](#4.2 Java中的接口隔离原则)
-
- [4.2.1 初始实现(未遵守ISP)](#4.2.1 初始实现(未遵守ISP))
- [4.2.2 改进的实现(遵守ISP)](#4.2.2 改进的实现(遵守ISP))
- [4.3 深入理解接口隔离原则](#4.3 深入理解接口隔离原则)
- [4.4 接口隔离原则在设计模式中的体现](#4.4 接口隔离原则在设计模式中的体现)
- [五 依赖反转原则(DIP)](#五 依赖反转原则(DIP))
-
- [5.1 为什么要遵循依赖反转原则](#5.1 为什么要遵循依赖反转原则)
- [5.2 Java中的依赖反转原则](#5.2 Java中的依赖反转原则)
-
- [5.2.1 初始实现(不遵循DIP)](#5.2.1 初始实现(不遵循DIP))
- [5.2.2 改进的实现(遵守DIP)](#5.2.2 改进的实现(遵守DIP))
- [5.3 深入理解:依赖倒置的两个原则](#5.3 深入理解:依赖倒置的两个原则)
- [5.4 依赖反转原则的实现技巧](#5.4 依赖反转原则的实现技巧)
-
- [5.4.1 使用接口(或抽象类)](#5.4.1 使用接口(或抽象类))
- [5.4.2 依赖注入(DI)](#5.4.2 依赖注入(DI))
- 5.5深度探究
-
- [5.5.1 DIP的优势](#5.5.1 DIP的优势)
- [5.5.2 限制与注意事项](#5.5.2 限制与注意事项)
零 五大设计原则
- 五大设计原则,也被称为SOLID原则,是面向对象设计中的五个核心原则,旨在帮助开发者设计出更易于理解、维护和扩展的软件系统。它们由Robert C. Martin("Uncle Bob")提出,广泛应用于软件工程中。
原则 | 全称 | 主要思想 |
---|---|---|
S | Single Responsibility Principle | 一个类只做一件事,职责单一 |
O | Open/Closed Principle | 对扩展开放,对修改关闭 |
L | Liskov Substitution Principle | 子类可以替换父类,行为一致 |
I | Interface Segregation Principle | 不依赖没用到的接口,接口拆分细化 |
D | Dependency Inversion Principle | 依赖抽象,依赖注入 |
- S - Single Responsibility Principle(单一职责原则) : 一个类应该只有一个引起它变化的原因,也就是说,一个类应当仅承担一个职责。解释: :每个类都应专注于一项职责,避免"杂多责任"导致的类变得难以理解或维护。 示例:一个"用户"类负责存储用户信息,不要让它同时负责用户的存储和发送邮件。 示例:一个"用户"类负责存储用户信息,不要让它同时负责用户的存储和发送邮件。
- O - Open/Closed Principle(开闭原则) :软件实体(类、模块、函数等)应对扩展开放,对修改关闭。解释: 在不改变已有代码的前提下,通过扩展新功能,使系统可以应对变化。 示例:使用抽象类或接口定义基本操作,然后通过继承或实现扩展具体功能,而不是修改原有代码。
- L - Liskov Substitution Principle(里氏替换原则) :子类应当可以替换掉它们的父类对象,而程序的行为不会发生改变。解释: 如果类B继承自类A,那么在任何使用A的地方都可以用B替换,且不会引起错误或异常。示例:如果有一个Bird类,定义了fly()方法,那么所有继承Bird的类都应能飞,不能有"不能飞"的子类破坏这个原则。
- I - Interface Segregation Principle(接口隔离原则) :客户端不应依赖于它不使用的接口。解释: 避免"胖接口",建议为不同客户设计专门的接口,让实现类只依赖它们真正需要的方法。 示例:不要让一个接口定义所有操作(比如print(), scan(), fax()),而是拆分成多个接口。
- D - Dependency Inversion Principle(依赖反转原则) :高层模块不应依赖于低层模块,两者都应依赖于抽象;抽象不应依赖细节,细节应依赖抽象。解释: 依赖于抽象而不是具体实现,减少模块之间的耦合,使系统更易于扩展和维护。 示例: 高层模块依赖于接口(如
MessageService
),而具体实现(如EmailService
)通过依赖注入提供。
一 单一职责原则(SRP)
1.1 单一职责原则
- 单一职责原则(Single Responsibility Principle)是面向对象设计中 SOLID 原则 中的
S
,其核心思想是:**一个类或方法应该只有一个引起它变化的原因。**换句话说:一个类只做一件事,职责单一。
- 为什么需要单一职责原则?
- 提高代码可维护性:职责清晰,修改时定位更准确。
- 增强代码复用性:单一功能模块更容易被其他模块调用。
- 降低耦合度:各司其职,减少类之间的相互影响。
- 便于测试和调试:功能集中,单元测试更容易覆盖。
单一职责 ≠ 一个类一个方法,而是让一个类只干一件事,并把它做好。
关键词 | 说明 |
---|---|
内聚性 | 类内部方法围绕一个中心职责 |
可维护性 | 修改一处不会影响其他功能 |
扩展性 | 新需求可通过新增类实现,而非修改旧类 |
1.2 Java中的单一职责原则
1.2.1 反例:违反 SRP 的类
java
public class User {
private String name;
public void saveToDatabase() {
// 模拟保存到数据库逻辑
System.out.println(name + " saved to database.");
}
public void sendEmail(String email) {
// 模拟发送邮件逻辑
System.out.println("Email sent to " + email);
}
}
👉 问题分析:User
类承担了两个职责:管理用户数据(如保存)、发送邮件逻辑。如果将来数据库操作或邮件服务发生变更,都需要修改这个类,违背了 SRP。
1.2.2 正确做法:拆分职责
java
// 用户实体类
public class User {
private String name;
private String email;
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
// 数据访问层
public class UserRepository {
public void save(User user) {
System.out.println(user.getName() + " saved to database.");
}
}
// 邮件服务类
public class EmailService {
public void sendEmail(String email) {
System.out.println("Email sent to " + email);
}
}
✅ 改进点:
- 每个类职责单一:
User
:描述用户信息。UserRepository
:负责持久化。EmailService
:负责邮件发送。
- 修改任何一个功能,只需改动对应的类,符合 SRP。
1.3 四 SRP 的"边界"
- 粒度控制要合理,不能过度拆分。
- 职责划分应基于业务场景与变化频率。
- 可借助接口抽象来解耦职责。
1.4 初学者常见误区
误区 | 正确认识 |
---|---|
把所有功能都写在一个类里 | 应该按职责分离 |
认为每个类只能有一个方法 | 方法可以有多个,只要它们属于同一职责 |
拆分太细导致类爆炸 | 合理划分,避免过度设计 |
1.5 思考与延伸
- SRP 是构建高内聚、低耦合系统的基础。
- 在实际开发中,结合 依赖注入(DI) 和 接口编程 效果更好。
- Spring 框架中大量使用 SRP,比如
Service
、Repository
、Controller
分离。
二 开闭原则(OCP)
2.1 开闭原则
-
开闭原则(Open-Closed Principle)由"面向对象设计的四大基本原则"之一,提出者是著名的软件工程师 Bertrand Meyer。它的核心思想是:**软件实体(类、模块、函数等)应对扩展开放,对修改关闭。**也就是说,一旦一个类或模块被开发完成并投入使用后,尽量不去修改它的源代码,而是通过扩展的方式来增加新的功能。
-
**为什么要遵守开闭原则?**提高系统的可维护性: 避免频繁修改已有代码,减少引入新错误的风险;增强系统的扩展性: 可以在不改变已有代码的基础上增加新功能,满足不断变化的需求。
2.2 Java中的开闭原则
- 假设有一个简单的需求:计算不同类型的图形的面积。
2.2.1 初始实现(未遵守开闭原则)
java
public class ShapeCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.radius * circle.radius;
} else if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.width * rectangle.height;
}
return 0;
}
}
class Circle {
public double radius;
public Circle(double radius) {
this.radius = radius;
}
}
class Rectangle {
public double width;
public double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
- 问题: 每新增一种图形,就需要修改
ShapeCalculator
,违反开闭原则。
2.2.2 改进的实现(遵守开闭原则)
- 为了避免频繁修改
ShapeCalculator
,可以设计一个抽象的接口,让每个图形自己负责计算面积。
java
// 定义接口
interface Shape {
double getArea();
}
// 实现不同的图形
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double getArea() {
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 getArea() {
return width * height;
}
}
// 计算器类
public class ShapeCalculator {
public double calculateArea(Shape shape) {
return shape.getArea();
}
}
2.2.3 关键点分析
- 封装变化点: 每新增一个图形,只需要实现
Shape
接口,不需要修改ShapeCalculator
,符合"对扩展开放,对修改关闭"的原则。 - 代码扩展方便: 增加新图形,只需实现
Shape
接口即可。
2.3 深度探究
- 为什么这符合开闭原则?
- 多态的应用: 通过接口(
Shape
)实现多态,ShapeCalculator
无需关心具体是哪种图形,只需调用统一的接口方法。 - 降低耦合度: 不同图形的具体实现与计算器解耦,系统变得更灵活。
- 多态的应用: 通过接口(
2.4 初学者建议
- 理解多态和接口的基本概念,熟悉设计原则的思想。
- 在实际开发中,养成"少修改、多扩展"的习惯。
- 反复练习:尝试将已有的"巨石式"代码改造成符合开闭原则的结构。
三 里氏替换原则(LSP)
当然可以!作为一名致力于帮助初学者理解设计模式的"博客大师",我将用浅显易懂的语言,结合Java示例,深入探讨里氏替换原则(Liskov Substitution Principle, LSP)。这是面向对象设计中的一项核心原则,对于写出灵活、可扩展的代码非常重要。
3.1 里氏替换原则
- 里氏替换原则就是:如果用父类类型定义的对象,可以被任何子类对象替换,而程序的行为都不受影响,那么这个继承关系就是符合里氏替换原则的。 通俗理解: 你可以用子类的对象替换父类的对象,而程序仍然能正常工作,没有错误或者异常。
- 核心思想:子类可以替代父类,行为一致,不引入预料之外的副作用或错误。
- 为什么要遵守里氏替换原则?
- 保证代码的可扩展性:当你用子类替换父类,不会破坏原有的功能。
- 增强代码的灵活性:方便未来增加新子类,不需要修改现有代码。
- 符合面向对象的设计理念:强调"开闭原则",对扩展开放,对修改封闭。
3.2 Java中的里氏替换原则
3.2.1 初始实现(未遵守LSP)
- 假设有一个
Rectangle
(矩形)类和一个Square
(正方形)类,Square
继承自Rectangle
。
java
class Rectangle {
protected double width;
protected double height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(double width) {
this.width = this.height = width;
}
@Override
public void setHeight(double height) {
this.width = this.height = height;
}
}
- 问题: 如果你写一个方法接受
Rectangle
对象,调用setWidth()
和setHeight()
,期望得到一个矩形,但如果传入的是Square
对象,行为会变得不可预测。
java
public void resizeRectangle(Rectangle rect) {
System.out.println("Area: " + rect.getArea());
}
- 调用:
java
Rectangle rect = new Rectangle();
rect.setWidth(2);
rect.setHeight(3);
resizeRectangle(rect); // 输出:Area: 6
Square square = new Square();
square.setWidth(2);
square.setHeight(3);
resizeRectangle(square); // 输出:Area: 9
Square
的行为破坏了Rectangle
预期的行为,不符合里氏替换原则。
3.2.2 改进的实现(遵守LSP)
- 为了遵守LSP,我们应该避免让
Square
继承Rectangle
,或者设计一个更合理的继承关系。 - 一种解决方案是定义一个通用的接口或抽象类,比如
Shape
,分别实现Rectangle
和Square
,它们都实现自己的行为。
java
interface Shape {
double getArea();
}
class Rectangle implements Shape {
protected double width;
protected double height;
public void setWidth(double width) { this.width = width; }
public void setHeight(double height) { this.height = height; }
public double getArea() { return width * height; }
}
class Square implements Shape {
private double side;
public void setSide(double side) { this.side = side; }
public double getArea() { return side * side; }
}
- 这样,传入的
Shape
类型对象,无论是矩形还是正方形,都可以正常使用,符合LSP。
3.3 深度探究
更深入的理解::LSP要求子类在继承时,不仅要遵循类型关系,更要遵循"行为契约"。这意味着子类不能缩减父类的功能,也不能增加不符合父类预期的行为。
- 设计建议 :
- 避免让子类改变父类已有的方法的行为。
- 避免在子类中重写父类的方法,导致行为偏离预期。
- 使用接口或抽象类,定义明确的行为契约,让子类实现。
- 设计时考虑"预期行为",确保子类符合父类的规范。
实际应用中:
- 在重写方法时,要保证行为不变或更宽松(如返回值、异常处理等)。
- 使用设计模式(如策略模式、组合优于继承)减少继承带来的潜在问题。
- 利用单元测试验证子类是否忠实地替代父类。
四 接口隔离原则(ISP)
4.1 接口隔离原则
- 接口隔离原则(Interface Segregation Principle, ISP)是面向对象设计的五大原则之一(SOLID原则中的"I") 。它的核心思想是: "客户端不应该依赖它不使用的方法" 。换句话说,一个接口(Interface)应该尽量小而专一,让实现接口的类只需要关注自己真正用到的功能,避免被迫实现无关的方法。
4.2 Java中的接口隔离原则
4.2.1 初始实现(未遵守ISP)
- 假设有一个很大的接口,里面定义了很多方法:
java
public interface Animal {
void eat();
void fly();
void swim();
}
- 如果一只鸟实现这个接口,理论上它要实现
eat()
、fly()
和swim()
,但是鸟不会游泳呀! - 如果一只鱼实现这个接口,鱼不会飞,也要实现
fly()
方法,勉强实现可能只是空方法或者抛异常。
这就违反了接口隔离原则,造成了:
- 实现类负担过重:实现无用的方法,代码冗余。
- 代码难以维护:修改接口,可能影响不相关的实现。
- 设计不灵活:难以扩展和复用。
4.2.2 改进的实现(遵守ISP)
- 将一个胖接口拆成多个功能单一的小接口。比如:
java
public interface Eater {
void eat();
}
public interface Flyer {
void fly();
}
public interface Swimmer {
void swim();
}
- 然后不同的类只实现自己需要的接口:
java
public class Bird implements Eater, Flyer {
@Override
public void eat() {
System.out.println("Bird is eating");
}
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
public class Fish implements Eater, Swimmer {
@Override
public void eat() {
System.out.println("Fish is eating");
}
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
- 这样,不同的动物只关心自己相关的行为,实现起来更清晰、易维护。
4.3 深入理解接口隔离原则
- 面向接口编程:ISP强调接口设计要简洁、专注。接口是定义行为的契约,不应该臃肿。
- 提高灵活性和可扩展性:拆分接口后,新增功能只需新增接口,不必修改已有接口和类,符合开闭原则。
- 减少耦合:客户端只依赖相关接口,避免因接口变更影响无关代码。
- 配合依赖倒置原则:ISP与依赖倒置原则(DIP)相辅相成,设计出灵活的系统结构。
4.4 接口隔离原则在设计模式中的体现
很多设计模式都体现了接口隔离原则:
- 策略模式:定义多个小的策略接口,客户端只用其中一种策略,避免一个策略接口承担过多职责。
- 装饰器模式:装饰接口通常小而单一,装饰类只负责增强相关功能。
- 观察者模式:定义观察者接口只包含通知相关的方法,避免包含无关行为。
五 依赖反转原则(DIP)
- **依赖反转原则(Dependency Inversion Principle, DIP)**是五大设计原则(SOLID原则)之一。它旨在降低模块之间的耦合度,提高系统的灵活性和可维护性。
- 简单来说,DIP 强调:
- 高层模块(即业务逻辑)不应该依赖于低层模块(具体实现)。
- 两者都应该依赖于抽象(接口或抽象类)。
- 抽象不依赖于细节,细节依赖于抽象。
这听起来可能抽象,但实际上它是为了让我们的系统更易于扩展和变化。
- 依赖反转原则(DIP)是设计灵活、可维护系统的重要原则。它鼓励我们通过抽象来降低模块之间的耦合,让系统更易于扩展和测试。
5.1 为什么要遵循依赖反转原则
- 假设你在开发一个支付系统,你可能会有不同的支付方式,比如支付宝、微信支付、银行卡支付等。如果你的代码直接依赖于具体的支付类,就会导致:更换支付方式时需要修改大量代码,单元测试变得困难(因为具体实现绑死在代码中)。
- DIP的目标是让"高层"(业务逻辑)不依赖于"低层"(具体实现),而是都依赖于抽象(接口),从而达到:松耦合 、易扩展 、便于测试。
5.2 Java中的依赖反转原则
5.2.1 初始实现(不遵循DIP)
java
class Light {
void turnOn() { System.out.println("Light is ON"); }
void turnOff() { System.out.println("Light is OFF"); }
}
class Switch {
private Light light = new Light();
void operate() {
light.turnOn();
// 其他操作
}
}
- 在这个例子中,
Switch
直接依赖于Light
的具体实现,若想切换到其他灯,就需要修改Switch
类。
5.2.2 改进的实现(遵守DIP)
java
// 定义一个抽象接口
interface Switchable {
void turnOn();
void turnOff();
}
// 具体实现
class Light implements Switchable {
public void turnOn() { System.out.println("Light is ON"); }
public void turnOff() { System.out.println("Light is OFF"); }
}
class Fan implements Switchable {
public void turnOn() { System.out.println("Fan is ON"); }
public void turnOff() { System.out.println("Fan is OFF"); }
}
// 高层模块
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
void operate() {
device.turnOn();
// 其他逻辑
}
}
关键点:
Switch
依赖于Switchable
接口,而非具体实现。- 可以随意切换
Light
或Fan
,无需修改Switch
类。
5.3 深入理解:依赖倒置的两个原则
DIP实际上包含两个方面:
- 高层模块不依赖于低层模块。这意味着高层模块(如业务逻辑)只依赖抽象。
- 抽象不依赖于细节,细节依赖于抽象。这意味着具体实现(细节)依赖于抽象。
简要总结:
- 面向接口编程:通过接口或抽象类依赖。
- 依赖注入(Dependency Injection):将依赖的具体实现通过构造器、Setter等注入,减少硬编码依赖。
5.4 依赖反转原则的实现技巧
5.4.1 使用接口(或抽象类)
- 定义接口,让高层模块依赖接口而不是具体实现。
java
interface MessageService {
void sendMessage(String message);
}
class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
class Notification {
private MessageService messageService;
public Notification(MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
5.4.2 依赖注入(DI)
- 通过构造器或框架(如Spring)将实现注入。这样,
Notification
类不关心具体的实现,只依赖接口,增强了灵活性。
java
public class Main {
public static void main(String[] args) {
MessageService email = new EmailService();
Notification notification = new Notification(email);
notification.notify("Hello World");
}
}
5.5深度探究
5.5.1 DIP的优势
- 增强系统的可扩展性:添加新功能(如新支付方式)只需实现接口,无需修改现有代码。
- 提升可测试性:可以用模拟(Mock)对象替代具体实现进行单元测试。
- 降低耦合度:高层模块与低层模块解耦。
5.5.2 限制与注意事项
- 过度设计:在简单场景中过度使用接口,可能增加不必要的复杂度。
- 依赖注入的复杂性:引入框架(如Spring)可能增加学习成本。
- 设计平衡:应结合实际需求合理应用。