目录
1. 开闭原则(Open-Closed Principle, OCP)
2. 里氏替换原则(Liskov Substitution Principle, LSP)。
3. 依赖倒置原则(Dependency Inversion Principle, DIP)
4. 接口隔离原则(Interface Segregation Principle, ISP)
四、重构到模式(Refactor to DesignPattern)
一、面向对象原则(OO+SOLID )
包括但不限于以下几个方面:
序号 | 名称 | 作用 |
1 | 单一职责原则 (SRP) | 一个类应该有且仅有一个引起它变化的原因,即一个类应该只负责一项职责。 |
2 | 开闭原则 (OCP) | 软件实体(类、模块、函数等)应该是可扩展的,而不可修改的。即对于扩展是开放的,对于修改是关闭的。 |
3 | 里氏替换原则 (LSP) | 子类型必须能够替换它们的基类型,即派生类(子类)能够替换基类(父类)出现的任何地方,并且保证原来程序的逻辑正确性不被破坏。 我的理解:基类(父类)的派生类(子类)要考虑周全,例如图形的派生类矩形和正方形计算面积,要重写正方形设置宽高的方法,以免产生未知错误。 |
4 | 依赖倒置原则 (DIP) | 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。 |
5 | 接口隔离原则 (ISP) | 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。 |
关于以上几个方面的理解,ChatGPT是这么说的:
1. 开闭原则(Open-Closed Principle, OCP)
它指出软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
简单来说,开闭原则要求系统设计要能够在不修改现有代码的情况下进行功能扩展。
举个例子来说明开闭原则:
假设有一个图形绘制程序,最初设计用于绘制不同类型的图形,如圆形和矩形:
```java
class Circle {
public void draw() {
System.out.println("Drawing Circle");
// 绘制圆形的具体实现
}
}
class Rectangle {
public void draw() {
System.out.println("Drawing Rectangle");
// 绘制矩形的具体实现
}
}
```
这个设计在现有需求下工作正常,但如果未来需要添加新的图形类型(比如三角形),按照现有的设计,需要修改已有的代码,违反了开闭原则。
改进后的设计应用了开闭原则:
```java
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Drawing Circle");
// 绘制圆形的具体实现
}
}
class Rectangle implements Shape {
public void draw() {
System.out.println("Drawing Rectangle");
// 绘制矩形的具体实现
}
}
class Triangle implements Shape {
public void draw() {
System.out.println("Drawing Triangle");
// 绘制三角形的具体实现
}
}
```
在这个改进的设计中,引入了抽象的 `Shape` 接口,每种具体的图形都实现了这个接口。现在,如果需要添加新的图形类型(比如三角形),只需要创建新的类并实现 `Shape` 接口,而不需要修改现有的 `Circle` 和 `Rectangle` 类。
这种方式符合开闭原则,因为系统的扩展是通过添加新代码而不是修改现有代码来实现的。通过遵循开闭原则,可以提高系统的稳定性和可维护性,使系统更易于扩展和修改。
总结一下,开闭原则要求在设计软件时,应该尽量避免修改既有代码,而是通过扩展现有代码来满足新的需求,从而提高代码的可重用性和可维护性。
2. 里氏替换原则(Liskov Substitution Principle, LSP)。
关键点在于子类应该能够完全替换掉父类,并且在任何使用父类的地方都不会产生意外的行为或错误结果。
举个例子,假设有一个图形类(Shape)和它的子类矩形(Rectangle)和正方形(Square)。如果我们有一个程序期望可以使用任何形状来计算面积,那么矩形和正方形的行为应该一致且符合预期。如果正方形类重写了设置宽度和高度的方法,使得它们总是相等,那么在计算面积时不应该导致意外的结果。
违反LSP的例子可以是,当我们传入一个正方形对象而程序却按照矩形的方式来处理,导致计算出的面积不正确。这种情况下,代码不能够正确地处理正方形,因为它没有考虑到正方形特有的行为(宽高相等)。
因此,遵循LSP意味着在设计和使用继承时,需要确保子类在任何使用父类的地方都能够正确地工作,并且不会破坏程序的整体逻辑。
3. 依赖倒置原则(Dependency Inversion Principle, DIP)
重点在于如何降低代码模块之间的耦合度,使系统更加灵活和可扩展。
假设有一个电商系统,订单处理模块需要通知顾客订单状态。最初的设计可能是直接在订单处理模块中使用具体的邮件通知类来发送邮件,像这样:
```java
class OrderProcessor {
private EmailNotifier notifier = new EmailNotifier();
public void processOrder(Order order) {
// 处理订单逻辑
notifier.notifyCustomer(order.getCustomer(), "Your order has been processed.");
}
}
```
这种设计不符合依赖倒置原则,因为高层的订单处理模块直接依赖于低层的具体实现(EmailNotifier 类)。
改进后的设计应用了依赖倒置原则:
```java
interface Notifier {
void notifyCustomer(Customer customer, String message);
}
class EmailNotifier implements Notifier {
public void notifyCustomer(Customer customer, String message) {
// 发送邮件通知的具体实现
System.out.println("Sending email to " + customer.getEmail() + ": " + message);
}
}
class OrderProcessor {
private Notifier notifier;
public OrderProcessor(Notifier notifier) {
this.notifier = notifier;
}
public void processOrder(Order order) {
// 处理订单逻辑
notifier.notifyCustomer(order.getCustomer(), "Your order has been processed.");
}
}
```
在这个改进的设计中,订单处理模块不再直接依赖于具体的 EmailNotifier 类,而是依赖于通用的 Notifier 接口。这样做的好处是,如果未来需要添加其他的通知方式(比如短信通知),只需编写新的实现 Notifier 接口的类即可,而不需要修改订单处理模块的代码。这种方式降低了模块间的耦合度,使系统更加灵活和可扩展。
因此,依赖倒置原则的核心思想是,通过引入抽象层(接口或抽象类),使高层模块不依赖于具体实现细节,而是依赖于抽象。
4. 接口隔离原则(Interface Segregation Principle, ISP)
强调设计接口时应该精简单一,不应该强迫客户端依赖于它们不使用的方法。
举个例子来说明接口隔离原则:
假设有一个电子设备接口的设计:
**不符合ISP的设计**:
```java
interface Device {
void powerOn();
void powerOff();
void volumeUp();
void volumeDown();
void play();
void pause();
void stop();
}
```
在上述设计中,如果有些设备只支持基本的开关和音量控制,但不支持播放控制(比如一些简单的音响设备),那么客户端可能会被迫实现不需要的方法。
**符合ISP的设计**:
```java
interface Switchable {
void powerOn();
void powerOff();
}
interface AdjustableVolume {
void volumeUp();
void volumeDown();
}
interface Playable {
void play();
void pause();
void stop();
}
```
在这个改进的设计中,将接口分解为更小的接口,每个接口都代表一个特定的行为。这样,具体的设备类可以选择实现它们需要的接口,而不需要强制实现所有方法。
例如,对于简单的音响设备:
```java
class SimpleSpeaker implements Switchable, AdjustableVolume {
// 实现开关和音量控制的方法
}
class AdvancedMediaPlayer implements Switchable, AdjustableVolume, Playable {
// 实现开关、音量控制和播放控制的方法
}
```
这种方式符合ISP,因为每个类只需实现它们真正需要的接口方法,避免了不必要的方法实现,提高了代码的可维护性和灵活性。
总结来说,接口隔离原则要求接口设计应该小而专一,确保客户端不会依赖于其不需要的接口方法,从而降低耦合度,增强系统的灵活性和可扩展性。
二、设计模式(Design Pattern)
设计模式是解决特定软件设计问题的经过验证的解决方案。它们提供了一种通用的、可重复使用的设计思路,可以用于解决各种不同的问题,并且已经在实践中被证明是有效的。
设计模式通常分为三种类型:创建型模式、结构型模式和行为型模式。
1. **创建型模式**:这些模式与对象创建相关,包括工厂模式、抽象工厂模式、建造者模式、原型模式和单例模式。它们关注如何创建对象,以及如何将对象实例化。
2. **结构型模式**:这些模式关注类和对象的组合,包括适配器模式、桥接模式、组合模式、装饰者模式、外观模式、享元模式和代理模式。它们解决了不同对象之间的组合问题,提供了更加灵活的结构。
3. **行为型模式**:这些模式关注对象之间的通信,包括责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式和访问者模式。它们解决了对象之间的交互和通信问题。
每种设计模式都有其特定的用途和优势,可以根据具体的设计需求选择合适的模式来解决问题,提高代码的可维护性、可扩展性和重用性。
三、简单设计(SimpleDesign)
- 通过所有测试(Passes its tests)
- 尽可能消除重复 (Minimizes duplication)
- 尽可能清晰表达 (Maximizes clarity)
- 更少代码元素 (Has fewer elements)
以上四个原则的重要程度依次降低。
四、重构到模式(Refactor to DesignPattern)
重构到模式(Refactor to Design Pattern)是一种软件开发实践,它涉及到将现有的代码通过重构的方式转换为遵循某种设计模式。这种实践的目的是使代码更加清晰、模块化并具有更好的可维护性。
以下是一些常见的从普通代码重构到设计模式的方法:
1. **重构到单例模式**:如果一个系统中有一个全局唯一的对象,可以通过将构造函数设置为私有,并提供一个静态方法来获取该对象的实例,从而将其重构为单例模式。
2. **重构到工厂模式**:当有多个类实现了同一个接口或继承自同一个基类,并且需要通过一个统一的方式来创建这些类的实例时,可以重构为工厂模式,通过工厂类来创建对象。
3. **重构到策略模式**:如果代码中有许多条件语句或if-else分支,可以根据不同的条件执行不同的逻辑,可以将这些逻辑封装成不同的策略类,并通过上下文类来切换不同的策略。
4. **重构到观察者模式**:当有多个对象需要响应某个事件时,可以将这些对象作为观察者注册到发布者中,当事件发生时,发布者通知所有注册的观察者。
5. **重构到状态模式**:如果一个对象的行为取决于它的状态,并且这个状态可以在运行时改变,可以将这些行为封装到不同的状态类中,并在状态改变时切换状态对象。
6. **重构到装饰者模式**:当需要向一个现有的类添加新的功能,但不希望修改这个类本身时,可以使用装饰者模式。创建一个装饰者类,继承自被装饰的类,并添加新的功能。
7. **重构到模板方法模式**:如果一个类中的某些操作可以在不同的子类中以不同的方式实现,但是有一些操作是相同的,可以将这些相同的操作封装到父类中,作为模板方法,而将不同的操作留给子类实现。
重构到模式的过程通常涉及到代码的修改和添加,需要对原有的设计进行深入的理解,并且在重构过程中进行充分的测试,以确保代码的正确性。