设计模式的六大原则
为什么学习设计模式的六大原则,举个例子Spring设计的思想与设计模式原则高度契合,我们可以根据这些原理来评判自己在代码实现的时候有没有可能让程序更灵活可扩展,有助于我们写出代码模块化并且结构合理的程序。
单一职责原则(Single Responsibility Principle,SRP)是面向对象设计中的一项重要原则,它指出一个类应该只有一个原因引起变化,也就是说,一个类应该只承担一个职责或功能。
具体含义:
1.定义清晰:每个类应该专注于做一件事情。这使得代码更加清晰易懂,有助于维护和扩展。
2.降低复杂性:当一个类承担多个职责时,它的复杂性会增加,导致难以理解和修改。如果需要更改某个职责,可能会影响其他职责,造成不必要的错误。
3.提高可维护性:符合单一职责原则的类通常比较简单,便于测试和维护。如果需要修改或扩展某个功能,只需要调整相关类,而不会影响到系统中的其他部分。
4.增强可重用性:将功能分离到不同的类中可以提高代码的重用行。其他组件可以根据需要引用特定的类,而不必依赖于包含多个职责的复杂类。
java
假设有一个 Report 类,负责生成报告并发送邮件。如果这个类同时生成报告和发送邮件,就违反了单一职责原则。
class Report {
public void generateReport() {
// 生成报告的逻辑
}
public void sendEmail() {
// 发送邮件的逻辑
}
}
为了遵循单一职责原则,可以将其拆分为两个类:
class ReportGenerator {
public void generateReport() {
// 生成报告的逻辑
}
}
class EmailSender {
public void sendEmail() {
// 发送邮件的逻辑
}
}
这样,每个类都有单一的职责,更加符合 SRP 的要求,从而提高了代码的可维护性和可读性。
开闭原则(Open/Closed Principle,OCP)是面向对象设计中的一个重要原则,旨在促进系统的可扩展性和维护性。它的核心思想是:
定义:
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
具体含义:
1.对扩展开发:意味着可以通过添加新的代码来增加功能,而不需要改动已有的代码。这通常通过继承、接口或者抽象类实现。
2.对修改关闭:指的是现有的代码在需求变更的时候不应该被修改,以免引入新的错误或者破坏已有的功能。
实现方式:
使用抽象类和接口:通过定义接口或者抽象类,允许子类提供具体的实现,从而支持新功能的添加。
策略模式:可以通过封装算法,将其作为参数传递,允许在运行时选择不同的算法。
模版方法模式:在基类中定义算法的框架,允许子类实现具体的步骤。
java
假设我们有一个简单的图形绘制程序,最初只能绘制圆形:
class Circle {
public void draw() {
System.out.println("Drawing a circle");
}
}
如果现在需要新增矩形的绘制功能,按照开闭原则,我们可以创建一个新的类,而不是修改原有的 Circle 类:
class Rectangle {
public void draw() {
System.out.println("Drawing a rectangle");
}
}
通过定义一个接口:
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Drawing a circle");
}
}
class Rectangle implements Shape {
public void draw() {
System.out.println("Drawing a rectangle");
}
}
现在,如果需要增加其他形状(如三角形),只需创建一个新的类,实现 Shape 接口,而不需要修改现有的类。
总结:
开闭原则有助于构建灵活且易于扩展的系统,减少因需求变更而引起的代码修改,从而提高软件的可维护性和稳定性。
里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计中的一个基本原则,旨在确保子类可以替代父类而不会影响程序的正确性。这个原则由计算机科学家Barbara Liskov在1987年提出。
定义:
如果对每一个类型为T1的对象o1,都存在类型为T2的对象o2,使得在对象o1出现的地方都可以用对象o2代替,而程序的行为不变,那么类型T2是类型T1的子类型。
具体含义:
-
可替代性:子类的对象必须能够替代父类的对象,而不影响系统的正常运行。这意味着子类应该遵循父类的行为规范。
-
遵循约定:子类在实现父类的方法时,不能违背父类所定义的行为。例如,如果父类的方法承诺某些条件,子类也必须遵守这些条件。
-
类型一致性:使用父类引用的地方,应该能够使用子类的对象,而不需要进行额外的检查或导致意外的行为。
java
假设我们有一个 Bird 鸟 类和一个 Penguin 企鹅类:
class Bird {
public void fly() {
System.out.println("Flying");
}
}
class Sparrow extends Bird {
@Override
public void fly() {
System.out.println("Sparrow flying");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins cannot fly");
}
}
在这个例子中,Penguin 类违反了里氏替换原则,因为它不能替代 Bird 类的行为,造成程序在使用 Penguin 对象时会抛出异常。
正确的做法:
为了遵循里氏替换原则,我们可以将飞行行为提取到一个接口中,只让能够飞的鸟实现这个接口:
interface Flyable {
void fly();
}
class Bird {
// 其他鸟类通用的属性和方法
}
class Sparrow extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow flying");
}
}
class Penguin extends Bird {
// 不实现 Flyable 接口
}
这样,Sparrow 可以飞,而 Penguin 则不必实现飞行的方法,遵循了里氏替换原则。
总结:
里氏替换原则强调了继承关系中的替代性,确保在使用多态时,程序的行为保持一致。这有助于提高代码的可维护性和可扩展性,减少因子类不当实现引起的错误。
接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计中的一个重要原则,旨在减少类之间的依赖关系,使得系统更加灵活和可维护。
定义:
接口隔离原则指出:**不应该强迫一个类依赖于它不使用的方法。**换句话说,客户端不应该被迫实现它不需要的方法。接口应该尽可能小,以便仅包含客户端所需的功能。
具体含义:
-
小而专用的接口:设计小的、专用的接口,使得每个接口只包含特定的功能,而不是一个庞大的多功能接口。这有助于提高代码的可读性和可维护性。
-
降低耦合:通过将接口拆分为多个专用接口,减少了类之间的依赖,从而降低了耦合度。这使得修改某个接口或实现类时,不会对其他不相关的类造成影响。
-
增强灵活性:当系统需求变化时,通过实现不同的接口,可以更容易地适应新需求,而不需要大幅修改代码。
java
假设我们有一个大型接口 Animal,其中定义了许多方法:
interface Animal {
void eat();
void fly();
void swim();
}
如果我们有一个 Bird 类和一个 Fish 类,它们都实现了这个接口:
class Bird implements Animal {
@Override
public void eat() {
System.out.println("Bird is eating");
}
@Override
public void fly() {
System.out.println("Bird is flying");
}
@Override
public void swim() {
// 不适用
}
}
class Fish implements Animal {
@Override
public void eat() {
System.out.println("Fish is eating");
}
@Override
public void fly() {
// 不适用
}
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
在这个例子中,Bird 和 Fish 都实现了不适用的方法,导致它们的实现变得不干净且难以维护。
改进:
为了遵循接口隔离原则,我们可以将接口拆分为更小、更具体的接口:
interface Eater {
void eat();
}
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Bird implements Eater, Flyable {
@Override
public void eat() {
System.out.println("Bird is eating");
}
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
class Fish implements Eater, Swimmable {
@Override
public void eat() {
System.out.println("Fish is eating");
}
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
在这个改进后的设计中,Bird 和 Fish 只实现了它们所需的接口,遵循了接口隔离原则,从而使得代码更加清晰和易于维护。
总结:
接口隔离原则强调了接口的设计应当简洁和专一,避免冗余和不必要的依赖。这有助于提高系统的灵活性和可维护性,使得程序员能够更方便地管理和扩展代码。
依赖倒置原则(Dependency Inversion Principle, DIP)是面向对象设计中的一个重要原则,旨在降低模块之间的依赖关系,提高系统的灵活性和可维护性。
定义:
依赖倒置原则可以总结为以下两条主要规则:
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
具体含义:
- 高层模块:通常指的是业务逻辑或者应用的核心部分,负责处理重要的功能。
- 低层模块:通常指的是实现细节,比如数据库操作、文件读写等具体实现。
通过依赖倒置原则,我们将高层模块与低层模块之间的直接依赖关系,通过抽象接口进行解耦,这样高层模块就可以独立于具体的实现,从而提高了灵活性和可测试性。
java
示例:
假设我们有一个简单的邮件发送功能,传统的实现可能如下所示:
class EmailSender {
public void sendEmail(String message) {
// 发送邮件的具体实现
}
}
class NotificationService {
private EmailSender emailSender;
public NotificationService() {
this.emailSender = new EmailSender(); // 直接依赖于具体实现
}
public void notifyUser(String message) {
emailSender.sendEmail(message);
}
}
在这个例子中,NotificationService 直接依赖于 EmailSender,这使得代码的扩展和测试变得困难。
改进:
为了遵循依赖倒置原则,我们可以引入一个抽象接口:
interface MessageSender {
void send(String message);
}
class EmailSender implements MessageSender {
@Override
public void send(String message) {
// 发送邮件的具体实现
}
}
class NotificationService {
private MessageSender messageSender;
public NotificationService(MessageSender messageSender) {
this.messageSender = messageSender; // 依赖于抽象,不依赖于具体实现
}
public void notifyUser(String message) {
messageSender.send(message);
}
}
在这个改进后的设计中,NotificationService 依赖于 MessageSender 接口,而不是具体的 EmailSender 类。这样,我们可以轻松地添加其他类型的消息发送实现,例如短信发送或推送通知,只需实现 MessageSender 接口即可。
总结:
依赖倒置原则强调高层模块应该依赖于抽象,而不是具体实现。这种设计使得系统更加灵活、可扩展,同时也更容易进行单元测试。通过依赖倒置原则,开发人员可以更好地管理复杂系统中的依赖关系,提升代码质量。
合成复用原则(Composition over Inheritance Principle)是面向对象设计中的一个重要原则,强调在设计软件系统时,优先使用对象组合(合成)而不是类继承来实现代码复用和功能扩展。
定义:
合成复用原则的核心思想是通过将多个对象组合在一起,来形成更复杂的功能,而不是通过继承关系来实现。这样可以减少类之间的耦合,提高灵活性和可维护性。
具体含义:
- 组合:将不同的对象组合在一起,以实现所需的功能。例如,可以通过将不同的组件组合成为一个完整的系统。
- 继承:通过创建一个子类来扩展或修改父类的行为。虽然继承能实现复用,但可能导致紧密耦合和脆弱的基类问题。
优势:
-
灵活性:通过组合,可以在运行时动态地改变对象的行为,而继承通常是在编译时确定的。
-
降低耦合:组合使得类之间的依赖关系更松散,易于维护和扩展。
-
更好的测试:组合的对象更容易进行单元测试,因为它们通常是独立的,可以单独测试各个组成部分。
示例:
假设我们正在设计一个图形编辑器,传统的继承方法可能如下:
java
假设我们正在设计一个图形编辑器,传统的继承方法可能如下:
class Shape {
void draw() {}
}
class Circle extends Shape {
void draw() {
// 绘制圆形
}
}
class Square extends Shape {
void draw() {
// 绘制方形
}
}
在这种情况下,如果我们想要添加新的形状,就需要创建新的子类。这种继承方式会导致类层次结构的膨胀。
改进:
使用合成复用原则,我们可以通过组合不同的绘制行为来实现:
interface Drawable {
void draw();
}
class Circle implements Drawable {
public void draw() {
// 绘制圆形
}
}
class Square implements Drawable {
public void draw() {
// 绘制方形
}
}
class Canvas {
private List<Drawable> shapes = new ArrayList<>();
public void addShape(Drawable shape) {
shapes.add(shape);
}
public void drawShapes() {
for (Drawable shape : shapes) {
shape.draw();
}
}
}
在这个改进的例子中,Canvas 类可以动态地组合不同的 Drawable 对象,而不需要依赖于固定的继承结构。这样,后续添加新形状只需实现 Drawable 接口,而无需修改现有的类。
总结:
合成复用原则提倡优先使用组合而非继承来实现代码复用,能够提高系统的灵活性、可维护性和可扩展性。在实际开发中,遵循这一原则有助于构建更加模块化和松耦合的系统设计。