详细介绍设计模式七大原则

1. 概述

设计模式的七大原则旨在提高软件的可维护性、可复用性和可扩展性,包括:

  1. 单一职责原则:一个类应该只有一个引起它变化的原因。
  2. 开闭原则:软件实体应对扩展开放,对修改封闭。
  3. 里氏替换原则:子类型必须能够替换掉它们的基类型。
  4. 依赖倒置原则:高层模块不应依赖于低层模块,两者都应依赖于抽象;抽象不应依赖于细节,细节应依赖于抽象。
  5. 接口隔离原则:使用多个专门的接口比使用单一的总接口更好。
  6. 合成/聚合复用原则:尽量使用对象的组合/聚合,而不是继承关系达到复用的目的。
  7. 迪米特法则(最少知道原则):一个对象应对其他对象有尽可能少的了解。

这些原则指导开发者设计出更加健壯、灵活、易于维护的软件系统。

2. 单一职责原则

单一职责原则(Single Responsibility Principle, SRP)指一个类应该仅有一个引起它变化的原因。这意味着一个类应该只负责一项职责。
通俗地讲,单一职责原则就像是说,一个人只应该有一个工作。想象你有一个朋友,他既是厨师也是司机。如果有一天他因为烹饪而分心,导致开车出事了,那就是因为他承担了太多的责任。在编程中,如果一个类同时负责多件事情(比如,既存储数据又显示数据),那么当其中一部分需要改变时,很容易影响到其他部分。遵循单一职责原则,意味着每个类只负责一件事情,这样当需求变化时,只需修改有限的部分,减少错误,使代码更容易维护和理解。

反面案例(违反SRP)

java 复制代码
class User {
    private String userName;
    private String userEmail;

    public void saveUser(User user) {
        // 保存用户信息到数据库
    }

    public void printUserDetails() {
        // 打印用户详情
    }
}

这个User类违反了单一职责原则,因为它既处理用户信息的保存,又处理用户信息的打印,这两个职责应该由不同的类来承担。

正面案例(遵循SRP)

为了遵守单一职责原则,我们可以将上述User类分解为两个类:一个负责用户数据的管理,另一个负责用户数据的显示。

java 复制代码
class User {
    private String userName;
    private String userEmail;

    // 仅包含用户数据和基本操作
}

class UserPersistence {
    public void saveUser(User user) {
        // 保存用户信息到数据库
    }
}

class UserDisplay {
    public void printUserDetails(User user) {
        // 打印用户详情
    }
}

通过这种方式,我们将用户信息的保存和显示分别封装到了UserPersistenceUserDisplay类中,每个类都只负责单一的功能,从而遵循了单一职责原则。

Java生态系统中单一职责原则的体现可以在其标准库中找到许多例子,例如:

  • java.io包 :专门用于输入输出操作,其中的类如FileReaderBufferedReader等都专注于单一功能。
  • java.util包 :提供了一系列工具类和接口,如ListMapSet接口分别专注于不同类型的集合操作。
  • javax.servlet.http.HttpServlet :在开发Web应用时,这个类允许你通过重写doGetdoPost方法来处理HTTP GET或POST请求,而不必同时处理两者,遵循了单一职责。

这些类和接口的设计遵循了单一职责原则,每个类或接口都专注于一组特定的功能,使得Java生态系统更加模块化和易于维护。

3. 开闭原则

开闭原则(Open-Closed Principle, OCP)是面向对象设计的核心原则之一,它指出软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着在不修改现有代码的情况下,应该能够添加新功能。

开闭原则就像是给你的应用程序装上了一个"扩展插槽",让你可以随时增加新功能而不需要打开机器去重新焊接电路板。想象一下,如果你有一个游戏机,每当出现新游戏时,你不需要更换游戏机内部的硬件就能玩,只需要购买新的游戏卡带插上去即可。这样,游戏机的设计就允许了扩展(新增游戏),而不需要修改(打开游戏机更换部件),这正是开闭原则的精髓。

反面案例

假设我们有一个根据用户类型计算折扣的系统:

java 复制代码
class DiscountCalculator {
    public double calculateDiscount(String userType) {
        if ("VIP".equals(userType)) {
            return 0.2;
        } else {
            return 0.1;
        }
    }
}

如果需要添加新的用户类型和折扣规则,我们必须修改DiscountCalculator类,违反了开闭原则。

正面案例

为了遵循开闭原则,我们可以定义一个接口和多个实现该接口的类,每个类对应一种用户类型的折扣计算方式:

java 复制代码
interface DiscountStrategy {
    double calculateDiscount();
}

class VIPDiscount implements DiscountStrategy {
    public double calculateDiscount() {
        return 0.2;
    }
}

class RegularDiscount implements DiscountStrategy {
    public double calculateDiscount() {
        return 0.1;
    }
}

class DiscountCalculator {
    public double calculateDiscount(DiscountStrategy strategy) {
        return strategy.calculateDiscount();
    }
}

这样,当需要添加新的用户类型时,只需添加新的DiscountStrategy实现类,无需修改现有的DiscountCalculator类,从而遵循了开闭原则。

4. 里氏替换原则

里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的一个原则,它指出如果类A是类B的一个子类型,那么程序中使用类B的地方就可以不改变程序的行为的前提下,用类A来替换类B。

里氏替换原则就像是说,如果你有一个苹果,那么任何地方需要苹果的地方,你都可以用一个红苹果或一个绿苹果来替代,而不会影响到使用它的地方的正常运作。在编程中,如果你用一个子类对象替换了一个父类对象,那么程序还应该能够像原来一样运行,而不会出现错误或者异常行为。这就要求子类在继承父类的时候,不仅要继承父类的特性,还要保证这些特性的行为不被改变。

反面案例

假设我们有一个矩形类和一个正方形类,正方形继承自矩形:

java 复制代码
class Rectangle {
    protected int width, height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

这里,Square违反了LSP,因为它改变了Rectangle的行为,使得设置宽度或高度的操作同时改变了另一方。

正面案例

为了遵循LSP,我们可以抽象出一个共同的基类或接口,然后让RectangleSquare分别实现它们自己的行为:

java 复制代码
interface Shape {
    int getArea();
}

class Rectangle implements Shape {
    protected int width, height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public int getArea() {
        return width * height;
    }
}

class Square implements Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

在这个例子中,RectangleSquare都遵循LSP,因为它们都独立实现了Shape接口,且使用它们的代码不需要知道具体是哪一个形状类,从而保证了类型的替换不会导致程序行为的改变。

5.依赖倒置原则

依赖倒置原则(Dependency Inversion Principle, DIP)指的是高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。这个原则的核心在于促进系统的解耦,从而使得系统更易于扩展和维护。

依赖倒置原则(Dependency Inversion Principle, DIP)的核心思想是高层模块不应该依赖低层模块,它们都应该依赖于抽象;抽象不应该依赖细节,细节应该依赖抽象。用通俗的语言来说,就像是建筑的设计不应该基于具体的砖块类型,而是基于砖块的一般特性。这样,无论使用什么样的砖块,只要符合这些特性,就能构建出建筑。在编程中,这意味着我们的代码应该依赖于接口或抽象类,而不是具体的实现类,这样可以使代码更灵活、更易于维护和扩展。

反面案例

假设我们有一个LightBulb类和一个ElectricPowerSwitch类,后者依赖于前者:

java 复制代码
class LightBulb {
    public void turnOn() {
        // 实现开灯
    }

    public void turnOff() {
        // 实现关灯
    }
}

class ElectricPowerSwitch {
    private LightBulb lightBulb;

    public ElectricPowerSwitch(LightBulb lightBulb) {
        this.lightBulb = lightBulb;
    }

    public void press() {
        // 实现开关灯
    }
}

这个例子违反了DIP,因为ElectricPowerSwitch直接依赖于LightBulb的具体实现。

正面案例

为了遵循DIP,我们可以引入一个抽象的接口,使得高层和低层模块都依赖于这个接口:

java 复制代码
interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    public void turnOn() {
        // 实现开灯
    }

    public void turnOff() {
        // 实现关灯
    }
}

class ElectricPowerSwitch {
    private Switchable device;

    public ElectricPowerSwitch(Switchable device) {
        this.device = device;
    }

    public void press() {
        // 实现控制任何Switchable设备
    }
}

在这个例子中,通过引入Switchable接口,ElectricPowerSwitch不再依赖于LightBulb的具体实现,而是依赖于抽象。这样,我们可以轻松地用另一个实现了Switchable接口的类来替换LightBulb,比如风扇,而不需要修改ElectricPowerSwitch类的代码。

6.接口隔离原则

接口隔离原则(Interface Segregation Principle, ISP)强调不应该强迫客户依赖于它们不使用的接口。换句话说,更倾向于创建专门的接口而不是一个大而全的接口。

接口隔离原则(Interface Segregation Principle, ISP)讲的是不应该强迫客户依赖于它们不用的接口。用一个简单的例子来说,如果有一个多功能打印机,它可以打印、扫描和复印。根据接口隔离原则,我们不应该只有一个接口包含所有这些功能,因为不是每个使用打印机的人都需要扫描和复印的功能。相反,应该为打印、扫描和复印各自提供独立的接口。这样,只需要打印功能的用户就不必实现或依赖于扫描和复印的接口了。简而言之,接口隔离原则就是让接口更小、更专注,避免一个庞大的接口承担太多的职责。

反面案例

假设我们有一个接口包含了太多的方法,客户类必须实现它们即使不需要所有的方法:

java 复制代码
interface Worker {
    void work();
    void eat();
}

class HumanWorker implements Worker {
    public void work() {
        // 实现工作
    }

    public void eat() {
        // 实现吃饭
    }
}

class RobotWorker implements Worker {
    public void work() {
        // 实现工作
    }

    public void eat() {
        // 机器人不需要吃饭,但依然需要实现该方法
    }
}

正面案例

遵循接口隔离原则,我们应该将Worker接口拆分为更小的、更专门的接口:

java 复制代码
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class HumanWorker implements Workable, Eatable {
    public void work() {
        // 实现工作
    }

    public void eat() {
        // 实现吃饭
    }
}

class RobotWorker implements Workable {
    public void work() {
        // 实现工作
    }
    // 机器人不需要实现Eatable接口
}

这样,每个类只需实现它实际需要的接口,避免了不必要的依赖。

7. 合成/聚合复用原则

合成/聚合复用原则(Composition/Aggregation Reuse Principle, CARP)强调使用对象的组合或聚合来实现新功能,而不是通过继承关系。这样做可以提高代码的灵活性和复用性。

合成/聚合复用原则就像是搭积木。想象你正在建造一个小屋,你可以选择用预制的部分(比如窗户、门等)来组合成你想要的结构,而不是自己从头开始制造每一个部分。在编程中,这个原则告诉我们应该通过将现有的对象(积木块)组合起来来创建新的功能,而不是通过继承一个大而全的类(从零开始造一个整体)。这样做使得代码更加灵活,因为你可以随时替换或者重新组合这些"积木块",而不是被固定在一种设计之中。

合成/聚合复用原则鼓励使用对象的组合或聚合来实现功能的复用,而不是通过继承。这是因为继承会导致强耦合关系,使得父类和子类之间的依赖关系过于紧密,这样一来,修改父类可能会影响到所有的子类,增加了代码的复杂性和维护难度。相比之下,组合或聚合能够提供更加灵活的复用机制,对象之间的关系更加松散,修改一个对象不会直接影响到使用它的其他对象,这样有助于降低系统的耦合度,提高代码的可维护性和可扩展性。

反面案例

通过继承实现复用:

java 复制代码
class Vehicle {
    void startEngine() {
        // 启动引擎的代码
    }
}

class Car extends Vehicle {
    // Car 类继承了 Vehicle 类,复用了 startEngine 方法
}

这种方式使Car类与Vehicle类紧密耦合,限制了灵活性。

正面案例

使用组合来实现复用:

java 复制代码
class Engine {
    void start() {
        // 启动引擎的代码
    }
}

class Car {
    private Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }

    void startEngine() {
        engine.start();
    }
}

在这个例子中,Car类通过包含Engine类的实例(组合)来复用启动引擎的功能,而不是继承Vehicle类。这样Car类就可以更灵活地复用其他类的功能,同时也降低了类之间的耦合度。

8. 迪米特法则

迪米特法则(Law of Demeter, LoD),也称为最少知识原则,是一种软件开发的设计指导原则。它强调,一个对象应该对其他对象有尽可能少的了解,只与直接的朋友通信。直接的朋友指的是成员变量、方法参数或者对象创建的实例。

迪米特法则就像是说,一个人应该尽可能少地知道其他人的私事,只和直接的朋友交流。在编程中,这意味着一个类不应该知道太多其他类的细节,只和直接相关的类交互。这样做可以减少系统中的耦合,使得修改一个部分的时候,不会影响到太多其他部分,保持代码的整洁和可维护性。

反面案例

java 复制代码
class Paper {
    public String getContent() {
        return "content";
    }
}

class Printer {
    public void printPaper(Paper paper) {
        System.out.println(paper.getContent());
    }
}

class User {
    void print() {
        Printer printer = new Printer();
        Paper paper = new Paper();
        printer.printPaper(paper); // User 类直接与 Paper 类交互
    }
}

在这个例子中,User 类需要知道Paper 类的细节才能打印内容,违反了迪米特法则。

正面案例

java 复制代码
class Paper {
    private String content = "content";

    public void printContent() {
        System.out.println(content);
    }
}

class Printer {
    public void printPaper(Paper paper) {
        paper.printContent();
    }
}

class User {
    void print() {
        Printer printer = new Printer();
        Paper paper = new Paper();
        printer.printPaper(paper); // User 类不需要直接了解 Paper 类的内容
    }
}

在这个改进后的例子中,User 类通过Printer 类来打印文档,而不需要直接与Paper 类交互。User 类仅与Printer 类有直接关系,遵循了迪米特法则。

9. 附加知识

9.1 聚合、组合、继承的区别

聚合(Aggregation)表示一种弱"拥有"关系,对象间是整体与部分的关系,但部分可以离开整体而单独存在。

组合(Composition)表示一种强"拥有"关系,对象间也是整体与部分的关系,但部分不能离开整体而单独存在。

继承(Inheritance)表示一种"是"关系,用于表示一种类型是另一种类型的特化。

想象一下:

  • 聚合就像是一支球队和球员的关系。球队由球员组成,但是球员可以离开球队,加入其他球队,球队和球员都可以独立存在。

  • 组合就像是鸟和翅膀的关系。翅膀是鸟的一部分,翅膀不能脱离鸟单独存在。如果鸟不存在了,翅膀也就不存在了。

  • 继承就像是孩子和父母的关系。孩子从父母那里继承了特征(比如眼睛的颜色,头发的类型)。孩子是父母的一个特殊版本,拥有父母的一些特性,同时也可能有自己的一些特性。

代码示例
java 复制代码
// 聚合示例
class Engine { }
class Car {
    private Engine engine; // 聚合关系,Car有一个Engine,但Engine可以独立于Car存在
    Car(Engine engine) {
        this.engine = engine;
    }
}

// 组合示例
class Room { }
class House {
    private List<Room> rooms = new ArrayList<>(); // 组合关系,Room是House的一部分,不能独立存在
}

// 继承示例
class Vehicle {
    void start() { }
}
class Bicycle extends Vehicle { // 继承关系,Bicycle是Vehicle的一种
}

在这些例子中,CarEngine之间的聚合关系允许Engine脱离Car独立存在;HouseRoom之间的组合关系表明Room不能脱离House独立存在;Bicycle继承自Vehicle,表明每个Bicycle也是一个Vehicle

9.2 解读:抽象不应依赖于细节,细节应依赖于抽象

"抽象不应依赖于细节,细节应依赖于抽象"是依赖倒置原则的核心思想。这句话意味着在设计软件时,高层模块(定义应用程序的核心行为)不应该依赖于低层模块(具体实现细节),而是两者都应该依赖于抽象(接口或抽象类)。同样,这些抽象不应该依赖于具体的实现细节,而具体的实现细节应该依赖于抽象。

遵循"抽象不应依赖于细节,细节应依赖于抽象"的原则具有重要意义,因为它促进了代码的灵活性和可维护性。这样做使得代码对改变更加开放:当系统的具体实现需要变化时,不需要对依赖于抽象的高层模块进行大幅度修改。这降低了代码间的耦合度,使得各个部分更容易理解、测试和重用。简而言之,这个原则帮助开发者构建出易于扩展和修改的系统,减少了未来可能的工作量和复杂性。

想象一下,你正在建造一栋房子。根据"抽象不应依赖细节,细节应依赖于抽象"的原则,你不应该从门把手或砖块开始规划整栋房子;相反,你应该从房子的设计图纸开始,这些图纸定义了房子的结构和它应该如何建造。

在编程中,这意味着你的代码(即房子的设计)不应该依赖于具体的实现(即门把手和砖块),而应该依赖于接口或抽象类(即图纸)。这样,如果你决定更换门把手或砖块(即具体的实现),你的代码仍然可以工作,因为它依赖的是抽象,而不是具体的细节。

java 复制代码
// 抽象
interface Switchable {
    void turnOn();
    void turnOff();
}

// 细节
class LightBulb implements Switchable {
    public void turnOn() { /* 实现开灯 */ }
    public void turnOff() { /* 实现关灯 */ }
}

// 高层模块
class ElectricPowerSwitch {
    private Switchable device; // 依赖于抽象,而不是具体细节
    public ElectricPowerSwitch(Switchable device) {
        this.device = device;
    }
    public void press() {
        /* 使用 device,而不关心它是怎么实现的 */
    }
}

在这个例子中,无论Switchable背后是LightBulb还是其他任何实现Switchable的设备,ElectricPowerSwitch都可以工作,因为它依赖于抽象而不是具体的细节。

相关推荐
杨充16 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充17 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充17 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充17 小时前
2.面向对象的特性
设计模式
杨充17 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充17 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充17 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯20 小时前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯1 天前
GoF设计模式——解释器模式
java·后端·spring·设计模式