9、设计模式与惯用法:构建高质量软件的关键

设计模式与惯用法:构建高质量软件的关键

1 设计原则与设计模式的区别

在软件开发中,设计原则和设计模式是两个核心概念。设计原则是指导软件设计的基本准则,它们通常独立于特定的编程范式和技术。例如,KISS(保持简单和直接)、DRY(不要重复自己)、YAGNI(你不会需要它)、单一职责原则、开闭原则和信息隐藏等原则,无论是在面向对象编程还是函数式编程中,都是适用的。这些原则帮助开发者编写简洁、可维护且易于扩展的代码。

设计模式则是针对特定上下文中具体设计问题的解决方案。它们通常与面向对象编程紧密相关,例如,《设计模式:可复用面向对象软件的基础》一书中描述的23种经典设计模式。设计模式提供了经过验证的解决方案,帮助开发者应对常见的设计挑战。设计原则更为持久且重要,因为它们是设计模式的基础。掌握这些原则后,开发者可以更好地理解和应用设计模式。

2 常用的设计模式及其应用场景

2.1 依赖注入 (Dependency Injection, DI)

依赖注入是一种解耦组件依赖关系的技术,使得组件不必直接创建或查找其依赖项。通过外部提供依赖项,组件可以专注于其核心功能,而不必关心依赖项的生命周期管理。DI的核心思想是将依赖项的创建和管理移交给外部容器,从而使代码更加模块化和易于测试。

实现依赖注入的方法
  1. 构造函数注入 :依赖项通过构造函数传递给组件。这种方式确保了组件在构造时就被完全初始化,之后可以直接使用。
  2. Setter注入 :依赖项通过setter方法传递给组件。这种方式适用于运行时动态注入依赖项的情况。
示例代码
class Logger final {
public:
    static Logger& getInstance() {
        static Logger theLogger{};
        return theLogger;
    }

    void writeInfoEntry(std::string_view entry) {
        // ...
    }

    void writeWarnEntry(std::string_view entry) {
        // ...
    }

    void writeErrorEntry(std::string_view entry) {
        // ...
    }
};

class Customer {
public:
    Customer() = default;

    void setLoggingService(const Logger& loggingService) {
        logger = loggingService;
    }

private:
    Logger logger;
};

2.2 适配器 (Adapter)

适配器模式用于将一个类的接口转换成客户端期望的另一个接口,从而使原本由于接口不兼容而不能一起工作的类可以协同工作。适配器模式常用于集成第三方库或不同团队开发的模块,确保这些模块能够无缝协作。

示例代码
class LoggingFacility {
public:
    virtual void writeInfoEntry(std::string_view entry) = 0;
    virtual void writeWarnEntry(std::string_view entry) = 0;
    virtual void writeErrorEntry(std::string_view entry) = 0;
};

class BoostLogAdapter : public LoggingFacility {
public:
    virtual void writeInfoEntry(std::string_view entry) override {
        BOOST_LOG_TRIVIAL(info) << entry;
    }

    virtual void writeWarnEntry(std::string_view entry) override {
        BOOST_LOG_TRIVIAL(warn) << entry;
    }

    virtual void writeErrorEntry(std::string_view entry) override {
        BOOST_LOG_TRIVIAL(error) << entry;
    }
};

2.3 策略 (Strategy)

策略模式定义了一系列算法,并将每个算法封装起来,使它们可以互换。策略模式允许算法独立于使用它的客户端变化,从而提高了代码的灵活性和可扩展性。策略模式非常适合处理需要根据不同条件选择不同算法的场景,例如排序算法的选择。

示例代码
class Sorter {
public:
    virtual void sort(std::vector<int>& data) = 0;
};

class BubbleSort : public Sorter {
public:
    void sort(std::vector<int>& data) override {
        // Bubble Sort implementation
    }
};

class QuickSort : public Sorter {
public:
    void sort(std::vector<int>& data) override {
        // Quick Sort implementation
    }
};

class Context {
public:
    void setStrategy(Sorter* strategy) {
        this->strategy = strategy;
    }

    void executeSort(std::vector<int>& data) {
        strategy->sort(data);
    }

private:
    Sorter* strategy;
};

2.4 命令 (Command)

命令模式将请求封装成对象,从而可以参数化方法调用。命令模式支持命令的异步执行、队列化以及撤销操作等功能。通过将命令对象传递给调用者和接收者,命令模式实现了请求发送者和接收者的解耦。

示例代码
class Command {
public:
    virtual void execute() = 0;
};

class HelloWorldOutputCommand : public Command {
public:
    void execute() override {
        std::cout << "Hello, World!" << std::endl;
    }
};

class Server {
public:
    void acceptCommand(std::shared_ptr<Command> command) {
        command->execute();
    }
};

3 命令处理器 (Command Processor)

命令处理器模式进一步扩展了命令模式的应用,特别是在处理多个命令时。命令处理器可以管理和执行一系列命令,支持命令的组合、记录和重放功能。通过引入命令处理器,可以实现更复杂的命令处理逻辑,例如宏命令的执行。

示例代码
class CompositeCommand : public Command {
public:
    void addCommand(std::shared_ptr<Command> command) {
        commands.push_back(command);
    }

    void execute() override {
        for (const auto& command : commands) {
            command->execute();
        }
    }

private:
    std::vector<std::shared_ptr<Command>> commands;
};

命令处理器模式的应用场景非常广泛,例如在图形界面应用程序中,用户可以执行一系列绘图操作,并且可以通过命令处理器撤销这些操作。


在接下来的部分,我们将详细介绍PIMPL习语及其在实际项目中的应用。PIMPL(Pointer to Implementation)是一种用于隐藏类实现细节的技术,有助于减少编译依赖并提高编译速度。通过将类的私有成员移到单独的实现类中,PIMPL可以帮助优化大型项目的编译时间和依赖管理。

4 PIMPL习语及其应用

PIMPL(Pointer to Implementation)是一种用于隐藏类实现细节的技术,有助于减少编译依赖并提高编译速度。通过将类的私有成员移到单独的实现类中,PIMPL可以帮助优化大型项目的编译时间和依赖管理。PIMPL也被称为Handle Body、Compilation Firewall或Cheshire Cat technique。这种方法使得类的接口部分保持稳定,即使内部实现发生更改,也不会影响使用该类的其他代码。

4.1 PIMPL的实现原理

PIMPL的主要思想是将类的私有成员变量和方法移到一个独立的实现类中,并通过一个指针(通常是智能指针)来间接访问这些成员。这样,头文件中只需要声明接口部分,而具体的实现则放在源文件中。当类的实现发生变化时,只有实现文件需要重新编译,而使用该类的其他代码不受影响。

示例代码
// Customer.h
#ifndef CUSTOMER_H_
#define CUSTOMER_H_

#include <memory>
#include <string>

class Address;

class Customer {
public:
    Customer();
    virtual ~Customer();
    std::string getFullName() const;
    void setShippingAddress(const Address& address);

private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

#endif // CUSTOMER_H_
// Customer.cpp
#include "Customer.h"
#include "Address.h"

struct Customer::Impl {
    int customerId;
    std::string forename;
    std::string surname;
    Address shippingAddress;
};

Customer::Customer() : pImpl(std::make_unique<Impl>()) {}

Customer::~Customer() = default;

std::string Customer::getFullName() const {
    return pImpl->forename + " " + pImpl->surname;
}

void Customer::setShippingAddress(const Address& address) {
    pImpl->shippingAddress = address;
}

4.2 PIMPL的优点

  1. 减少编译依赖 :通过将实现细节隐藏在源文件中,减少了头文件之间的依赖关系,从而降低了编译时间。
  2. 提高编译速度 :当类的实现发生变化时,只有实现文件需要重新编译,而使用该类的其他代码不受影响。
  3. 保护实现细节 :PIMPL使得类的内部实现对使用者不可见,增加了代码的安全性和可维护性。

4.3 PIMPL的缺点

  1. 增加内存开销 :由于引入了额外的指针,可能会导致一定的内存开销。
  2. 性能影响 :每次访问私有成员时都需要通过指针间接访问,可能会带来轻微的性能损失。

4.4 PIMPL的实际应用

PIMPL在大型项目中非常有用,尤其是在频繁修改类的内部实现时。例如,在一个电商系统中, Customer 类可能是核心业务实体,被多个模块引用。通过使用PIMPL,可以确保 Customer 类的接口保持稳定,即使内部实现发生变化,也不会影响其他模块的编译和运行。


5 总结与展望

5.1 总结

本文详细介绍了几种常用的设计模式及其应用场景,包括依赖注入、适配器、策略、命令和命令处理器模式。每种模式都有其独特的应用场景和实现方式,能够帮助开发者解决特定的设计问题。此外,还深入探讨了PIMPL习语,这是一种用于隐藏类实现细节的技术,有助于减少编译依赖并提高编译速度。

5.2 未来发展方向

随着软件开发的不断发展,设计模式和惯用法也在不断演进。未来,我们可以期待更多创新的设计模式和惯用法出现,以应对日益复杂和多样化的软件需求。同时,随着新技术的引入,现有的设计模式和惯用法也将不断优化和完善。


6 实际项目中的应用案例

6.1 案例1:电商系统中的依赖注入

在一个电商系统中,依赖注入被广泛应用于各个模块中,以解耦组件之间的依赖关系。例如, OrderService 类依赖于 PaymentGateway 类来处理支付逻辑。通过依赖注入, OrderService 类不需要直接创建或查找 PaymentGateway 实例,而是通过构造函数或setter方法接收依赖项。

应用流程
  1. 创建 PaymentGateway 接口。
  2. 实现具体的支付网关,如 StripePaymentGateway PayPalPaymentGateway
  3. OrderService 类中,通过构造函数注入 PaymentGateway 实例。
  4. 使用依赖注入框架(如Spring或DI容器)管理依赖项的创建和注入。

6.2 案例2:图形界面中的命令模式

在图形界面应用程序中,命令模式被用于封装用户的操作请求。例如,用户可以绘制圆形、矩形等形状,并且可以通过命令处理器撤销这些操作。命令模式使得这些操作可以被参数化、队列化和撤销,从而提高了用户体验和系统的灵活性。

应用流程
  1. 定义命令接口 Command
  2. 实现具体的命令类,如 DrawCircleCommand DrawRectangleCommand
  3. 创建命令处理器 CommandProcessor ,用于管理和执行命令。
  4. 用户执行绘图操作时,创建相应的命令对象并传递给命令处理器。
  5. 通过命令处理器执行命令,并支持撤销功能。

7 表格总结

设计模式/惯用法 主要用途 优点 缺点
依赖注入 解耦组件依赖关系 提高代码模块化和可测试性 可能增加配置复杂度
适配器 使不兼容的接口协同工作 提高模块间的兼容性 可能增加代码复杂度
策略 允许算法独立变化 提高代码灵活性和可扩展性 可能增加类的数量
命令 封装请求为对象 支持异步执行和撤销操作 可能增加类的数量
PIMPL 隐藏实现细节 减少编译依赖,提高编译速度 增加内存开销和性能影响

8 流程图说明

graph TD;
    A[依赖注入] --> B[构造函数注入];
    A --> C[Setter注入];
    B --> D[组件在构造时被完全初始化];
    C --> E[适用于运行时动态注入依赖项的情况];
graph TD;
    A[PIMPL] --> B[将类的私有成员移到单独的实现类中];
    A --> C[通过智能指针间接访问私有成员];
    B --> D[减少编译依赖];
    B --> E[提高编译速度];
    C --> F[保护实现细节];

通过以上内容,我们不仅深入了解了几种常用的设计模式及其应用场景,还掌握了PIMPL习语的具体实现和应用。希望这些知识能够帮助你在实际项目中构建高质量的软件系统。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值