设计模式(二)

发布于:2025-06-28 ⋅ 阅读:(18) ⋅ 点赞:(0)

迪米特法则(最少知识原则):定义、核心思想与实践解析

一、迪米特法则(LoD)的核心定义

迪米特法则(Law of Demeter, LoD),又称 “最少知识原则(Least Knowledge Principle)”,是面向对象设计的经典指导原则之一。其核心思想是:一个对象应当尽可能少地与其他对象发生相互作用,只与 “直接的朋友” 通信,避免与 “陌生人” 产生直接交互。

二、关键概念:“直接的朋友” 与 “陌生人”
  • 直接的朋友:指与当前对象有直接关联的类,包括:

    1. 当前对象的成员变量(属性)所引用的对象;
    2. 方法的参数所引用的对象;
    3. 方法的返回值所引用的对象;
    4. 方法内部创建的对象(部分场景下需谨慎判断)。
  • 陌生人:与当前对象无直接关联的类。例如,若类 A 通过类 B 的成员变量访问类 C,则类 C 是类 A 的 “陌生人”。

三、对用户提供语句的深度解析

用户提到的语句:“如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。” 这直接点明了迪米特法则的核心设计逻辑:

1. 避免不必要的直接交互

两个类若无需直接通信(即无业务逻辑上的直接关联),则不应建立直接依赖。直接交互会导致类之间的耦合度升高,当其中一个类的实现变化时,可能引发连锁反应(如修改类 C 会影响类 A)。

2. 通过 “第三者” 转发调用

若必须调用另一个类的方法,应通过一个中间类(第三者)间接转发。中间类作为 “桥梁”,将两个原本无关的类解耦,使它们仅依赖中间类,而非彼此。

四、违反迪米特法则的典型问题

以 “客户 - 员工 - 项目” 管理系统为例:

反例:直接与 “陌生人” 交互
// 客户类(直接与项目类交互,违反LoD)
class Client {
public:
    void checkProjectProgress(Employee* employee) {
        // 客户直接访问员工的项目对象(陌生人)
        Project* project = employee->getProject();
        project->showProgress();  // Client与Project无直接关联,属于陌生人
    }
};

// 员工类
class Employee {
private:
    Project* project;  // 员工的项目(直接朋友)
public:
    Project* getProject() { return project; }
};

// 项目类
class Project {
public:
    void showProgress() { std::cout << "项目进度:80%" << std::endl; }
};

问题

  • Client类直接调用Project类的方法,但ClientProject无直接关联(ProjectClient的 “陌生人”)。
  • Project类修改showProgress()方法(如重命名为displayProgress()),Client类需同步修改,耦合度高。
正例:通过 “第三者” 转发调用(符合 LoD)
// 客户类(仅与直接朋友Employee交互)
class Client {
public:
    void checkProjectProgress(Employee* employee) {
        employee->reportProjectProgress();  // 通过Employee转发调用(第三者)
    }
};

// 员工类(作为第三者,封装Project的访问)
class Employee {
private:
    Project* project;  // 直接朋友
public:
    // 员工负责转发项目进度查询(避免Client直接访问Project)
    void reportProjectProgress() {
        project->showProgress();  // 员工与Project是直接朋友
    }
};

// 项目类(无需暴露给Client)
class Project {
public:
    void showProgress() { std::cout << "项目进度:80%" << std::endl; }
};
五、迪米特法则的设计目标
  1. 降低类间耦合:通过限制直接交互的范围,减少修改一个类对其他类的影响。
  2. 提高模块独立性:每个类仅关注自身职责,符合 “单一职责原则”。
  3. 增强可维护性:修改或扩展功能时,仅需调整少数相关类,而非全局改动。
六、迪米特法则的实践注意事项
  1. 避免过度中介:引入过多中间类可能导致系统复杂度上升(如 “上帝类”),需权衡 “解耦” 与 “复杂度”。
  2. 明确 “直接朋友” 边界:需根据业务逻辑判断哪些类是 “直接朋友”。例如,若Client的核心职责是查询项目进度,Employee作为项目的负责人,是合理的 “直接朋友”。
  3. 接口隔离:通过抽象接口(如IProjectReporter)定义转发方法,避免中间类与具体实现强耦合。
七、总结

迪米特法则的本质是通过限制对象间的交互范围,降低系统耦合度。用户提到的语句精准概括了这一原则:

  • 不直接与 “陌生人” 通信,避免不必要的依赖;
  • 通过 “第三者” 转发调用,将交互限制在 “直接朋友” 范围内。

在实际开发中,遵循迪米特法则能显著提升代码的可维护性和扩展性,尤其在大型系统中(如微服务架构、模块化设计),是降低模块间耦合的关键指导原则。

依赖倒转原则(Dependency Inversion Principle, DIP)详解

一、核心概念

依赖倒转原则是面向对象设计的重要原则,由 Robert C. Martin( Uncle Bob)提出,是 SOLID 原则的一部分。其核心思想是:

  1. 高层模块不应该依赖低层模块,两者都应该依赖抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象

简而言之,依赖关系应通过抽象(接口 / 抽象类)建立,而非具体实现类。这使系统更灵活、可扩展,符合 “开闭原则”。

二、核心角色
  1. 高层模块:负责业务逻辑的模块(如订单服务、用户管理)。
  2. 低层模块:具体实现细节(如数据库操作、文件存储)。
  3. 抽象接口:定义高层模块和低层模块共同遵循的契约。
三、违反 DIP 的典型问题
反例:高层模块直接依赖低层模块
// 低层模块:MySQL数据库实现
class MySQLDatabase {
public:
    void saveOrder(const std::string& orderData) {
        std::cout << "将订单数据 [" << orderData << "] 存入MySQL数据库" << std::endl;
    }
};

// 高层模块:订单服务(直接依赖具体数据库实现)
class OrderService {
private:
    MySQLDatabase database;  // 直接依赖具体类

public:
    void createOrder(const std::string& orderData) {
        // 业务逻辑...
        database.saveOrder(orderData);  // 直接调用具体实现
    }
};

问题

  • OrderService(高层)直接依赖MySQLDatabase(低层),若需切换数据库(如改用 Redis),必须修改OrderService代码,违反开闭原则。
  • 无法在测试时使用模拟数据库,单元测试困难。
四、符合 DIP 的实现
正例:通过接口解耦高层与低层
// 抽象接口:定义数据库操作契约
class Database {
public:
    virtual void saveOrder(const std::string& orderData) = 0;
    virtual ~Database() = default;
};

// 低层模块:MySQL实现(依赖抽象)
class MySQLDatabase : public Database {
public:
    void saveOrder(const std::string& orderData) override {
        std::cout << "将订单数据 [" << orderData << "] 存入MySQL数据库" << std::endl;
    }
};

// 低层模块:Redis实现(依赖抽象)
class RedisDatabase : public Database {
public:
    void saveOrder(const std::string& orderData) override {
        std::cout << "将订单数据 [" << orderData << "] 缓存到Redis" << std::endl;
    }
};

// 高层模块:订单服务(依赖抽象)
class OrderService {
private:
    Database* database;  // 依赖抽象接口,而非具体类

public:
    // 通过构造函数注入依赖(依赖注入)
    explicit OrderService(Database* db) : database(db) {}

    void createOrder(const std::string& orderData) {
        // 业务逻辑...
        database->saveOrder(orderData);  // 通过接口调用,不关心具体实现
    }
};

// 客户端代码:控制反转(IoC)
int main() {
    // 选择具体实现并注入到高层模块
    Database* mysqlDb = new MySQLDatabase();
    OrderService service(mysqlDb);
    service.createOrder("订单#123");  // 输出:存入MySQL数据库

    // 动态切换实现(无需修改OrderService)
    delete mysqlDb;
    Database* redisDb = new RedisDatabase();
    OrderService service2(redisDb);
    service2.createOrder("订单#456");  // 输出:缓存到Redis

    delete redisDb;
    return 0;
}
五、关键设计模式
  1. 依赖注入(Dependency Injection, DI)

    • 通过构造函数、Setter 方法或接口注入依赖对象,而非在类内部创建。
    • 示例中的OrderService通过构造函数接收Database接口实现。
  2. 控制反转(Inversion of Control, IoC)

    • 将对象创建和依赖关系的控制权从类内部转移到外部(如客户端或容器)。
    • 示例中main()函数负责创建具体数据库实例并注入到OrderService
六、依赖倒转的优势
优势 说明
可维护性 修改低层实现(如数据库)时,无需改动高层模块(如业务逻辑)。
可扩展性 新增低层实现(如添加 MongoDB 支持)时,只需实现接口,高层模块无感知。
可测试性 测试时可注入模拟对象(如MockDatabase),隔离外部依赖,便于单元测试。
松耦合 高层与低层仅通过抽象接口交互,耦合度降至最低。
七、实践注意事项
  1. 抽象粒度

    • 接口应专注于业务契约(如saveOrder()),而非实现细节。
    • 避免设计 “胖接口”(包含过多方法),遵循接口隔离原则。
  2. 避免循环依赖

    • 若 A 依赖 B 的抽象,B 依赖 A 的抽象,可能导致循环依赖。可通过中间抽象层或事件机制解耦。
  3. 结合工厂模式

    • 复杂场景下,可通过工厂类创建具体实现,进一步隔离高层模块与具体类的依赖。
八、总结

依赖倒转原则的核心是通过抽象隔离高层与低层模块,使系统更灵活、可维护。关键点:

  1. 高层模块和低层模块均依赖抽象接口;
  2. 通过依赖注入和控制反转实现对象间的解耦;
  3. 抽象接口应反映业务需求,而非具体实现。

在大型系统(如微服务架构)中,DIP 是实现 “高内聚、低耦合” 的基石,广泛应用于框架设计(如 Spring)和模块化开发中。

外观模式(Facade Pattern):概念、实现与应用场景

一、外观模式的核心概念

外观模式是一种结构型设计模式,其核心思想是为复杂的子系统提供一个统一的简单接口,隐藏子系统的复杂性,使客户端只需通过这个接口与子系统交互,而无需直接调用多个子系统的复杂方法。

二、核心角色

  1. 外观类(Facade)

    • 提供简化的接口,封装子系统的复杂操作。
    • 协调子系统的调用顺序和交互逻辑。
  2. 子系统类(Subsystem Classes)

    • 实现具体功能的类,如数据库操作、文件处理、网络请求等。
    • 不依赖外观类,可独立存在和使用。
  3. 客户端(Client)

    • 通过外观类间接调用子系统功能,无需了解内部细节。

三、代码示例

场景:计算机启动过程涉及 CPU、内存、硬盘等多个子系统的复杂初始化,通过外观模式简化启动流程。

步骤 1:定义子系统类

// 子系统:CPU
class CPU {
public:
    void startup() {
        std::cout << "CPU 启动..." << std::endl;
    }

    void shutdown() {
        std::cout << "CPU 关闭..." << std::endl;
    }
};

// 子系统:内存
class Memory {
public:
    void startup() {
        std::cout << "内存自检并加载..." << std::endl;
    }

    void shutdown() {
        std::cout << "内存清理并释放..." << std::endl;
    }
};

// 子系统:硬盘
class HardDrive {
public:
    void startup() {
        std::cout << "硬盘初始化..." << std::endl;
    }

    void shutdown() {
        std::cout << "硬盘停止读写..." << std::endl;
    }
};

步骤 2:创建外观类

// 外观类:封装计算机启动和关闭的复杂流程
class ComputerFacade {
private:
    CPU cpu;
    Memory memory;
    HardDrive hardDrive;

public:
    // 简化的启动接口
    void start() {
        std::cout << "=== 计算机启动流程 ===" << std::endl;
        cpu.startup();
        memory.startup();
        hardDrive.startup();
        std::cout << "=== 计算机启动完成 ===" << std::endl;
    }

    // 简化的关闭接口
    void shutdown() {
        std::cout << "=== 计算机关闭流程 ===" << std::endl;
        cpu.shutdown();
        memory.shutdown();
        hardDrive.shutdown();
        std::cout << "=== 计算机已关闭 ===" << std::endl;
    }
};

步骤 3:客户端代码

int main() {
    // 创建外观对象
    ComputerFacade computer;

    // 客户端只需调用简单接口,无需关心内部子系统的复杂交互
    std::cout << "用户按下电源键..." << std::endl;
    computer.start();  // 调用简化的启动接口

    std::cout << "\n用户按下关机键..." << std::endl;
    computer.shutdown();  // 调用简化的关闭接口

    return 0;
}

四、外观模式的优势

优势 说明
简化接口 隐藏子系统的复杂性,提供统一的简单接口,降低客户端使用难度。
松耦合 客户端与子系统解耦,子系统的修改不影响客户端,符合开闭原则。
提高安全性 限制客户端直接访问子系统,减少误操作风险。
可维护性增强 子系统的内部变化只需在外观类中调整,不影响其他部分。

五、外观模式的适用场景

  1. 复杂子系统的简化访问

    • 当子系统包含多个模块且交互复杂时,如操作系统、框架内部组件。
  2. 分层架构的边界定义

    • 在系统分层设计中,每层通过外观类对外提供统一接口,隔离层与层之间的依赖。
  3. 遗留系统的适配

    • 为旧系统提供新的简化接口,便于新系统集成或维护。

六、注意事项

  1. 避免过度抽象

    • 外观类应只封装真正必要的功能,避免成为 “上帝类”(包含所有子系统的方法)。
  2. 保留直接访问的可能性

    • 外观类不禁止客户端直接调用子系统,若需要更细粒度的控制,客户端仍可绕过外观类。
  3. 与中介者模式的区别

    • 外观模式:单向简化,子系统间无直接交互,依赖外观类。
    • 中介者模式:双向协调,子系统间通过中介者间接交互,减少子系统间的直接依赖。

七、总结

外观模式的核心价值在于通过一个统一的接口封装复杂子系统,使客户端无需了解内部细节,降低系统的使用门槛和耦合度。它是 “封装变化” 和 “依赖倒转” 原则的典型应用,广泛用于框架设计(如 Java 的java.net.URL类封装网络请求)、游戏引擎(如 Unity 的 API 封装底层渲染逻辑)和企业系统(如 ERP 系统的业务门面层)。

建造者模式(Builder Pattern):概念、实现与应用场景

一、建造者模式的核心概念

建造者模式是一种创建型设计模式,其核心思想是将一个复杂对象的构建与表示分离,使同样的构建过程可以创建不同的表示。它允许用户通过指定复杂对象的类型和内容,一步一步创建出一个完整的对象,而无需关心对象内部的具体构造细节。

二、核心角色
  1. 产品类(Product)

    • 要创建的复杂对象,包含多个组成部分(如属性、子对象)。
  2. 抽象建造者(Builder)

    • 定义创建产品各个部分的抽象方法(如buildPartA()buildPartB())。
    • 通常包含返回最终产品的方法(如getProduct())。
  3. 具体建造者(Concrete Builder)

    • 实现抽象建造者的方法,完成产品各部分的具体构造。
    • 负责组装产品的各个部分。
  4. 指挥者(Director)

    • 负责安排复杂对象的建造顺序。
    • 通过调用建造者的方法来构建产品,不直接与产品类交互。
三、代码示例

场景:构建电脑(Computer),包含 CPU、内存、硬盘等组件,通过建造者模式实现不同配置的电脑组装。

步骤 1:定义产品类

// 产品类:电脑
class Computer {
private:
    std::string cpu;     // CPU型号
    std::string memory;  // 内存容量
    std::string hardDisk;// 硬盘容量
    std::string graphics;// 显卡型号(可选)

public:
    void setCPU(const std::string& cpu) {
        this->cpu = cpu;
    }

    void setMemory(const std::string& memory) {
        this->memory = memory;
    }

    void setHardDisk(const std::string& hardDisk) {
        this->hardDisk = hardDisk;
    }

    void setGraphics(const std::string& graphics) {
        this->graphics = graphics;
    }

    void showInfo() const {
        std::cout << "电脑配置:" << std::endl
                  << "CPU: " << cpu << std::endl
                  << "内存: " << memory << std::endl
                  << "硬盘: " << hardDisk << std::endl;
        if (!graphics.empty()) {
            std::cout << "显卡: " << graphics << std::endl;
        }
    }
};

步骤 2:定义抽象建造者

// 抽象建造者:定义构建电脑各部分的接口
class ComputerBuilder {
protected:
    Computer computer;

public:
    virtual void buildCPU() = 0;
    virtual void buildMemory() = 0;
    virtual void buildHardDisk() = 0;
    virtual void buildGraphics() = 0;  // 可选组件

    // 返回构建好的电脑
    Computer getComputer() {
        return computer;
    }

    virtual ~ComputerBuilder() = default;
};

步骤 3:实现具体建造者

// 具体建造者:标准办公电脑
class OfficeComputerBuilder : public ComputerBuilder {
public:
    void buildCPU() override {
        computer.setCPU("Intel i5-12400");
    }

    void buildMemory() override {
        computer.setMemory("16GB DDR4");
    }

    void buildHardDisk() override {
        computer.setHardDisk("512GB SSD");
    }

    void buildGraphics() override {
        // 办公电脑不配置独立显卡
        computer.setGraphics("集成显卡");
    }
};

// 具体建造者:游戏电脑
class GamingComputerBuilder : public ComputerBuilder {
public:
    void buildCPU() override {
        computer.setCPU("AMD Ryzen 7 5800X");
    }

    void buildMemory() override {
        computer.setMemory("32GB DDR5");
    }

    void buildHardDisk() override {
        computer.setHardDisk("1TB NVMe SSD");
    }

    void buildGraphics() override {
        computer.setGraphics("NVIDIA RTX 4070");
    }
};

步骤 4:定义指挥者

// 指挥者:控制建造过程的顺序
class ComputerDirector {
private:
    ComputerBuilder* builder;

public:
    explicit ComputerDirector(ComputerBuilder* builder) : builder(builder) {}

    // 构建电脑的流程
    void constructComputer() {
        builder->buildCPU();
        builder->buildMemory();
        builder->buildHardDisk();
        builder->buildGraphics();
    }
};

步骤 5:客户端代码

int main() {
    // 构建办公电脑
    OfficeComputerBuilder officeBuilder;
    ComputerDirector officeDirector(&officeBuilder);
    officeDirector.constructComputer();
    Computer officeComputer = officeBuilder.getComputer();

    std::cout << "办公电脑配置:" << std::endl;
    officeComputer.showInfo();

    // 构建游戏电脑
    GamingComputerBuilder gamingBuilder;
    ComputerDirector gamingDirector(&gamingBuilder);
    gamingDirector.constructComputer();
    Computer gamingComputer = gamingBuilder.getComputer();

    std::cout << "\n游戏电脑配置:" << std::endl;
    gamingComputer.showInfo();

    return 0;
}
四、建造者模式的优势
优势 说明
分离构建与表示 客户端无需知道产品内部细节,只需指定类型和内容,构建过程由指挥者控制。
分步构建复杂对象 产品的构建过程被分解为多个步骤,允许更精细的控制和更灵活的扩展。
支持多种配置 同一构建过程可通过不同的具体建造者创建不同配置的产品。
符合开闭原则 新增具体建造者无需修改现有代码,扩展性好。
五、建造者模式的适用场景
  1. 复杂对象的构建

    • 当对象包含多个部分且构建过程复杂时,如汽车、房屋、文档等。
  2. 需要多种配置

    • 同一产品需要多种不同的配置组合,如游戏角色定制、电脑组装。
  3. 构建步骤稳定但具体实现可变

    • 产品的构建步骤固定,但每个步骤的具体实现可能不同。
六、与工厂模式的对比
维度 建造者模式 工厂模式
创建复杂度 适合构建复杂对象(多步骤) 适合创建简单对象(单一操作)
关注点 强调分步构建和配置的灵活性 强调对象创建的统一接口
返回结果 通常通过指挥者分步构建,最终返回完整对象 直接返回产品实例
应用场景 产品有多种配置组合(如电脑组装) 产品类型明确,创建逻辑集中管理
七、简化版建造者模式(省略指挥者)

在某些场景下,可省略指挥者角色,直接通过建造者链式调用构建方法。例如:

// 简化版建造者(省略指挥者)
class ComputerBuilder {
private:
    Computer computer;

public:
    ComputerBuilder& setCPU(const std::string& cpu) {
        computer.setCPU(cpu);
        return *this;
    }

    ComputerBuilder& setMemory(const std::string& memory) {
        computer.setMemory(memory);
        return *this;
    }

    // 其他setter方法...

    Computer build() {
        return computer;
    }
};

// 客户端使用
Computer customComputer = ComputerBuilder()
    .setCPU("AMD Ryzen 9")
    .setMemory("64GB")
    .build();
八、总结

建造者模式的核心价值在于将复杂对象的构建过程与表示分离,通过分步构建和多态实现,使同一构建流程可创建不同配置的产品。它在以下场景特别有用:

  • 需要创建复杂对象且构建过程包含多个步骤;
  • 需要支持同一产品的多种配置变体;
  • 希望客户端代码与产品内部结构解耦。

该模式广泛应用于框架设计(如 Java 的StringBuilder、Android 的AlertDialog.Builder)、游戏开发(角色创建系统)和企业系统(报表生成器)。


网站公告

今日签到

点亮在社区的每一天
去签到