C++设计模式-学习笔记



整理自李建忠老师的《C++设计模式》视频教程

视频:C++设计模式[李建忠]:



C++设计模式-李建忠



设计模式简介


课程目标

  • 理解松耦合设计思想
  • 掌握面向对象设计原则
  • 掌握重构技法改善设计
  • 掌握GOF核心设计模式

什么是设计模式

“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。——Christopher Alexander


GOF 设计模式

在这里插入图片描述

  • 历史性著作《设计模式:可复用面向对象软件的基础》一书中描述了23种经典面向对象设计模式,创立了模式在软件设计中的地位。

  • 由于《设计模式》一书确定了设计模式的地位,通常所说的设计模式隐含地表示“面向对象设计模式”。但这并不意味“设计模式"就等于“面向对象设计模式”。



如何解决复杂性?

  • 分解

    • 人们面对复杂性有一个常见的做法:即分而治之,将大问题分解为多个小问题,将复杂问题分解为多个简单问题。
  • 抽象

    • 更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节,而去处理泛化和理想化了的对象模型。

面向对象设计原则

  1. 依赖倒置原则(DIP)
  • 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。
  • 抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。
  1. 开放封闭原则(OCP)
  • 对扩展开放,对更改封闭。
  • 类模块应该是可扩展的,但是不可修改。
  1. 单一职责原则(SRP)
  • 一个类应该仅有一个引起它变化的原因。
  • 变化的方向隐含着类的责任。
  1. Liskov 替换原则(LSP)
  • 子类必须能够替换它们的基类(IS-A)。
  • 继承表达类型抽象。
  1. 接口隔离原则(ISP)
  • 不应该强迫客户程序依赖它们不用的方法。
  • 接口应该小而完备。
  1. 优先使用对象组合,而不是类继承
  • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用” 。
  • 继承在某种程度上破坏了封装性,子类父类耦合度高。
  • 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
  1. 封装变化点
  • 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
  1. 针对接口编程,而不是针对实现编程
  • 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
  • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
  • 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。

从封装变化角度对模式分类

组件协作:

单一职责:

对象创建:

对象性能:

接口隔离:

状态变化:

数据结构:

行为变化:

领域问题:

总结

现代较少用的模式

  • Builder
  • Mediator
  • Memento
  • Iterator
  • Chain of Resposibility
  • Command
  • Visitor
  • Interpreter


Observer 观察者模式


1. “组件协作”模式:

现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。

  • 典型模式
    • Template Method
    • Strategy
    • Observer / Event

2. 动机(Motivation)

在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。

使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。


3. 模式定义

定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。——《设计模式》GoF


4. 结构(Structure)

在这里插入图片描述


5. 需要改进的C++伪代码

//--------------------------class MainForm---------------------------------//

class MainForm : public Form
{
    TextBox *txtFilePath;
    TextBox *txtFileNumber;
    ProgressBar *progressBar; //观察者 //如:进度条显示

public:
    void Button1_Click()
    {

        string filePath = txtFilePath->getText();
        int number = atoi(txtFileNumber->getText().c_str());

        FileSplitter splitter(filePath, number, progressBar); //传递给下一级

        splitter.split();
    }
};

//--------------------------class FileSplitter---------------------------------//

class FileSplitter
{
    string m_filePath;
    int m_fileNumber;
    ProgressBar *m_progressBar; //依赖于上一级,所以要解决这种依赖关系

public:
    FileSplitter(const string &filePath,
                 int fileNumber,
                 ProgressBar *progressBar)
        : m_filePath(filePath),
          m_fileNumber(fileNumber),
          m_progressBar(progressBar)
    {
    }

    void split()
    {

        //1.读取大文件

        //2.分批次向小文件中写入
        for (int i = 0; i < m_fileNumber; i++)
        {
            //...
            float progressValue = m_fileNumber;
            progressValue = (i + 1) / progressValue;
            m_progressBar->setValue(progressValue);
        }
    }
};

//-----------------------------------------------------------//


6. 改进后的C++伪代码

//-----------------------class IProgress------class MainForm------------------------------//

//观察者 - 基类
class IProgress
{
public:
    virtual void DoProgress(float value) = 0;
    virtual ~IProgress() {}
};

// MainForm 是一个观察者 - 子类
class MainForm : public Form, public IProgress
{
    TextBox *txtFilePath;
    TextBox *txtFileNumber;

    ProgressBar *progressBar; //

public:
    void Button1_Click()
    {

        string filePath = txtFilePath->getText();
        int number = atoi(txtFileNumber->getText().c_str());

        ConsoleNotifier cn;

        //此class的对象负责:分隔文件为几份
        FileSplitter splitter(filePath, number);

        splitter.addIProgress(this); //自身是一个观察者 //订阅通知
        splitter.addIProgress(&cn);  //订阅通知

        //函数功能:分隔文件
        //分隔文件时,更新进度条,这个时候所有订阅者都要能收到通知
        splitter.split();

        splitter.removeIProgress(this); //删除一个观察者
    }

    //自身是一个观察者 - 子类
    //自身作为观察者的处理函数--显示百分比
    virtual void DoProgress(float value)
    {
        progressBar->setValue(value);
    }
};

class ConsoleNotifier : public IProgress //另一个观察者
{
public:
    // 此观察者对应的处理函数
    // 不显示百分比,打点来显示进度
    virtual void DoProgress(float value)
    {
        cout << ".";
    }
};

//--------------------------class FileSplitter---------------------------------//

class FileSplitter
{
    string m_filePath;
    int m_fileNumber;

    List<IProgress *> m_iprogressList; // 抽象通知机制,支持多个观察者

public:
    FileSplitter(const string &filePath,
                 int fileNumber)
        : m_filePath(filePath),
          m_fileNumber(fileNumber)
    {
    }

    void split()
    {

        //1.读取大文件

        //2.分批次向小文件中写入
        for (int i = 0; i < m_fileNumber; i++)
        {
            //...

            float progressValue = m_fileNumber;
            progressValue = (i + 1) / progressValue;
            onProgress(progressValue); //给观察者-发送通知
        }
    }

    void addIProgress(IProgress *iprogress) //添加观察者,添加订阅
    {
        m_iprogressList.push_back(iprogress);
    }

    void removeIProgress(IProgress *iprogress) //移除某个观察者
    {
        m_iprogressList.remove(iprogress);
    }

protected:
    virtual void onProgress(float value) // 给观察者-发送通知
    {

        List<IProgress *>::iterator itor = m_iprogressList.begin();

        while (itor != m_iprogressList.end())
            (*itor)->DoProgress(value); //更新进度条
        itor++;
    }
};

//-----------------------------------------------------------//


7. 要点总结

使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。

目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。

观察者自己决定是否需要订阅通知,目标对象对此一无所知。

Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。



Factory Method工厂方法


1. “对象创建”模式

通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。

  • 典型模式
    • Factory Method
    • Abstract Factory
    • Prototype
    • Builder

2. 动机(Motivation)

在软件系统中,经常面临着创建对象的工作;由于需求的变化,需要创建的对象的具体类型经常变化。

如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合?


3. 模式定义

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化延迟(目的:解耦,手段:虚函数)到子类。——《设计模式》GoF


4. 结构(Structure)

在这里插入图片描述


5. 需要改进的C++伪代码

//--------------------------class MainForm---------------------------------//
class MainForm : public Form
{
    TextBox *txtFilePath;
    TextBox *txtFileNumber;
public:
    void Button1_Click()
    {
        ISplitter *splitter =
            new BinarySplitter(); //分隔文件//依然依赖具体类->需要改进 

        splitter->split();
    }
};
//----------------------------class ISplitter-------------------------------//

class ISplitter
{
public:
    virtual void split() = 0;
    virtual ~ISplitter() {}
};

class BinarySplitter : public ISplitter // 二进制格式 分隔文件
{};

class TxtSplitter : public ISplitter // 分隔文本
{};

class PictureSplitter : public ISplitter // 分隔图片
{};

class VideoSplitter : public ISplitter // 分隔视频
{};

//-----------------------------------------------------------//


6. 改进后的C++伪代码

//--------------------------class MainForm---------------------------------//
class MainForm : public Form
{
    SplitterFactory *factory; //工厂类 - 父类指针

public:
    MainForm(SplitterFactory *factory)
    {
        this->factory = factory;//外界(用户) 自己指定 具体 子工厂类对象
    }

    void Button1_Click()
    {

        ISplitter *splitter =
            factory->CreateSplitter(); //多态

        splitter->split();
    }
};

//-------------------class ISplitter-----class SplitterFactory--------------//
//分隔类-基类
class ISplitter
{
public:
    virtual void split() = 0;
    virtual ~ISplitter() {}
};

//工厂类-基类
class SplitterFactory
{
public:
    virtual ISplitter *CreateSplitter() = 0;
    virtual ~SplitterFactory() {}
};
//-------------class ISplitter的子类-----class SplitterFactory的子类--------//

//分隔类 的派生类 们
class BinarySplitter : public ISplitter
{
};

class TxtSplitter : public ISplitter
{
};

class PictureSplitter : public ISplitter
{
};

class VideoSplitter : public ISplitter
{
};

//工厂类(服务于分隔类) 的派生类 们
class BinarySplitterFactory : public SplitterFactory
{
public:
    virtual ISplitter *CreateSplitter()
    {
        return new BinarySplitter();
    }
};

class TxtSplitterFactory : public SplitterFactory
{
public:
    virtual ISplitter *CreateSplitter()
    {
        return new TxtSplitter();
    }
};

class PictureSplitterFactory : public SplitterFactory
{
public:
    virtual ISplitter *CreateSplitter()
    {
        return new PictureSplitter();
    }
};

class VideoSplitterFactory : public SplitterFactory
{
public:
    virtual ISplitter *CreateSplitter()
    {
        return new VideoSplitter();
    }
};

//-----------------------------------------------------------//


7.要点总结

Factory Method模式用于隔离类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导致软件的脆弱。

Factory Method模式通过面向对象的手法,将所要创建的具体对象工作延迟到子类,从而实现一种扩展(而非更改)的策略,较好地解决了这种紧耦合关系。

Factory Method模式解决“单个对象”的需求变化。缺点在于要求创建方法/参数相同。



Abstract Factory 抽象工厂


1. 对象创建”模式

通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。

  • 典型模式
    • Factory Method
    • Abstract Factory
    • Prototype
    • Builder

2. 动机(Motivation)

在软件系统中,经常面临着 “一系列相互依赖的对象” 的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。

如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作”的紧耦合?

3. 模式定义

提供一个接口,让该接口负责创建一系列 “相关或者相互依赖的对象” ,无需指定它们具体的类。——《设计模式》GoF


4. 结构(Structure)

在这里插入图片描述


5. C++伪代码

class EmployeeDAO
{

public:
    vector<EmployeeDO> GetEmployees()
    {
        //连接数据库
        SqlConnection *connection =
            new SqlConnection();
        connection->ConnectionString = "...";

        //执行SQL语句
        SqlCommand *command =
            new SqlCommand();
        command->CommandText = "...";
        command->SetConnection(connection); //关联性

        //读取执行结果
        SqlDataReader *reader = command->ExecuteReader(); //关联性
        while (reader->Read())
        {
        }
    }
};

6. C++伪代码改进(版本1 - Factory Method)

//--------------------------------------------------//
class IDBConnection{};
class IDBConnectionFactory
{
public:
    virtual IDBConnection *CreateDBConnection() = 0;
};

class SqlConnection : public IDBConnection{}; //支持SQL Server
class SqlConnectionFactory : public IDBConnectionFactory{}; //支持SQL Server的factory

class OracleConnection : public IDBConnection{}; //支持Oracle

//--------------------------------------------------//
class IDBCommand{};
class IDBCommandFactory
{
public:
    virtual IDBCommand *CreateDBCommand() = 0;
};

class SqlCommand : public IDBCommand{}; //支持SQL Server
class SqlCommandFactory : public IDBCommandFactory{};

class OracleCommand : public IDBCommand{}; //支持Oracle

//--------------------------------------------------//
class IDataReader{};
class IDataReaderFactory
{
public:
    virtual IDataReader *CreateDataReader() = 0;
};

class SqlDataReader : public IDataReader{}; //支持SQL Server
class SqlDataReaderFactory : public IDataReaderFactory{};

class OracleDataReader : public IDataReader{}; //支持Oracle

//--------------------------------------------------//

class EmployeeDAO
{
    IDBConnectionFactory *dbConnectionFactory;
    IDBCommandFactory *dbCommandFactory;
    IDataReaderFactory *dataReaderFactory;

public:
    vector<EmployeeDO> GetEmployees()
    {
        IDBConnection *connection =
            dbConnectionFactory->CreateDBConnection();
        connection->ConnectionString("...");

        IDBCommand *command =
            dbCommandFactory->CreateDBCommand();
        command->CommandText("...");
        command->SetConnection(connection); //关联性

        IDBDataReader *reader = command->ExecuteReader(); //关联性
        while (reader->Read())
        {
        }
    }
};

如果用户不小心把 mysql的connection 和 oracle的command 混搭在一起,那将会出错!
所以要把这些有关系的 一系列 操作,封装到一起,这就是 Abstract Factory的思想,代码如下:


7. C++伪代码改进(版本2 - Abstract Factory)

//-----------------------------------------------------------//

//数据库访问有关的基类
class IDB
{
    DBConnection();
    DBCommand();
    DataReader();
};

class IDBFactory
{
public:
    virtual IDBConnection *CreateDBConnection() = 0;
    virtual IDBCommand *CreateDBCommand() = 0;
    virtual IDataReader *CreateDataReader() = 0;
};
//支持SQL Server
class SqlDBFactory : public IDBFactory
{
public:
    virtual IDBConnection *CreateDBConnection() = 0;
    virtual IDBCommand *CreateDBCommand() = 0;
    virtual IDataReader *CreateDataReader() = 0;
};
//支持Oracle
class OracleDBFactory : public IDBFactory
{
public:
    virtual IDBConnection *CreateDBConnection() = 0;
    virtual IDBCommand *CreateDBCommand() = 0;
    virtual IDataReader *CreateDataReader() = 0;
};

class EmployeeDAO
{
    IDBFactory *dbFactory;

public:
    vector<EmployeeDO> GetEmployees()
    {
    	//连接数据库
        IDBConnection *connection =
            dbFactory->CreateDBConnection();
        connection->ConnectionString("...");
		
		//执行SQL语句
        IDBCommand *command =
            dbFactory->CreateDBCommand();
        command->CommandText("...");
        command->SetConnection(connection); //关联性
		
		//读取执行结果
        IDBDataReader *reader = command->ExecuteReader(); //关联性
        while (reader->Read())
        {
        }
    }
};
//-----------------------------------------------------------//

8. 要点总结

如果没有应对 “多系列对象构建” 的需求变化,则没有必要使用Abstract Factory模式,这时候使用简单的工厂完全可以。

“系列对象” 指的是在某一特定系列下的对象之间有相互依赖、或作用的关系。不同系列的对象之间不能相互依赖。

Abstract Factory模式主要在于应对“新系列”的需求变动(如:增加一个系列)。其缺点在于难以应对“新对象”的需求变动(对一个系列的基类进行更改)。



Singleton 单件模式


1. “对象性能”模式

面向对象很好地解决了“抽象”的问题,但是必不可免地要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。

  • 典型模式
    • Singleton
    • Flyweight

2. 动机(Motivation)

在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。

如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?

这应该是类设计者的责任,而不是使用者的责任。


3. 模式定义

保证一个类仅有一个实例,并提供一个该实例的全局访问点。——《设计模式》GoF


4. 结构(Structure)

在这里插入图片描述


C++伪代码

#include <iostream>
#include <mutex>
#include <atomic>
using namespace std;

typedef mutex Lock;

class Singleton
{
private:
    Singleton();
    Singleton(const Singleton &other);

public:
    static Singleton *getInstance1();
    static Singleton *getInstance2();
    static Singleton *getInstance3();
    static Singleton *getInstance4();

    static Singleton *m_instance;
};
Singleton *Singleton::m_instance = nullptr;

//线程非安全版本(单线程时可以使用,多线程时不行)
Singleton *Singleton::getInstance1()
{
    if (m_instance == nullptr)
    {
        m_instance = new Singleton();
    }
    return m_instance;
}

//线程安全版本,但锁的代价过高(多线程时,很多读取操作的话,会影响性能)
Singleton *Singleton::getInstance2()
{
    Lock lock;
    if (m_instance == nullptr)
    {
        m_instance = new Singleton();
    }
    return m_instance;
}

//双检查锁,但由于内存读写reorder不安全
Singleton *Singleton::getInstance3()
{

    if (m_instance == nullptr)
    {
        Lock lock;
        if (m_instance == nullptr)
        {
            m_instance = new Singleton();
        }
    }
    return m_instance;
}

//C++ 11版本之后的跨平台实现 (volatile)
std::atomic<Singleton *> Singleton::m_instance;
//std::mutex Singleton::m_mutex;
std::mutex m_mutex;

Singleton *Singleton::getInstance4()
{
    Singleton *tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire); //获取内存fence
    if (tmp == nullptr)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr)
        {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release); //释放内存fence
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}


6. 要点总结

Singleton模式中的实例构造器可以设置为protected以允许子类派生。

Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。

如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。



Chain of Resposibility 职责链


1. “数据结构”模式

常常有一些组件在内部具有特定的数据结构,如果让客户程序依赖这些特定的数据结构,将极大地破坏组件的复用。这时候,将这些特定数据结构封装在内部,在外部提供统一的接口,来实现与特定数据结构无关的访问,是一种行之有效的解决方案。

  • 典型模式
    • Composite
    • Iterator
    • Chain of Resposibility

2. 动机(Motivation)

在软件构建过程中,一个请求可能被多个对象处理,但是每个请求在运行时只能有一个接受者,如果显式指定,将必不可少地带来请求发送者与接受者的紧耦合。

如何使请求的发送者不需要指定具体的接受者?让请求的接受者自己在运行时决定来处理请求,从而使两者解耦。


3. 模式定义

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。——《设计模式》GoF


4. 结构(Structure)

在这里插入图片描述


5. C++代码

#include <iostream>
#include <string>

using namespace std;

enum class RequestType
{
    REQ_HANDLER1,
    REQ_HANDLER2,
    REQ_HANDLER3
};

class Reqest
{
    string description;
    RequestType reqType;

public:
    Reqest(const string &desc, RequestType type) : description(desc), reqType(type) {}
    RequestType getReqType() const { return reqType; }
    const string &getDescription() const { return description; }
};

class ChainHandler
{
public:
    ChainHandler() { nextChain = nullptr; }
    void setNextChain(ChainHandler *next) { nextChain = next; }

    void handle(const Reqest &req)
    {
        if (canHandleRequest(req))
            processRequest(req);
        else
            sendReqestToNextHandler(req);
    }
    
private:
    ChainHandler *nextChain;
    void sendReqestToNextHandler(const Reqest &req)
    {
        if (nextChain != nullptr)
            nextChain->handle(req);
    }

protected:
    virtual bool canHandleRequest(const Reqest &req) = 0;
    virtual void processRequest(const Reqest &req) = 0;
};

class Handler1 : public ChainHandler
{
protected:
    bool canHandleRequest(const Reqest &req) override
    {
        return req.getReqType() == RequestType::REQ_HANDLER1;
    }
    void processRequest(const Reqest &req) override
    {
        cout << "Handler1 is handle reqest: " << req.getDescription() << endl;
    }
};

class Handler2 : public ChainHandler
{
protected:
    bool canHandleRequest(const Reqest &req) override
    {
        return req.getReqType() == RequestType::REQ_HANDLER2;
    }
    void processRequest(const Reqest &req) override
    {
        cout << "Handler2 is handle reqest: " << req.getDescription() << endl;
    }
};

class Handler3 : public ChainHandler
{
protected:
    bool canHandleRequest(const Reqest &req) override
    {
        return req.getReqType() == RequestType::REQ_HANDLER3;
    }
    void processRequest(const Reqest &req) override
    {
        cout << "Handler3 is handle reqest: " << req.getDescription() << endl;
    }
};

int main()
{
    Handler1 h1;
    Handler2 h2;
    Handler3 h3;
    h1.setNextChain(&h2);
    h2.setNextChain(&h3);

    Reqest req("process task ... ", RequestType::REQ_HANDLER3);
    h1.handle(req);
    return 0;
}

6. 要点总结

Chain of Responsibility 模式的应用场合在于 “一个请求可能有多个接受者,但是最后真正的接受者只有一个” ,这时候请求发送者与接受者的耦合有可能出现“变化脆弱”的症状,职责链的自的就是将二者解耦,从而更好地应对变化。

应用了Chain of Responsibility模式后,对象的职责分派将更具灵活性。我们可以在运行时动态添加/修改请求的处理职责。

如果请求传递到职责链的末尾仍得不到处理,应该有一个合理的缺省机制。这也是每一个接受对象的责任,而不是发出请求的对象的责任。



  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值