本文将深入探讨C++中的一些创建型设计模式,这些模式旨在提供更高效、更灵活的解决方案。通过实例和详细解释,读者将了解如何利用这些模式来提高代码质量、可维护性和可扩展性,从而更好地应对复杂的软件设计问题。
简介
创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。
创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。
其包含以下几个模式:
- 工厂方法模式: 在父类中提供一个创建对象的接口以允许子类决定实例化对象的类型。
- 抽象工厂模式:让你能创建一系列相关的对象,而无需指定其具体类。
- 生成器模式:使你能够分步骤创建复杂对象。该模式允许你使用相同的创建代码生成不同类型和形式的对象。
- 原型模式:让你能够复制已有对象,而又无需使代码依赖它们所属的类。
- 单例模式:让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
工厂方法模式(Factory Method)
前言
假设你正在开发一款物流管理应用。最初版本只能处理卡车运输,因此大部分代码都在位于名为“卡车”的类中。
一段时间后,这款应用变得极受欢迎。你每天都能收到十几次来自海运公司的请求,希望应用能够支持海上物流功能。
但是代码问题该如何处理呢?目前,大部分代码都与 卡车 类相关。在程序中添加 轮船 类需要修改全部代码。更糟糕的是,如果你以后需要在程序中支持另外一种运输方式,很可能需要再次对这些代码进行大幅修改。
最后,你将不得不编写繁复的代码,根据不同的运输对象类,在应用中进行不同的处理。
工厂方法模式建议使用特殊的工厂方法代替对于对象构造函数的直接调用(即使用 new 运算符)。不用担心,对象仍将通过 new
运算符创建,只是该运算符改在工厂方法中调用罢了。工厂方法返回的对象通常被称作“产品”。
乍看之下,这种更改可能毫无意义:我们只是改变了程序中调用构造函数的位置而已。但是,仔细想一下,现在你可以在子类中重写工厂方法,从而改变其创建产品的类型。
但有一点需要注意:仅当这些产品具有共同的基类或者接口时,子类才能返回不同类型的产品,同时基类中的工厂方法还应将其返回类型声明为这一共有接口。
举例来说, 卡车(Truck)和 轮船(Ship)类都必须实现运输(Transport)接口, 该接口声明了一个交付(deliver)的方法。 每个类都将以不同的方式实现该方法:卡车走陆路交付货物,轮船走海路交付货物。
陆路运输(RoadLogistics)类中的工厂方法返回卡车对象,而海路运输(SeaLogistics)类则返回轮船对象。
调用工厂方法的代码(通常被称为客户端代码)无需了解不同子类返回实际对象之间的差别。客户端将所有产品视为抽象的“运输”。 客户端知道所有运输对象都提供“交付”方法,但是并不关心其具体实现方式。
实现结构
- **产品(Product)**将会对接口进行声明。对于所有由创建者及其子类构建的对象,这些接口都是通用的。
- **具体产品(Concrete Products)**是产品接口的不同实现。
- **创建者(Creator)**类声明返回产品对象的工厂方法。该方法的返回对象类型必须与产品接口相匹配。 你可以将工厂方法声明为抽象方法,强制要求每个子类以不同方式实现该方法。或者,你也可以在基础工厂方法中返回默认产品类型。注意,尽管它的名字是创建者,但他最主要的职责并不是创建产品。一般来说,创建者类包含一些与产品相关的核心业务逻辑。工厂方法将这些逻辑处理从具体产品类中分离出来。
- 具体创建者(Concrete Creators) 将会重写基础工厂方法,使其返回不同类型的产品。注意,并不一定每次调用工厂方法都会创建新的实例。工厂方法也可以返回缓存、对象池或其他来源的已有对象。
适用场景
- 当你在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。
工厂方法将创建产品的代码与实际使用产品的代码分离,从而能在不影响其他代码的情况下扩展产品创建部分代码。例如,如果需要向应用中添加一种新产品,你只需要开发新的创建者子类,然后重写其工厂方法即可。
- 如果你希望用户能扩展你软件库或框架的内部组件,可使用工厂方法。
继承可能是扩展软件库或框架默认行为的最简单方法。但是当你使用子类替代标准组件时,框架如何辨识出该子类?解决方案是将各框架中构造组件的代码集中到单个工厂方法中,并在继承该组件之外允许任何人对该方法进行重写。
- 如果你希望复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法。
在处理大型资源密集型对象(比如数据库连接、文件系统和网络资源)时,你会经常碰到这种资源需求。
优缺点
优点:
- 你可以避免创建者和具体产品之间的紧密耦合。
- 单一职责原则。你可以将产品创建代码放在程序的单一位置,从而使得代码更容易维护。
- 开闭原则。无需更改现有客户端代码,你就可以在程序中引入新的产品类型。
缺点:应用工厂方法模式需要引入许多新的子类,代码可能会因此变得更复杂。最好的情况是将该模式引入创建者类的现有层次结构中。
示例演示
Creator.h:
#ifndef CREATOR_H_
#define CREATOR_H_
#include <memory>
#include "Product.h"
// 抽象工厂类 生产电影
class Factory {
public:
virtual std::shared_ptr<Movie> get_movie() = 0;
};
#endif // CREATOR_H_
ConcreteCreator.h:
#ifndef CONCRETE_CREATOR_H_
#define CONCRETE_CREATOR_H_
#include <memory>
#include "Creator.h"
#include "ConcreteProduct.h"
// 具体工厂类 中国生产者
class ChineseProducer : public Factory {
public:
std::shared_ptr<Movie> get_movie() override { return std::make_shared<ChineseMovie>(); }
};
// 具体工厂类 日本生产者
class JapaneseProducer : public Factory {
public:
std::shared_ptr<Movie> get_movie() override { return std::make_shared<JapaneseMovie>(); }
};
// 具体工厂类 美国生产者
class AmericanProducer : public Factory {
public:
std::shared_ptr<Movie> get_movie() override { return std::make_shared<AmericanMovie>(); }
};
#endif // CONCRETE_CREATOR_H_
Product.h:
#ifndef PRODUCT_H_
#define PRODUCT_H_
#include <string>
// 抽象产品类 电影
class Movie {
public:
virtual std::string get_a_movie() = 0;
};
#endif // PRODUCT_H_
ConcreteProduct.h:
#ifndef CONCRETE_PRODUCT_H_
#define CONCRETE_PRODUCT_H_
#include <iostream>
#include <string>
#include "Product.h"
// 具体产品类 电影::国产电影
class ChineseMovie : public Movie {
public:
std::string get_a_movie() override {
return "《让子弹飞》";
}
};
// 具体产品类 电影::日本电影
class JapaneseMovie : public Movie {
public:
std::string get_a_movie() override {
return "《千与千寻》";
}
};
// 具体产品类 电影::美国电影
class AmericanMovie : public Movie {
public:
std::string get_a_movie() override {
return "《钢铁侠》";
}
};
#endif // CONCRETE_PRODUCT_H_
main.cpp:
#include "ConcreteCreator.h"
int main() {
std::shared_ptr<Factory> factory;
std::shared_ptr<Movie> product;
// 这里假设从配置中读到的是Chinese(运行时决定的)
std::string conf = "China";
// 程序根据当前配置或环境选择创建者的类型
if (conf == "China") {
factory = std::make_shared<ChineseProducer>();
} else if (conf == "Japan") {
factory = std::make_shared<JapaneseProducer>();
} else if (conf == "America") {
factory = std::make_shared<AmericanProducer>();
} else {
std::cout << "error conf" << std::endl;
}
product = factory->get_movie();
std::cout << "获取一部电影: " << product->get_a_movie() << std::endl;
//输出结果为:获取一部电影: 《让子弹飞》
}
抽象工厂模式(Abstract Factory)
前言
假设你正在开发一款家具商店模拟器。你的代码中包括一些类,用于表示:
- 一系列相关产品,例如椅子(Chair)、沙发(Sofa)和咖啡桌(CoffeeTable)
- 系列产品的不同变体,例如你可以使用现代(Modern)、维多利亚(Victorian)和装饰风艺术(ArtDeco)等风格生成这些产品
你需要设法单独生成每件家具对象,这样才能确保其风格一致。此外, 你也不希望在添加新产品或新风格时修改已有代码。家具供应商对于产品目录的更新非常频繁,你不会想在每次更新时都去修改核心代码的。
抽象工厂模式建议为系列中的每件产品明确声明接口(例如椅子、沙发或咖啡桌)。然后,确保所有产品变体都继承这些接口。例如,所有风格的椅子都实现椅子接口;所有风格的咖啡桌都实现咖啡桌接口,以此类推。
接下来, 我们需要声明抽象工厂——包含系列中所有产品构造方法的接口。例如创建椅子(createChair)、创建沙发(createSofa)和 创建咖啡桌(createCoffeeTable)。这些方法必须返回抽象产品类型,即我们之前抽取的那些接口: 椅子,沙发和咖啡桌等等。
对于系列产品的每个变体, 我们都将基于抽象工厂接口创建不同的工厂类。每个工厂类都只能返回特定类别的产品,例如, 现代家具工厂(ModernFurnitureFactory)只能创建现代椅(ModernChair)、现代沙发(ModernSofa)和现代咖啡桌(ModernCoffeeTable)对象。
客户端代码可以通过相应的抽象接口调用工厂和产品类。你无需修改实际客户端代码,就能更改传递给客户端的工厂类,也能更改客户端代码接收的产品变体。
假设客户端想要工厂创建一把椅子。客户端无需了解工厂类,也不用管工厂类创建出的椅子类型。无论是现代风格,还是维多利亚风格的椅子,对于客户端来说没有分别,它只需调用抽象椅子接口就可以了。这样一来,客户端只需知道椅子以某种方式实现了坐下(sitOn)方法就足够了。此外,无论工厂返回的是何种椅子变体,它都会和由同一工厂对象创建的沙发或咖啡桌风格一致。
最后一点说明:如果客户端仅接触抽象接口,那么谁来创建实际的工厂对象呢?一般情况下,应用程序会在初始化阶段创建具体工厂对象。而在此之前,应用程序必须根据配置文件或环境设定选择工厂类别。
实现结构
- 抽象产品(Abstract Product)为构成系列产品的一组不同但相关的产品声明接口。
- 具体产品(Concrete Product)是抽象产品的多种不同类型实现。所有变体(维多利亚/现代)都必须实现相应的抽象产品(椅子/沙发)。
- 抽象工厂(Abstract Factory)接口声明了一组创建各种抽象产品的方法。
- 具体工厂(Concrete Factory)实现抽象工厂的构建方法。每个具体工厂都对应特定产品变体,且仅创建此种产品变体。
- 尽管具体工厂会对具体产品进行初始化,其构建方法签名必须返回相应的抽象产品。这样,使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。客户端(Client)只需通过抽象接口调用工厂和产品对象,就能与任何具体工厂/产品变体交互。
适用场景
- 如果代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,你不希望代码基于产品的具体类进行构建,在这种情况下,你可以使用抽象工厂。
抽象工厂为你提供了一个接口,可用于创建每个系列产品的对象。只要代码通过该接口创建对象,那么你就不会生成与应用程序已生成的产品类型不一致的产品。
- 如果你有一个基于一组抽象方法的类,且其主要功能因此变得不明确,那么在这种情况下可以考虑使用抽象工厂模式。
在设计良好的程序中,每个类仅负责一件事。如果一个类与多种类型产品交互,就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。
优缺点
优点:
- 你可以确保同一工厂生成的产品相互匹配。
- 你可以避免客户端和具体产品代码的耦合。
- 单一职责原则。你可以将产品生成代码抽取到同一位置,使得代码易于维护。
- 开闭原则。向应用程序中引入新产品变体时,你无需修改客户端代码。
缺点:由于采用该模式需要向应用中引入众多接口和类,代码可能会比之前更加复杂。
实例演示
AbstractFactory.h:
#ifndef ABSTRACT_FACTORY_H_
#define ABSTRACT_FACTORY_H_
#include <memory>
#include "AbstractProduct.h"
// 抽象工厂类 生产电影和书籍类等
class Factory {
public:
virtual std::shared_ptr<Movie> productMovie() = 0;
virtual std::shared_ptr<Book> productBook() = 0;
};
#endif // ABSTRACT_FACTORY_H_
ConcreteFactory.h:
#ifndef CONCRETE_FACTORY_H_
#define CONCRETE_FACTORY_H_
#include <memory>
#include "AbstractFactory.h"
#include "ConcreteProduct.h"
// 具体工厂类 中国生产者
class ChineseProducer : public Factory {
public:
std::shared_ptr<Movie> productMovie() override {
return std::make_shared<ChineseMovie>();
}
std::shared_ptr<Book> productBook() override {
return std::make_shared<ChineseBook>();
}
};
// 具体工厂类 日本生产者
class JapaneseProducer : public Factory {
public:
std::shared_ptr<Movie> productMovie() override {
return std::make_shared<JapaneseMovie>();
}
std::shared_ptr<Book> productBook() override {
return std::make_shared<JapaneseBook>();
}
};
#endif // CONCRETE_FACTORY_H_
AbstractProduct.h:
#ifndef ABSTRACT_PRODUCT_H_
#define ABSTRACT_PRODUCT_H_
#include <string>
// 抽象产品类 电影
class Movie {
public:
virtual std::string showMovieName() = 0;
};
// 抽象产品类 书籍
class Book {
public:
virtual std::string showBookName() = 0;
};
#endif // ABSTRACT_PRODUCT_H_
ConcreteProduct.h:
#ifndef CONCRETE_PRODUCT_H_
#define CONCRETE_PRODUCT_H_
#include <iostream>
#include <string>
#include "AbstractProduct.h"
// 具体产品类 电影::国产电影
class ChineseMovie : public Movie {
std::string showMovieName() override {
return "《让子弹飞》";
}
};
// 具体产品类 电影::日本电影
class JapaneseMovie : public Movie {
std::string showMovieName() override {
return "《千与千寻》";
}
};
// 具体产品类 书籍::国产书籍
class ChineseBook : public Book {
std::string showBookName() override {
return "《三国演义》";
}
};
// 具体产品类 书籍::日本书籍
class JapaneseBook : public Book {
std::string showBookName() override {
return "《白夜行》";
}
};
#endif // CONCRETE_PRODUCT_H_
main.cpp:
#include <iostream>
#include "AbstractFactory.h"
#include "ConcreteFactory.h"
int main() {
std::shared_ptr<Factory> factory;
// 这里假设从配置中读到的是Chinese(运行时决定的)
std::string conf = "China";
// 程序根据当前配置或环境选择创建者的类型
if (conf == "China") {
factory = std::make_shared<ChineseProducer>();
} else if (conf == "Japan") {
factory = std::make_shared<JapaneseProducer>();
} else {
std::cout << "error conf" << std::endl;
}
std::shared_ptr<Movie> movie;
std::shared_ptr<Book> book;
movie = factory->productMovie();
book = factory->productBook();
std::cout << "获取一部电影: " << movie->showMovieName() << std::endl;
std::cout << "获取一本书: " << book->showBookName() << std::endl;
}
输出结果
获取一部电影: 《让子弹飞》
获取一本书: 《三国演义》
生成器模式(Builder)
前言
假设有这样一个复杂对象,在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中;甚至还有更糟糕的情况,那就是这些代码散落在客户端代码的多个位置。
如果为每种可能的对象都创建一个子类,这可能导致程序过于复杂:
例如, 我们来思考如何创建一个房屋(House)对象。建造一栋简单的房屋,首先你需要建造四面墙和地板,安装房门和一套窗户,然后再建造一个屋顶。但是如果你想要一栋更宽敞更明亮的房屋,还要有院子和其他设施(例如暖气、排水和供电设备),那又该怎么办呢?
最简单的方法是扩展房屋基类,然后创建一系列涵盖所有参数组合的子类。但最终你将面对相当数量的子类。任何新增的参数(例如门廊类型)都会让这个层次结构更加复杂。
另一种方法则无需生成子类。你可以在房屋“基类”中创建一个包括所有可能参数的超级构造函数,并用它来控制房屋对象。这种方法确实可以避免生成子类,但它却会造成另外一个问题(这些大量的参数不是每次都要全部用上的)。
通常情况下绝大部分的参数都没有使用,这对于构造函数的调用十分不简洁。例如,只有很少的房子有游泳池,因此与游泳池相关的参数十之八九是毫无用处的。
生成器模式建议将对象构造代码从产品类中抽取出来,并将其放在一个名为生成器的独立对象中。生成器模式能让你分步骤创建复杂对象,生成器不允许其他对象访问正在创建中的产品。
该模式会将对象构造过程划分为一组步骤, 比如创建墙壁(buildWalls)和创建房门(buildDoor)等。每次创建对象时,你都需要通过生成器对象执行一系列
步骤。重点在于你无需调用所有步骤,而只需调用创建特定对象配置所需的那些步骤即可。
当你需要创建不同形式的产品时,其中的一些构造步骤可能需要不同的实现。例如,木屋的房门可能需要使用木头制造,而城堡的房门则必须使用石头制造。
在这种情况下,你可以创建多个不同的生成器,用不同方式实现一组相同的创建步骤。然后你就可以在创建过程中使用这些生成器(例如按顺序调用多个构造步骤)来生成不同类型的对象。
实现结构
- 生成器(Builder)接口声明在所有类型生成器中通用的产品构造步骤。
- 具体生成器(Concrete Builders)提供构造过程的不同实现。具体生成器也可以构造不遵循通用接口的产品。
- 产品(Products)是最终生成的对象。由不同生成器构造的产品无需属于同一类层次结构或接口。
- 主管(Director)类定义调用构造步骤的顺序,这样你就可以创建和复用特定的产品配置。
- 客户端(Client)必须将某个生成器对象与主管类关联。一般情况下,你只需通过主管类构造函数的参数进行一次性关联即可。此后主管类就能使用生成器对象完成后续所有的构造任务。但在客户端将生成器对象传递给主管类制造方法时还有另一种方式。在这种情况下,你在使用主管类生产产品时每次都可以使用不同的生成器。
适用场景
- 使用生成器模式可避免“重叠构造函数(telescopicconstructor)”的出现。
假设你的构造函数中有十个可选参数,那么调用该函数会非常不方便;因此,你需要重载这个构造函数,新建几个只有较少参数的简化版。但这些构造函数仍需调用主构造函数,传递一些默认数值来替代省略掉的参数。生成器模式让你可以分步骤生成对象,而且允许你仅使用必须的步骤。应用该模式后,你再也不需要将几十个参数塞进构造函数里了。
- 当你希望使用代码创建不同形式的产品(例如石头或木头房屋)时,可使用生成器模式。
如果你需要创建的各种形式的产品,它们的制造过程相似且仅有细节上的差异,此时可使用生成器模式。基本生成器接口中定义了所有可能的制造步骤,具体生成器将实现这些步骤来制造特定形式的产品。同时,主管类将负责管理制造步骤的顺序。
- 使用生成器构造组合树或其他复杂对象。
生成器模式让你能分步骤构造产品。你可以延迟执行某些步骤而不会影响最终产品。你甚至可以递归调用这些步骤,这在创建对象树时非常方便。生成器在执行制造步骤时,不能对外发布未完成的产品。这可以避免客户端代码获取到不完整结果对象的情况。
优缺点
优点:
- 你可以分步创建对象,暂缓创建步骤或递归运行创建步骤。
- 生成不同形式的产品时,你可以复用相同的制造代码。
- 单一职责原则。你可以将复杂构造代码从产品的业务逻辑中分离出来。
缺点:由于该模式需要新增多个类,因此代码整体复杂程度会有所增加。
实例演示
Product.h:
#ifndef PRODUCT_H_
#define PRODUCT_H_
#include <string>
#include <iostream>
// 产品类 车
class Car {
public:
Car() {}
void set_car_tire(std::string t) {
tire_ = t;
std::cout << "set tire: " << tire_ << std::endl;
}
void set_car_steering_wheel(std::string sw) {
steering_wheel_ = sw;
std::cout << "set steering wheel: " << steering_wheel_ << std::endl;
}
void set_car_engine(std::string e) {
engine_ = e;
std::cout << "set engine: " << engine_ << std::endl;
}
private:
std::string tire_; // 轮胎
std::string steering_wheel_; // 方向盘
std::string engine_; // 发动机
};
#endif // PRODUCT_H_
Builder.h:
#ifndef BUILDER_H_
#define BUILDER_H_
#include "Product.h"
// 抽象建造者
class CarBuilder {
public:
Car getCar() {
return car_;
}
// 抽象方法
virtual void buildTire() = 0;
virtual void buildSteeringWheel() = 0;
virtual void buildEngine() = 0;
protected:
Car car_;
};
#endif // BUILDER_H_
ConcreteBuilder.h:
#ifndef CONCRETE_BUILDER_H_
#define CONCRETE_BUILDER_H_
#include "Builder.h"
// 具体建造者 奔驰
class BenzBuilder : public CarBuilder {
public:
// 具体实现方法
void buildTire() override {
car_.set_car_tire("benz_tire");
}
void buildSteeringWheel() override {
car_.set_car_steering_wheel("benz_steering_wheel");
}
void buildEngine() override {
car_.set_car_engine("benz_engine");
}
};
// 具体建造者 奥迪
class AudiBuilder : public CarBuilder {
public:
// 具体实现方法
void buildTire() override {
car_.set_car_tire("audi_tire");
}
void buildSteeringWheel() override {
car_.set_car_steering_wheel("audi_steering_wheel");
}
void buildEngine() override {
car_.set_car_engine("audi_engine");
}
};
#endif // CONCRETE_BUILDER_H_
Director.h:
#ifndef DIRECTOR_H_
#define DIRECTOR_H_
#include "Builder.h"
class Director {
public:
Director() : builder_(nullptr) {}
void set_builder(CarBuilder *cb) {
builder_ = cb;
}
// 组装汽车
Car ConstructCar() {
builder_->buildTire();
builder_->buildSteeringWheel();
builder_->buildEngine();
return builder_->getCar();
}
private:
CarBuilder* builder_;
};
#endif // DIRECTOR_H_
main.cpp:
#include "Director.h"
#include "ConcreteBuilder.h"
int main() {
// 抽象建造者(一般是动态确定的)
CarBuilder* builder;
// 指挥者
Director* director = new Director();
// 产品
Car car;
// 建造奔驰
std::cout << "==========construct benz car==========" << std::endl;
builder = new BenzBuilder();
director->set_builder(builder);
car = director->ConstructCar();
delete builder;
// 建造奥迪
std::cout << "==========construct audi car==========" << std::endl;
builder = new AudiBuilder();
director->set_builder(builder);
car = director->ConstructCar();
delete builder;
std::cout << "==========done==========" << std::endl;
delete director;
}
输出结果为:
==========construct benz car==========
set tire: benz_tire
set steering wheel: benz_steering_wheel
set engine: benz_engine
==========construct audi car==========
set tire: audi_tire
set steering wheel: audi_steering_wheel
set engine: audi_engine
==========done==========
原型模式(Prototype)
前言
如果你有一个对象,并希望生成与其完全相同的一个复制品,你该如何实现呢?首先,你必须新建一个属于相同类的对象。然后,你必须遍历原始对象的所有成员变量,并将成员变量值复制到新对象中。
但是,并非所有对象都能通过这种方式进行复制,因为有些对象可能拥有私有成员变量,它们在对象本身以外是不可见的。
直接复制还有另外一个问题。因为你必须知道对象所属的类才能创建复制品,所以代码必须依赖该类。即使你可以接受额外的依赖性,那还有另外一个问题:有时你只知道对象所实现的接口,而不知道其所属的具体类,比如可向方法的某个参数传入实现了某个接口的任何对象。
原型模式将克隆过程委派给被克隆的实际对象。模式为所有支持克隆的对象声明了一个通用接口,该接口让你能够克隆对象,同时又无需将代码和对象所属类耦合。通常情况下,这样的接口中仅包含一个“克隆”方法。
所有的类对“克隆”方法的实现都非常相似。该方法会创建一个当前类的对象,然后将原始对象所有的成员变量值复制到新建的类中。你甚至可以复制私有成员变量,因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。
支持克隆的对象即为原型。当你的对象有几十个成员变量和几百种类型时,对其进行克隆甚至可以代替子类的构造。
实现结构
- 原型(Prototype)接口将对克隆方法进行声明。在绝大多数情况下,其中只会有一个名为clone 克隆的方法。
- 具体原型(Concrete Prototype)类将实现克隆方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等等。
- 客户端(Client)可以复制实现了原型接口的任何对象。
适用场景
- 如果你需要复制一些对象,同时又希望代码独立于这些对象所属的具体类,可以使用原型模式。
这一点考量通常出现在代码需要处理第三方代码通过接口传递过来的对象时。即使不考虑代码耦合的情况,你的代码也不能依赖这些对象所属的具体类,因为你不知道它们的具体信息。原型模式为客户端代码提供一个通用接口,客户端代码可通过这一接口与所有实现了克隆的对象进行交互,它也使得客户端代码与其所克隆的对象具体类独立开来。
- 如果子类的区别仅在于其对象的初始化方式,那么你可以使用该模式来减少子类的数量。别人创建这些子类的目的可能是为了创建特定类型的对象。
在原型模式中,你可以使用一系列预生成的、各种类型的对象作为原型。客户端不必根据需求对子类进行实例化,只需找到合适的原型并对其进行克隆即可。
优缺点
优点:
- 你可以克隆对象,而无需与它们所属的具体类相耦合。
- 你可以克隆预生成原型,避免反复运行初始化代码。
- 你可以更方便地生成复杂对象。
- 你可以用继承以外的方式来处理复杂对象的不同配置。
缺点:克隆包含循环引用的复杂对象可能会非常麻烦。
实例演示
Prototype.h:
#ifndef PROTOTYPE_H_
#define PROTOTYPE_H_
// 抽象原型类
class Object {
public:
virtual Object* clone() = 0;
};
#endif // PROTOTYPE_H_
ConcretePrototype.h:
#ifndef CONCRETE_PROTOTYPE_H_
#define CONCRETE_PROTOTYPE_H_
#include <iostream>
#include <string>
#include "Prototype.h"
// 邮件的附件
class Attachment {
public:
void set_content(std::string content) {
content_ = content;
}
std::string get_content() {
return content_;
}
private:
std::string content_;
};
// 具体原型: 邮件类
class Email : public Object {
public:
Email() {}
Email(std::string text, std::string attachment_content) : text_(text), attachment_(new Attachment()) {
attachment_->set_content(attachment_content);
}
~Email() {
if (attachment_ != nullptr) {
delete attachment_;
attachment_ = nullptr;
}
}
void display() {
std::cout << "------------查看邮件------------" << std::endl;
std::cout << "正文: " << text_ << std::endl;
std::cout << "邮件: " << attachment_->get_content() << std::endl;
std::cout << "------------查看完毕------------" << std::endl;
}
// 深拷贝
Email* clone() override {
return new Email(this->text_, this->attachment_->get_content());
}
void changeText(std::string new_text) {
text_ = new_text;
}
void changeAttachment(std::string content) {
attachment_->set_content(content);
}
private:
std::string text_;
Attachment *attachment_ = nullptr;
};
#endif // CONCRETE_PROTOTYPE_H_
main.cpp:
#include "ConcretePrototype.h"
#include <cstdio>
int main() {
Email* email = new Email("最初的文案", "最初的附件");
Email* copy_email = email->clone();
copy_email->changeText("新文案");
copy_email->changeAttachment("新附件");
std::cout << "original email:" << std::endl;
email->display();
std::cout << "copy email:" << std::endl;
copy_email->display();
delete email;
delete copy_email;
}
输出结果:
original email:
------------查看邮件------------
正文: 最初的文案
邮件: 最初的附件
------------查看完毕------------
copy email:
------------查看邮件------------
正文: 新文案
邮件: 新附件
------------查看完毕------------
单例模式(Singleton)
前言
单例模式同时解决了两个问题,所以违反了单一职责原则:
- 保证一个类只有一个实例。
- 为该实例提供一个全局访问节点。
为什么会有人想要控制一个类所拥有的实例数量?最常见的原因是控制某些共享资源(例如数据库或文件)的访问权限。它的运作方式是这样的:如果你创建了一个对象,同时过一会儿后你决定再创建一个新对象,此时你会获得之前已创建的对象,而不是一个新对象。
注意,普通构造函数无法实现上述行为,因为构造函数的设计决定了它必须总是返回一个新对象。
所有单例的实现都包含以下两个相同的步骤:
- 将默认构造函数设为私有, 防止其他对象使用单例类的new运算符。
- 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。
实现结构
单例(Singleton) 类声明了一个名为getInstance 获取实例的静态方法来返回其所属类的一个相同实例。
单例的构造函数必须对客户端(Client) 代码隐藏。调用获取实例方法必须是获取单例对象的唯一方式。
适用场景
- 如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。
单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。该方法可以创建一个新对象,但如果该对象已经被创建,则返回已有的对象。
- 如果你需要更加严格地控制全局变量,可以使用单例模式。
单例模式与全局变量不同,它保证类只存在一个实例。除了单例类自己以外,无法通过任何方式替换缓存的实例。可以随时调整限制并设定生成单例实例的数量,只需修改获取实例方法, 即getInstance 中的代码即可实现。
优缺点
优点:
- 你可以保证一个类只有一个实例。
- 你获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
缺点:
- 违反了单一职责原则。该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。
实例演示
线程安全的懒汉模式:特点是在首次访问单例对象时才进行对象的实例化。懒汉模式的优点是可以避免在程序启动时就进行对象的实例化,节省了系统的资源。懒汉模式的缺点是在多线程环境下,可能会出现线程安全问题,需要额外的同步措施来保证线程安全性。
Singleton.h:
#ifndef SINGLETON_H_
#define SINGLETON_H_
#include <iostream>
#include <string>
#include <mutex>
class Singleton {
public:
static Singleton* GetInstance() {
if (instance_ == nullptr) {
// 加锁保证多个线程并发调用getInstance()时只会创建一个实例
m_mutex_.lock();
if (instance_ == nullptr) {
instance_ = new Singleton();
}
m_mutex_.unlock();
}
return instance_;
}
private:
Singleton() {}
static Singleton* instance_;
static std::mutex m_mutex_;
};
#endif // SINGLETON_H_
静态变量instance初始化不要放在头文件中, 如果多个文件包含singleton.h会出现重复定义问题
#include "Singleton.h"
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::m_mutex_;
饿汉模式:特点是在程序启动时就进行对象的实例化。在这种实现方式中,单例对象会在类加载时就被创建出来。
Singleton.h:
#ifndef SINGLETON_H_
#define SINGLETON_H_
class Singleton {
public:
static Singleton* GetInstance() {
return instance_;
}
private:
Singleton() {}
static Singleton* instance_;
};
#endif // SINGLETON_H_
#include "Singleton.h"
Singleton* Singleton::instance_ = new Singleton();
最推荐的单例写法
#ifndef SINGLETON_H_
#define SINGLETON_H_
class Singleton {
public:
static Singleton& GetInstance() {
static Singleton instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
};
#endif // SINGLETON_H_
解决了普通单例模式全局变量初始化依赖(C++只能保证在同一个文件中声明的static遍历初始化顺序和其遍历声明的顺序一致,但是不能保证不同文件中static遍历的初始化顺序)
缺点:
- 需要C++11支持(C++11保证static成员初始化的线程安全)
- 性能问题(同懒汉模式一样,每次调用GetInstance()方法时需要判断局部static变量是否已经初始化,如果没有初始化就会进行初始化,这个判断逻辑会消耗一点性能)