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

1. 概述

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

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

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

2. 单一职责原则

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

反面案例(违反SRP)

class User {
    private String userName;
    private String userEmail;

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

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

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

正面案例(遵循SRP)

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

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

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

反面案例

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

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

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

正面案例

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

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。

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

反面案例

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

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分别实现它们自己的行为:

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类,后者依赖于前者:

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,我们可以引入一个抽象的接口,使得高层和低层模块都依赖于这个接口:

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

反面案例

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

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接口拆分为更小的、更专门的接口:

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)强调使用对象的组合或聚合来实现新功能,而不是通过继承关系。这样做可以提高代码的灵活性和复用性。

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

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

反面案例

通过继承实现复用:

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

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

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

正面案例

使用组合来实现复用:

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),也称为最少知识原则,是一种软件开发的设计指导原则。它强调,一个对象应该对其他对象有尽可能少的了解,只与直接的朋友通信。直接的朋友指的是成员变量、方法参数或者对象创建的实例。

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

反面案例

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 类的细节才能打印内容,违反了迪米特法则。

正面案例

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)表示一种“是”关系,用于表示一种类型是另一种类型的特化。

想象一下:

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

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

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

代码示例
// 聚合示例
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 解读:抽象不应依赖于细节,细节应依赖于抽象

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

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

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

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

// 抽象
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都可以工作,因为它依赖于抽象而不是具体的细节。

你可能感兴趣的:(设计模式,设计模式,设计模式七大原则)