📋 摘要

六大设计原则(SOLID)是什么?

设计原则是设计模式的「底层方法论」:模式是原则在特定场景下的具体落地。中文语境常说「六大设计原则」,即 SOLID 五项 + 迪米特法则。

缩写原则一句话解释
SRP单一职责原则一个类只负责一件事,只有一个引起它变化的原因
OCP开闭原则对扩展开放,对修改关闭(加功能不加旧代码)
LSP里氏替换原则子类必须能完全替换父类,不破坏程序正确性
ISP接口隔离原则接口要小而专,客户端不依赖它用不到的接口
DIP依赖倒置原则高层与低层都依赖抽象,不依赖具体实现
LoD迪米特法则最少知识原则:只和直接朋友说话

以「图形绘制」为例,展示 SRP + OCP + DIP 的组合:

C++
// 违反 DIP:高层 Drawer 直接依赖具体类 Circle/Rect,加新图形要改 Drawer
class Drawer {
public:
    void drawCircle()   { /* ... */ }
    void drawRect()     { /* ... */ }
    // 新增三角形 -> 必须修改 Drawer,违反 OCP
};

// 依赖倒置 + 开闭:面向抽象 Shape 编程
class Shape {           // 抽象
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
};

class Circle : public Shape {
public:
    void draw() const override { /* 画圆 */ }
};

class Rectangle : public Shape {
public:
    void draw() const override { /* 画矩形 */ }
};

class Renderer {        // 高层只依赖抽象 Shape
    std::vector<std::unique_ptr<Shape>> shapes_;
public:
    void add(std::unique_ptr<Shape> s) { shapes_.push_back(std::move(s)); }
    void renderAll() const {
        for (const auto& s : shapes_) s->draw();
    }
};
// 新增 Triangle 只需新增类,Renderer 无需改动 => 开闭原则
💡 面试加分项

六大原则里 OCP 是「目标」,其余原则是「手段」:SRP/LSP/ISP/DIP 都是为了能方便地做到对扩展开放、对修改关闭。回答时点出这个递进关系比背定义更出彩。

适用场景与优缺点

  • 适用:任何面向对象系统的设计评审;重构时判断「这个类是不是干了两件事」。
  • 优点:系统更易维护、易扩展、可测试。
  • 缺点:过度设计会徒增类数量与间接层,需在复杂度与收益间权衡。

单例模式(懒汉/饿汉/线程安全)如何实现?

单例保证一个类全局只有一个实例,并提供全局访问点。核心是「私有构造函数 + 静态访问方法」。C++11 后推荐 Meyers 单例(函数内静态局部变量),天然线程安全且无需手动 new。

C++
#include <mutex>

// 1) Meyers 懒汉单例(推荐):C++11 起静态局部变量初始化线程安全
class Singleton {
public:
    Singleton(const Singleton&)            = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton& getInstance() {
        static Singleton instance;   // 首次调用时初始化,线程安全,进程结束自动析构
        return instance;
    }

    void doWork() { /* ... */ }

private:
    Singleton() = default;           // 构造函数私有
    ~Singleton() = default;
};

// 2) 饿汉单例:main 之前即创建,无线程安全顾虑,但可能造成「未使用也创建」
class EagerSingleton {
public:
    static EagerSingleton& getInstance() { return instance_; }
private:
    EagerSingleton() = default;
    static EagerSingleton instance_;     // 静态成员定义需在 .cpp 中
};
// EagerSingleton EagerSingleton::instance_;

// 3) 经典双检锁(DCLP):配合 atomic + acquire/release,仅演示,生产优先用 Meyers
class DCLSingleton {
public:
    static DCLSingleton* getInstance() {
        DCLSingleton* tmp = instance_.load(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mtx_);
            tmp = instance_.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new DCLSingleton();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
private:
    DCLSingleton() = default;
    static std::atomic<DCLSingleton*> instance_;
    static std::mutex mtx_;
};
⚠️ 易错点
  • 忘记把拷贝构造/赋值运算符 = delete,导致能复制出第二个实例。
  • 双检锁在 C++11 前存在内存序问题,必须用 atomic 且正确设置 acquire/release;现在直接写 Meyers 即可。
  • 单例的析构顺序在多模块间不可控,跨 DLL/共享库要格外小心。

适用场景与优缺点

  • 适用:配置管理器、日志器、连接池、线程池、数据库连接等全局唯一资源。
  • 优点:全局唯一、延迟初始化(懒汉)、避免重复资源开销。
  • 缺点:隐藏依赖、难测试(难以 mock)、生命周期不可控,滥用会变成「变相全局变量」。

简单工厂、工厂方法、抽象工厂的区别?

三者都是「把对象创建与使用解耦」,区别在于抽象层级与扩展方式:

  • 简单工厂:一个工厂类 + 一个静态/普通方法,按参数返回不同产品。缺点:加新产品要改工厂(违反 OCP)。
  • 工厂方法:每个产品对应一个工厂类(工厂接口 + 具体工厂),加新产品只需加类不改旧代码。
  • 抽象工厂:创建一组相关/相互依赖的产品族(如 Windows 家族 vs Mac 家族)。
C++
#include <memory>

// ---------- 简单工厂 ----------
struct Button { virtual void render() = 0; virtual ~Button() = default; };
struct WinButton  : Button { void render() override { /* Windows 按钮 */ } };
struct MacButton  : Button { void render() override { /* Mac 按钮 */ } };

class SimpleFactory {
public:
    static std::unique_ptr<Button> create(const std::string& os) {
        if (os == "win") return std::make_unique<WinButton>();
        if (os == "mac") return std::make_unique<MacButton>();
        return nullptr;
    }
};

// ---------- 工厂方法 ----------
struct Factory {                       // 抽象工厂
    virtual std::unique_ptr<Button> createButton() = 0;
    virtual ~Factory() = default;
};
struct WinFactory : Factory {
    std::unique_ptr<Button> createButton() override { return std::make_unique<WinButton>(); }
};
struct MacFactory : Factory {
    std::unique_ptr<Button> createButton() override { return std::make_unique<MacButton>(); }
};

// ---------- 抽象工厂:一组相关产品(按钮 + 复选框)----------
struct CheckBox { virtual void render() = 0; virtual ~CheckBox() = default; };
struct WinCheckBox : CheckBox { void render() override { /* ... */ } };
struct MacCheckBox : CheckBox { void render() override { /* ... */ } };

struct UIFactory {                          // 抽象工厂:一个「产品族」
    virtual std::unique_ptr<Button>   createButton()   = 0;
    virtual std::unique_ptr<CheckBox> createCheckBox() = 0;
    virtual ~UIFactory() = default;
};
struct WinUIFactory : UIFactory {
    std::unique_ptr<Button>   createButton()   override { return std::make_unique<WinButton>(); }
    std::unique_ptr<CheckBox> createCheckBox() override { return std::make_unique<WinCheckBox>(); }
};
💡 面试加分项

用一句话记住三者递进关系:简单工厂 = 一个函数造所有对象;工厂方法 = 一个工厂造一种对象;抽象工厂 = 一个工厂造一族对象。面试常追问「工厂方法比简单工厂好在哪里」——好在新增产品不违反开闭原则。

适用场景与优缺点

  • 适用:UI 跨平台组件、日志框架多后端、支付多渠道、对象创建逻辑复杂且希望集中管理。
  • 优点:解耦创建与使用;隐藏创建细节;便于扩展。
  • 缺点:类数量增多;抽象工厂新增「产品族内新产品」需改所有工厂接口。

建造者模式如何实现?

建造者将复杂对象的构造过程与其表示分离,允许同样的构建步骤产生不同表示,且支持链式调用(fluent interface)。

C++
#include <string>

// 产品:一台电脑
class Computer {
public:
    std::string cpu_, gpu_, ram_, disk_;
    void show() const {
        // 打印配置
    }
};

// 抽象建造者
class Builder {
public:
    virtual ~Builder() = default;
    virtual Builder& buildCPU(const std::string&)  = 0;
    virtual Builder& buildGPU(const std::string&)  = 0;
    virtual Builder& buildRAM(const std::string&)  = 0;
    virtual Computer build() = 0;   // 返回最终产品
};

// 具体建造者
class GameComputerBuilder : public Builder {
    Computer c_;
public:
    Builder& buildCPU(const std::string& cpu) override { c_.cpu_ = cpu; return *this; }
    Builder& buildGPU(const std::string& gpu) override { c_.gpu_ = gpu; return *this; }
    Builder& buildRAM(const std::string& ram) override { c_.ram_ = ram; return *this; }
    Computer build() override { return c_; }
};

// 指挥者:固定组装流程
class Director {
    Builder& b_;
public:
    explicit Director(Builder& b) : b_(b) {}
    Computer construct() {
        return b_.buildCPU("i9").buildGPU("RTX 4090").buildRAM("64G").build();
    }
};
💡 面试加分项

建造者 vs 工厂:工厂关心「造出哪个类的对象」,一步到位;建造者关心「分步骤构造 + 参数可选组合」,适合构造参数多、有默认值、顺序可变的场景(如 HTTP 请求、SQL 语句、ProtoBuf 消息)。链式 .setA().setB() 是建造者的标志性写法。

适用场景与优缺点

  • 适用:构造参数很多且有默认值(配置对象、网络请求)、构建步骤复杂但可复用、需要不可变对象。
  • 优点:分步构建、可复用构建流程、链式调用可读性好、隔离复杂构造逻辑。
  • 缺点:类数量多;若产品差异不大则引入建造者是过度设计。

原型模式如何实现?

原型通过克隆已有对象来创建新对象,而非 new。适用于创建成本高(如初始化耗时、依赖 DB/网络)的对象。

C++
#include <memory>

class Shape {
public:
    virtual ~Shape() = default;
    virtual std::unique_ptr<Shape> clone() const = 0;   // 原型核心方法
    virtual void draw() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    // 拷贝构造 + clone
    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Circle>(*this);   // 借助拷贝构造实现深拷贝
    }
    void draw() const override { /* 画圆 */ }
};

// 使用:原型注册表,按 key 克隆
class ShapeRegistry {
    std::unordered_map<std::string, std::unique_ptr<Shape>> prototypes_;
public:
    void registerShape(const std::string& key, std::unique_ptr<Shape> p) {
        prototypes_[key] = std::move(p);
    }
    std::unique_ptr<Shape> create(const std::string& key) {
        return prototypes_.at(key)->clone();     // 克隆而非 new
    }
};
⚠️ 易错点

C++ 里「拷贝构造/拷贝赋值」默认是浅拷贝。含指针成员时必须实现深拷贝(或对象用智能指针/值语义成员),否则 clone 出的对象共享同一块堆内存,会造成双重释放或数据串改。

适用场景与优缺点

  • 适用:对象创建成本高、需要「以一个实例为模板批量复制」、希望隐藏具体类的创建。
  • 优点:避免昂贵构造、运行时可动态增删原型、简化创建。
  • 缺点:深拷贝实现易出错;对循环引用成员需额外处理。

适配器模式如何实现?

适配器把一个类的接口转换成客户端期望的另一个接口,让原本接口不兼容的类能协同工作。C++ 有两种实现:类适配器(多继承) 与 对象适配器(组合),后者更常用。

C++
#include <memory>

// 已存在的旧接口(被适配者):只有 printText,不接受我们的 Render 接口
class LegacyPrinter {
public:
    void printText(const std::string& s) { /* 打印文本 */ }
};

// 客户端期望的新接口
class Printer {
public:
    virtual ~Printer() = default;
    virtual void render(const std::string& content) = 0;
};

// 对象适配器:内部持有一个 LegacyPrinter,做接口转换
class PrinterAdapter : public Printer {
    LegacyPrinter legacy_;   // 组合
public:
    void render(const std::string& content) override {
        legacy_.printText(content);   // 适配:render -> printText
    }
};

// 使用
void clientCode(Printer& p) { p.render("hello"); }
// PrinterAdapter ad; clientCode(ad);
ℹ️ 与其他模式区别
  • 适配器:接口不同,改造后复用(“插座转换头”)。
  • 装饰器:接口相同,动态增强功能。
  • 代理:接口相同,控制访问(延迟加载/权限/远程)。 三者代码结构相似(都「包一层」),区别在意图,面试常拿来对比。

适用场景与优缺点

  • 适用:对接第三方库/遗留代码、统一多个相似但不兼容的接口、数据格式转换。
  • 优点:复用现有类、符合开闭原则、解耦客户端与被适配者。
  • 缺点:增加一层间接;类适配器受 C++ 多继承与访问权限限制。

装饰器模式如何实现?

装饰器在不修改原类的前提下,动态地给对象添加额外职责,且可层层嵌套(如「加糖的加奶的咖啡」)。

C++
#include <memory>
#include <string>

// 组件接口
class Coffee {
public:
    virtual ~Coffee() = default;
    virtual std::string description() const = 0;
    virtual double cost() const = 0;
};

// 具体组件
class SimpleCoffee : public Coffee {
public:
    std::string description() const override { return "咖啡"; }
    double cost() const override { return 10.0; }
};

// 抽象装饰器:既是一个 Coffee,又持有一个 Coffee
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> inner_;
public:
    explicit CoffeeDecorator(std::unique_ptr<Coffee> c) : inner_(std::move(c)) {}
};

// 具体装饰器
class MilkDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;
    std::string description() const override { return inner_->description() + " + 牛奶"; }
    double cost() const override { return inner_->cost() + 2.0; }
};

class SugarDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;
    std::string description() const override { return inner_->description() + " + 糖"; }
    double cost() const override { return inner_->cost() + 1.0; }
};

// 使用:层层包裹
// auto coffee = std::make_unique<SugarDecorator>(
//     std::make_unique<MilkDecorator>(std::make_unique<SimpleCoffee>()));
💡 面试加分项

装饰器与继承对比:继承在编译期静态扩展、产生类爆炸;装饰器在运行期动态组合、按需叠加。C++ 中「输入/输出流」(std::fstream、std::istringstream)本质就是装饰器的应用。

适用场景与优缺点

  • 适用:需要动态、透明地给对象加功能;职责可自由组合(日志、权限、缓存、压缩包装)。
  • 优点:比继承更灵活、符合开闭原则、可任意组合。
  • 缺点:小类多、调试时嵌套层数多不易读。

代理模式如何实现?

代理为另一个对象提供替身或占位,以控制对它的访问。常见变体:虚拟代理(延迟加载)、远程代理、保护代理(权限)、日志代理。

C++
#include <memory>

class Image {
public:
    virtual ~Image() = default;
    virtual void display() = 0;
};

// 真实对象:加载昂贵的大图
class RealImage : public Image {
    std::string filename_;
    void loadFromDisk() { /* 昂贵的 IO */ }
public:
    explicit RealImage(const std::string& f) : filename_(f) { loadFromDisk(); }
    void display() override { /* 显示图片 */ }
};

// 虚拟代理:延迟加载,首次 display 才真正创建 RealImage
class ImageProxy : public Image {
    std::string filename_;
    std::unique_ptr<RealImage> real_;
public:
    explicit ImageProxy(const std::string& f) : filename_(f) {}
    void display() override {
        if (!real_) real_ = std::make_unique<RealImage>(filename_);
        real_->display();
    }
};
ℹ️ 代理 vs 装饰器 vs 适配器

三者都「包装」了对象:代理控制访问(创建时机、权限、跨进程);装饰器增强功能;适配器改变接口。回答时务必说明「意图差异」。

适用场景与优缺点

  • 适用:大对象延迟加载、访问控制与鉴权、远程服务本地存根(RPC)、日志埋点、智能指针(std::shared_ptr 的 deleter 也算一种控制)。
  • 优点:职责分离、延迟加载省资源、可加安全控制。
  • 缺点:增加一层间接、首次访问可能变慢。

外观模式如何实现?

外观为一组复杂子系统提供一个统一的高层接口,简化客户端调用。

C++
class CPU    { public: void freeze() { /* ... */ } void jump(long) { /* ... */ } void execute() { /* ... */ } };
class Memory { public: void load(long, const std::string&) { /* ... */ } };
class Disk   { public: std::string read(long) { return {}; } };

// 外观:把「启动电脑」这一串操作封装成单一入口
class ComputerFacade {
    CPU cpu_; Memory mem_; Disk disk_;
public:
    void start() {
        cpu_.freeze();
        mem_.load(0, disk_.read(0));
        cpu_.jump(0);
        cpu_.execute();
    }
    void shutdown() { /* ... */ }
};

// 客户端:一行搞定,无需了解子系统内部
// ComputerFacade computer; computer.start();
💡 面试加分项

外观 vs 中介者:两者都「降低耦合」,但外观是单向的(客户端 -> 子系统,简化入口);中介者是双向的(子系统之间通过中介者互不直接通信)。这个对比常被追问。

适用场景与优缺点

  • 适用:为复杂库/框架提供易用入口、分层架构中隔离子系统、遗留系统封装。
  • 优点:降低客户端复杂度、隔离子系统、减少依赖。
  • 缺点:外观可能变成「上帝对象」;不符合开闭原则时需扩展外观。

桥接模式如何实现?

桥接将抽象部分与实现部分分离,使两者可独立变化。核心是「组合代替继承」,避免「n 种形状 × m 种颜色」的类爆炸。

C++
#include <memory>

// 实现部分(Implementor)
class Color {
public:
    virtual ~Color() = default;
    virtual std::string fill() const = 0;
};
class Red   : public Color { public: std::string fill() const override { return "红色"; } };
class Blue  : public Color { public: std::string fill() const override { return "蓝色"; } };

// 抽象部分(Abstraction)
class Shape {
protected:
    std::unique_ptr<Color> color_;   // 组合:持有实现
public:
    explicit Shape(std::unique_ptr<Color> c) : color_(std::move(c)) {}
    virtual ~Shape() = default;
    virtual void draw() const = 0;
};

class Circle : public Shape {
public:
    using Shape::Shape;
    void draw() const override {
        // 画一个 color_->fill() 的圆
    }
};

// 使用:形状与颜色独立扩展,n+m 个类而非 n*m 个
// auto redCircle = std::make_unique<Circle>(std::make_unique<Red>());
// auto blueCircle = std::make_unique<Circle>(std::make_unique<Blue>());
💡 面试加分项

桥接 vs 策略:结构几乎一样(都持有一个接口指针),但桥接是「结构性」的——分离抽象与实现让两者独立演化;策略是「行为性」的——在运行时切换算法。意图不同,代码却很像,这正是面试区分点。

适用场景与优缺点

  • 适用:一个类有两个独立变化的维度(形状+颜色、设备+操作系统)、希望运行时切换实现、避免继承爆炸。
  • 优点:分离抽象与实现、符合开闭原则、组合复用。
  • 缺点:增加设计复杂度,需识别「变化的维度」。

组合模式如何实现?

组合将对象组织成树形结构,使客户端能统一处理单个对象与组合对象(如文件系统:文件与文件夹)。

C++
#include <memory>
#include <vector>

// 统一接口
class Component {
public:
    virtual ~Component() = default;
    virtual void operation() const = 0;
};

// 叶子:文件
class Leaf : public Component {
    std::string name_;
public:
    explicit Leaf(const std::string& n) : name_(n) {}
    void operation() const override { /* 对 name_ 操作 */ }
};

// 组合:文件夹,可包含叶子或子组合
class Composite : public Component {
    std::vector<std::unique_ptr<Component>> children_;
public:
    void add(std::unique_ptr<Component> c) { children_.push_back(std::move(c)); }
    void operation() const override {
        for (const auto& c : children_) c->operation();   // 递归调用
    }
};
💡 面试加分项

组合 vs 装饰器:两者都是「递归/树形结构」,但组合关注整体-部分结构(统一叶子和容器),装饰器关注给单一对象叠加职责。组合让客户端不用区分「是一个还是多个」。

适用场景与优缺点

  • 适用:文件系统、UI 控件树、菜单树、组织架构、AST 语法树。
  • 优点:统一处理叶子与组合、易递归、符合开闭原则。
  • 缺点:过度泛化会难以限制「某些组合不允许某叶子」,类型约束被削弱。

观察者模式如何实现?

观察者定义一对多依赖:当被观察对象(Subject)状态改变时,自动通知所有观察者(Observer)。典型实现是「发布-订阅」。

C++
#include <vector>
#include <algorithm>

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const std::string& msg) = 0;   // 收到通知
};

class Subject {
    std::vector<Observer*> observers_;
public:
    void attach(Observer* o)      { observers_.push_back(o); }
    void detach(Observer* o) {
        observers_.erase(std::remove(observers_.begin(), observers_.end(), o),
                         observers_.end());
    }
    void notify(const std::string& msg) {
        for (auto* o : observers_) o->update(msg);      // 广播
    }
};

class LogObserver : public Observer {
public:
    void update(const std::string& msg) override { /* 写日志 */ }
};
class UIObserver : public Observer {
public:
    void update(const std::string& msg) override { /* 刷新界面 */ }
};
⚠️ 易错点

观察者模式最常见的坑:通知过程中观察者被删除/移除导致悬空指针,或观察者回调里又触发通知造成死循环/重入。C++ 可用 std::weak_ptr 或「先拷贝快照再通知」规避。

适用场景与优缺点

  • 适用:事件驱动系统、GUI 事件、消息订阅、数据变化联动(如库存变化刷新多个视图)。
  • 优点:松耦合、符合开闭原则、动态增删订阅。
  • 缺点:通知顺序不可控、可能内存泄漏(忘 detach)、重入与悬空指针风险。

策略模式如何实现?

策略定义一系列算法,把它们封装成可互换的对象,让算法独立于使用它的客户端而变化。

C++
#include <memory>

// 策略接口
class SortStrategy {
public:
    virtual ~SortStrategy() = default;
    virtual void sort(std::vector<int>& data) const = 0;
};

class QuickSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) const override { /* 快排 */ }
};
class MergeSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) const override { /* 归并 */ }
};

// 上下文:持有策略,运行时可切换
class Sorter {
    std::unique_ptr<SortStrategy> strategy_;
public:
    void setStrategy(std::unique_ptr<SortStrategy> s) { strategy_ = std::move(s); }
    void sort(std::vector<int>& data) const {
        if (strategy_) strategy_->sort(data);
    }
};
// 使用:sorter.setStrategy(make_unique<QuickSort>()); 运行时可换 MergeSort
💡 面试加分项

策略 vs 状态:两者结构极像(都持有对象、委托调用),但策略是客户端主动选择算法(互不感知、通常不自动切换);状态是对象自身状态驱动行为变化(状态间有转换关系)。C++ 中 std::function / 函数指针常可替代小型策略,减少类爆炸。

适用场景与优缺点

  • 适用:多种算法可选(排序、压缩、支付、折扣策略)、消除大量 if-else。
  • 优点:符合开闭原则、运行时可切换、消除条件分支。
  • 缺点:类数量增多、客户端需了解各策略差异。

模板方法模式如何实现?

模板方法在父类中定义算法骨架,将可变步骤延迟到子类实现,子类只填空、不改整体流程。

C++
class DataProcessor {
public:
    virtual ~DataProcessor() = default;
    // 模板方法:固定流程骨架,通常声明为非虚/加 final 防止被重写
    void process() {
        readData();
        transformData();   // 可变的钩子
        writeData();
    }
protected:
    void readData()  { /* 固定:读数据 */ }
    void writeData() { /* 固定:写数据 */ }
    virtual void transformData() = 0;   // 可变步骤,子类实现
};

class CsvProcessor : public DataProcessor {
protected:
    void transformData() override { /* CSV 特有转换 */ }
};
class JsonProcessor : public DataProcessor {
protected:
    void transformData() override { /* JSON 特有转换 */ }
};
💡 面试加分项

模板方法 vs 策略:模板方法用继承(父类定骨架、子类填步骤,编译期确定);策略用组合(运行期切换整个算法)。模板方法体现了「好莱坞原则:Don’t call us, we’ll call you」——框架调用你,而非你调用框架。

适用场景与优缺点

  • 适用:算法骨架固定、部分步骤可变(批处理、编译器前端、测试框架 setUp/tearDown)。
  • 优点:复用公共流程、符合开闭原则、父类控制流程。
  • 缺点:继承带来的强耦合、子类受父类骨架约束。

责任链模式如何实现?

责任链将多个处理器串成一条链,请求沿链传递,直到某个处理器处理它(或全部都不处理)。典型应用:审批流、日志分级、HTTP 中间件。

C++
#include <memory>

class Handler {
protected:
    std::unique_ptr<Handler> next_;
public:
    virtual ~Handler() = default;
    Handler& setNext(std::unique_ptr<Handler> n) { next_ = std::move(n); return *this; }
    virtual void handle(int request) {
        if (next_) next_->handle(request);   // 自己不处理,传给下一个
    }
};

class Level1Handler : public Handler {
public:
    void handle(int request) override {
        if (request <= 100) { /* 处理小额审批 */ }
        else Handler::handle(request);       // 超出权限,交给上级
    }
};
class Level2Handler : public Handler {
public:
    void handle(int request) override {
        if (request <= 1000) { /* 处理中额审批 */ }
        else Handler::handle(request);
    }
};
// 使用:auto h1 = make_unique<Level1Handler>(); h1->setNext(make_unique<Level2Handler>());
💡 面试加分项

责任链的两种变体:拦截式(每个处理器都过一遍,如中间件、日志)与终止式(首个能处理的处理并停止,如审批流)。代码结构上差别只在「处理后是否继续传」。

适用场景与优缺点

  • 适用:多级审批、异常处理链、请求过滤器、日志级别路由。
  • 优点:解耦请求发送者与处理者、动态增删处理器、职责单一。
  • 缺点:请求可能没人处理(需兜底)、链条过长性能下降。

状态模式如何实现?

状态模式让对象在内部状态改变时改变其行为,把每个状态封装成独立类,消除大量 if-else/switch。

C++
#include <memory>

class State {
public:
    virtual ~State() = default;
    virtual void handle() = 0;
};

class Context;   // 前置声明
class StateA : public State { public: void handle() override; };
class StateB : public State { public: void handle() override; };

// 上下文持有当前状态
class Context {
    std::unique_ptr<State> state_;
public:
    explicit Context(std::unique_ptr<State> s) : state_(std::move(s)) {}
    void setState(std::unique_ptr<State> s) { state_ = std::move(s); }
    void request() { state_->handle(); }
    void toB();   // 提供状态切换入口
};
⚠️ 易错点

状态模式里状态对象之间的切换关系容易写成相互耦合(A 持有 B 的引用)。可用「上下文统一负责切换」或「状态返回下一个状态」来解耦,避免状态类之间产生循环依赖。

适用场景与优缺点

  • 适用:订单状态机(待支付→已支付→已发货→已完成)、TCP 连接状态、游戏角色状态、工作流引擎。
  • 优点:消除大量条件分支、状态逻辑内聚、新增状态符合开闭原则。
  • 缺点:状态类增多、状态转换关系可能复杂化。

命令模式如何实现?

命令模式把「请求」封装成对象,从而支持参数化、排队、日志、撤销/重做。

C++
#include <memory>
#include <vector>

// 命令接口
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;      // 支持撤销
};

// 接收者:真正干活的对象
class Editor {
public:
    void insert(const std::string& s) { /* 插入文本 */ }
    void remove(const std::string& s) { /* 删除文本 */ }
};

// 具体命令:绑定接收者与操作
class InsertCommand : public Command {
    Editor& editor_;
    std::string text_;
public:
    InsertCommand(Editor& e, std::string t) : editor_(e), text_(std::move(t)) {}
    void execute() override { editor_.insert(text_); }
    void undo()    override { editor_.remove(text_); }
};

// 调用者:执行/记录命令,支持撤销栈
class Invoker {
    std::vector<std::unique_ptr<Command>> history_;
public:
    void run(std::unique_ptr<Command> c) { c->execute(); history_.push_back(std::move(c)); }
    void undoLast() { if (!history_.empty()) { history_.back()->undo(); history_.pop_back(); } }
};
💡 面试加分项

命令模式是回调/函数对象的面向对象化:C++ 里轻量场景可用 std::function 或 lambda 代替 Command 类,但需要撤销/重做、命令队列、宏录制时,命令对象(execute + undo)更合适。

适用场景与优缺点

  • 适用:编辑器撤销/重做、任务队列/线程池任务、GUI 按钮动作、事务回滚、宏录制。
  • 优点:解耦调用者与执行者、支持撤销/队列/日志、可组合命令。
  • 缺点:类数量增多、每个操作都要封装。

迭代器模式如何实现?

迭代器提供顺序访问聚合对象元素的方式,且不暴露内部表示。C++ 中 STL 已内置迭代器,自己实现时核心是「begin()/end() + operator++ + operator*」。

C++
#include <vector>

// 聚合对象:一个自定义集合
template <typename T>
class MyContainer {
    std::vector<T> data_;
public:
    void add(const T& v) { data_.push_back(v); }

    // 嵌套迭代器:实现指针语义
    class Iterator {
        typename std::vector<T>::iterator it_;
    public:
        explicit Iterator(typename std::vector<T>::iterator it) : it_(it) {}
        Iterator& operator++()   { ++it_; return *this; }
        bool operator!=(const Iterator& other) const { return it_ != other.it_; }
        T& operator*()           { return *it_; }
    };

    Iterator begin() { return Iterator(data_.begin()); }
    Iterator end()   { return Iterator(data_.end()); }
};

// 使用:范围 for 或手动迭代
// MyContainer<int> c; for (auto& x : c) { ... }
💡 面试加分项

C++ 的迭代器设计是迭代器模式的「最高级形态」:配合 <algorithm> 泛型算法,实现数据容器与算法彻底解耦。面试可提到「STL 迭代器分类:输入/输出/前向/双向/随机访问」。

适用场景与优缺点

  • 适用:自定义容器/集合、遍历复杂数据结构(树、图)、统一遍历接口。
  • 优点:隐藏内部结构、统一遍历方式、支持多种遍历策略。
  • 缺点:简单集合引入迭代器略重;C++ 中注意迭代器失效问题。

享元模式如何实现?

享元通过共享不可变的内在状态来大量减少内存占用,把可变的「外在状态」抽离出去。典型应用:字体/字符对象池、棋盘棋子。

C++
#include <unordered_map>
#include <memory>

// 享元:共享的内在状态(字体、字号)——不可变
class Font {
    std::string name_;
    int size_;
public:
    Font(std::string name, int size) : name_(std::move(name)), size_(size) {}
    void render(const std::string& text, int x, int y) const {
        // 用 name_/size_ 在 (x,y) 画 text;x,y 是外在状态
    }
};

// 享元工厂:按内在状态缓存/复用对象
class FontFactory {
    std::unordered_map<std::string, std::shared_ptr<Font>> cache_;
public:
    std::shared_ptr<Font> getFont(const std::string& name, int size) {
        std::string key = name + ":" + std::to_string(size);
        auto it = cache_.find(key);
        if (it != cache_.end()) return it->second;
        auto f = std::make_shared<Font>(name, size);
        cache_[key] = f;
        return f;
    }
};
ℹ️ 关键概念

内在状态(intrinsic):可共享、不变,如字体的名称与字号。外在状态(extrinsic):随场景变化,如字符的位置 (x,y)、颜色,由客户端在调用时传入,不存入享元。

适用场景与优缺点

  • 适用:大量相似对象且内存紧张(文字排版、游戏粒子、缓存池)、对象状态可拆分为内外在。
  • 优点:大幅减少内存、集中管理共享对象。
  • 缺点:把状态拆开增加复杂度、线程安全需额外处理(共享对象只读则安全)。

中介者、备忘录、访问者模式各解决什么问题?

这三个模式「面试出现频率低于前十七个」,常以「各解决什么问题」打包考。分别回答一句话定位 + 最小骨架即可。

中介者模式(Mediator)

解决多对象间网状通信导致的耦合爆炸:所有交互都通过一个中介者对象,变成星形结构(如聊天室、MVC 的 Controller、机场塔台)。

C++
class Mediator {
public:
    virtual ~Mediator() = default;
    virtual void notify(class Component* sender, const std::string& event) = 0;
};

class Component {   // 组件只认识中介者,不认识彼此
protected:
    Mediator* mediator_;
public:
    explicit Component(Mediator* m) : mediator_(m) {}
};

// 具体中介者维护所有组件,转发消息
class ChatRoom : public Mediator {
    std::vector<Component*> members_;
public:
    void notify(Component* sender, const std::string& event) override {
        for (auto* m : members_) if (m != sender) { /* 转发给其他人 */ }
    }
};

备忘录模式(Memento)

解决对象状态快照与恢复(撤销):在不破坏封装的前提下捕获对象内部状态,稍后恢复。C++ 常用「拷贝当前状态对象」简化实现。

C++
#include <memory>

class Memento {          // 快照,只存状态
    int state_;
public:
    explicit Memento(int s) : state_(s) {}
    int state() const { return state_; }
};

class Originator {       // 发起者:创建/恢复备忘录
    int state_ = 0;
public:
    void setState(int s) { state_ = s; }
    std::unique_ptr<Memento> save() { return std::make_unique<Memento>(state_); }
    void restore(const Memento& m) { state_ = m.state(); }
};

class Caretaker {        // 管理者:保存/管理历史快照,但不读取其内容
    std::vector<std::unique_ptr<Memento>> history_;
public:
    void push(std::unique_ptr<Memento> m) { history_.push_back(std::move(m)); }
};
💡 面试加分项

备忘录 vs 命令:命令模式通过「重放逆操作」实现撤销(省内存但需每个命令可逆);备忘录通过「存快照」实现撤销(省逻辑但占内存)。大型对象状态常用备忘录,操作型任务常用命令。

访问者模式(Visitor)

解决在不修改元素类的前提下,为对象结构添加新操作,尤其适合「元素稳定、操作频繁增加」的场景(如编译器 AST 上新增各种分析)。

C++
class Element;
class Visitor {
public:
    virtual ~Visitor() = default;
    virtual void visit(class ElementA* e) = 0;
    virtual void visit(class ElementB* e) = 0;
};

class Element {
public:
    virtual ~Element() = default;
    virtual void accept(Visitor& v) = 0;   // 双重分派入口
};

class ElementA : public Element {
public:
    void accept(Visitor& v) override { v.visit(this); }   // 第一次分派:调用自身 accept
};
class ElementB : public Element {
public:
    void accept(Visitor& v) override { v.visit(this); }   // 第二次分派:visitor 按类型分派
};

class PrintVisitor : public Visitor {
public:
    void visit(ElementA* e) override { /* 打印 A */ }
    void visit(ElementB* e) override { /* 打印 B */ }
};
⚠️ 易错点

访问者利用双重分派(double dispatch)绕开 C++ 单分派限制。代价是:新增元素类型时要改所有 Visitor 接口(违反开闭原则),所以元素稳定、操作多变才用访问者。

三者一句话总结

模式解决什么一句话
中介者多对象网状耦合交互都走中间人,星形替代网状
备忘录状态保存与恢复存快照、可回滚
访问者稳定结构上加新操作双重分派,操作外置