设计模式之五大设计原则(SOLID原则)浅谈

文章目录

  • [零 五大设计原则](#零 五大设计原则)
  • [一 单一职责原则(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 依赖抽象,依赖注入

  1. S - Single Responsibility Principle(单一职责原则) : 一个类应该只有一个引起它变化的原因,也就是说,一个类应当仅承担一个职责。解释: :每个类都应专注于一项职责,避免"杂多责任"导致的类变得难以理解或维护。 示例:一个"用户"类负责存储用户信息,不要让它同时负责用户的存储和发送邮件。 示例:一个"用户"类负责存储用户信息,不要让它同时负责用户的存储和发送邮件。
  2. O - Open/Closed Principle(开闭原则) :软件实体(类、模块、函数等)应对扩展开放,对修改关闭。解释: 在不改变已有代码的前提下,通过扩展新功能,使系统可以应对变化。 示例:使用抽象类或接口定义基本操作,然后通过继承或实现扩展具体功能,而不是修改原有代码。
  3. L - Liskov Substitution Principle(里氏替换原则) :子类应当可以替换掉它们的父类对象,而程序的行为不会发生改变。解释: 如果类B继承自类A,那么在任何使用A的地方都可以用B替换,且不会引起错误或异常。示例:如果有一个Bird类,定义了fly()方法,那么所有继承Bird的类都应能飞,不能有"不能飞"的子类破坏这个原则。
  4. I - Interface Segregation Principle(接口隔离原则) :客户端不应依赖于它不使用的接口。解释: 避免"胖接口",建议为不同客户设计专门的接口,让实现类只依赖它们真正需要的方法。 示例:不要让一个接口定义所有操作(比如print(), scan(), fax()),而是拆分成多个接口。
  5. D - Dependency Inversion Principle(依赖反转原则) :高层模块不应依赖于低层模块,两者都应依赖于抽象;抽象不应依赖细节,细节应依赖抽象。解释: 依赖于抽象而不是具体实现,减少模块之间的耦合,使系统更易于扩展和维护。 示例: 高层模块依赖于接口(如MessageService),而具体实现(如EmailService)通过依赖注入提供。

一 单一职责原则(SRP)

1.1 单一职责原则

  • 单一职责原则(Single Responsibility Principle)是面向对象设计中 SOLID 原则 中的 S,其核心思想是:**一个类或方法应该只有一个引起它变化的原因。**换句话说:一个类只做一件事,职责单一。

  • 为什么需要单一职责原则?
  1. 提高代码可维护性:职责清晰,修改时定位更准确。
  2. 增强代码复用性:单一功能模块更容易被其他模块调用。
  3. 降低耦合度:各司其职,减少类之间的相互影响。
  4. 便于测试和调试:功能集中,单元测试更容易覆盖。

单一职责 ≠ 一个类一个方法,而是让一个类只干一件事,并把它做好。

关键词 说明
内聚性 类内部方法围绕一个中心职责
可维护性 修改一处不会影响其他功能
扩展性 新需求可通过新增类实现,而非修改旧类

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,比如 ServiceRepositoryController 分离。

二 开闭原则(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,分别实现RectangleSquare,它们都实现自己的行为。
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 深入理解接口隔离原则

  1. 面向接口编程:ISP强调接口设计要简洁、专注。接口是定义行为的契约,不应该臃肿。
  2. 提高灵活性和可扩展性:拆分接口后,新增功能只需新增接口,不必修改已有接口和类,符合开闭原则。
  3. 减少耦合:客户端只依赖相关接口,避免因接口变更影响无关代码。
  4. 配合依赖倒置原则: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接口,而非具体实现。
  • 可以随意切换LightFan,无需修改Switch类。

5.3 深入理解:依赖倒置的两个原则

DIP实际上包含两个方面:

  1. 高层模块不依赖于低层模块。这意味着高层模块(如业务逻辑)只依赖抽象。
  2. 抽象不依赖于细节,细节依赖于抽象。这意味着具体实现(细节)依赖于抽象。

简要总结:

  • 面向接口编程:通过接口或抽象类依赖。
  • 依赖注入(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)可能增加学习成本。
  • 设计平衡:应结合实际需求合理应用。

相关推荐
yuren_xia3 分钟前
Java 公平锁与非公平锁详解
java·网络
YuforiaCode4 分钟前
苍穹外卖-2025 完成基础配置环节(详细图解)
java·intellij-idea·苍穹外卖
山海上的风23 分钟前
23种设计模式--简单工厂模式理解版
java·开发语言·简单工厂模式
像污秽一样30 分钟前
软件开发新技术复习
java·spring boot·后端·rabbitmq·cloud
浮游本尊35 分钟前
Java学习第7天 - 网络编程与数据库操作
java
武昌库里写JAVA36 分钟前
关于springcloud的坑
java·开发语言·spring boot·学习·课程设计
?abc!1 小时前
(哈希)128. 最长连续序列
算法·leetcode·哈希算法
yihuiComeOn1 小时前
【大数据高并发核心场景实战】 - 数据持久化之冷热分离
java·后端
Zephyrtoria1 小时前
动态规划:01 背包(闫氏DP分析法)
java·算法·动态规划