常见设计模式的实现(Go和C++版)

WaitBright / 2023-08-14 / 原文

导语

在编程开发过程中,会碰到各类场景,如果每次都来一个问题解决一个问题会极大的降低开发效率,所以有必要将开发过程中遇到的场景加以总结。设计模式就是这样一套被反复验证、按照最佳实践的经验总结。GoF按照创建型结构型和行为型三大类总结提出了23种设计模式,本文用Go和C++语言来实现和演示在编程开发中常见的8种设计模式,并分析它们的应用场景。大家想要深入了解设计模式建议阅读文章最后的参考文献,本文只起到抛砖引玉的作用。

  • 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。
  • 结构型模式(Structural Pattern)描述如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构
  • 行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化,重点关注对象间的交互。

1.单例模式

单例模式在开发过程中经常遇到,是最简单的一种设计模式,是创建型的一种。让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。那为什么会有一个类只能创建一个实例的情况呢?常见的一个原因是同一时刻只有一个使用者能控制某个共享资源的访问权限,例如Windows任务管理器,无论启动多少次都只能显示一个,这样设计既避免了资源的浪费又防止多个窗口由于cpu、内存等数据变化带来的不一致问题。以下是两种语言的实现方式。

  • Go版本懒汉模式
import "sync"

// taskManager 任务窗口
type taskManager struct {}

// displayProcess 显示进程
func (tm *taskManager) DisplayProcess() {}
// displayMemory 显示内存
func (tm *taskManager) DisplayMemory() {}

var tm *taskManager
var mutex sync.Mutex

// GetInstance 获取任务窗口单例
func GetInstance() *taskManager {
	if tm == nil {
		mutex.Lock()
		if tm == nil {
			tm = &taskManager{}
		}
		mutex.Unlock()
	}
	return tm
}

该实现在调用GetInstance时才会创建对象,提高了代码效率,并通过锁实现了并发安全。另外还有一种利用once.Do函数特性实现方式。

  • Go版本Once方式
import "sync"

// taskManager 任务窗口
type taskManager struct{}

// displayProcess 显示进程
func (tm *taskManager) displayProcess() {}

// displayMemory 显示内存
func (tm *taskManager) displayMemory() {}

var tm *taskManager
var once sync.Once

// GetInstance 获取任务窗口单例
func GetInstance() *taskManager {
	once.Do(func() {
		tm = &taskManager{}
	})
	return tm
}

once.Do是线程安全的全局只执行一次的函数,它能保持taskManager实例全局只创建一次,并且能确保当同时有多个创建动作时,只有一个创建动作可以被执行。

  • C++版本
class TaskManger {
 private:
  TaskManger() {};
  TaskManger(const TaskManger &);
  TaskManger(const TaskManger &&);
  TaskManger &operator=(const TaskManger &);
  ~TaskManger(){}

 public:
  static TaskManger* GetInstance() {
    static TaskManger tm;
    return &tm;
  }
  void DisplayProcess() {std::cout << this << std::endl;}
  void DisplayMemory() {}
};

class TaskManger {
 private:
  TaskManger() {};
  TaskManger(const TaskManger &);
  TaskManger(const TaskManger &&);
  TaskManger &operator=(const TaskManger &);
  ~TaskManger(){}

 public:
  static TaskManger* GetInstance() {
    static TaskManger tm;
    return &tm;
  }
  void DisplayProcess() {std::cout << this << std::endl;}
  void DisplayMemory() {}
};

static void createInstance() {
  TaskManger* tm = TaskManger::GetInstance();
  tm->DisplayProcess();
}

int main() {
    std::thread t1(createInstance);
    std::thread t2(createInstance);
    t1.join();
    t2.join();
    return 0;
}

该实现方式将构造函数、拷贝构造函数、右值拷贝构造函数和赋值运算符重载函数私有化,避免外部构造对象。并且利用c++函数的静态变量唯一性,可以保证TaskManager对象唯一性和线程安全。

2.工厂模式

工厂模式属于创建型模式,用于实例化对象,主要包含简单工厂模式、抽象工厂模式、工厂方法模式三种方式。

2.1 简单工厂模式

该方式不属于23种模式,只是为了引出后面两种模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。使用场景是对象的创建和使用分离,客户端只知道传入工厂类的参数,对于如何创建对象不关心,常用于工厂类负责创建的对象比较少的场景。例如设计一个数据库管理的类,负责MySQL,Redis和MongoDB的代理对象。

  • Go版本
type DBConf struct {
	Host   string
	Port   int
	User   string
	Passwd string
}

// DataBase 数据库
type DataBase struct {
	Name string
	Conf DBConf
}

// Select 数据查询
func (db DataBase) Select() {
	fmt.Printf("%s select data\n", db.Name)
}

func NewDataBase(name string, conf DBConf) DataBase {
	return DataBase{
		Name: name,
		Conf: conf,
	}
}

func main() {
	db := NewDataBase("MySQL", DBConf{})
	db.Select()
}
  • C++版本
struct DBConf {
 public:
  std::string host;
  int port;
  std::string user;
  std::string passwd;
};

class DataBase {
 public:
  std::string name;
  DBConf conf;

 public:
  void Select() {
    std::cout << name << " select data" << std::endl;
  }
};

通过调用NewDataBase,当传入所需参数后就能实例化对象,而在main函数中使用对象的函数,从而实现了对象的创建和使用分离。

2.2 工厂方法模式

工厂方法模式是一种创建型设计模式,将对象创建从由一个对象负责所有具体类的实例化,变成由一群子类来负责对具体类的实例化。其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。工厂方法模式与简单工厂模式的区别就在于,工厂方法模式通过使用多态特性来实现具体对象的创建工作,并且每个具体对象都有一个对应的具体对象创建工厂类,从而避免了简单工厂模式如果要新增一种产品就要修改工厂中创建产品的函数。例如创建mysql和redis两种不同类型的数据库

  • Factory:抽象工厂
  • ConcreteFactory:具体工厂
  • Product:抽象产品
  • ConcreteProduct:具体产品

image

  • Go版本
type DBConf struct {
	Host   string
	Port   int
	User   string
	Passwd string
}

// DataBase 数据库
type DataBase struct {
	name string
	conf DBConf
}

// Select 数据查询
func (db DataBase) Select() {
	fmt.Printf("%s select data\n", db.name)
}

func NewDataBaseFactory(name string) func(conf DBConf) DataBase {
	return func(conf DBConf) DataBase {
		return DataBase{
			name: name,
			conf: conf,
		}
	}
}

func main() {
	dbFactory := NewDataBaseFactory("MySQL")
	db := dbFactory(DBConf{Host: "127.0.0.1"})
	db.Select()

	redisFactory := NewDataBaseFactory("Redis")
	redis := redisFactory(DBConf{Host: "127.0.0.2"})
	redis.Select()
}
  • C++版本
struct DBConf {
 public:
  std::string host;
  int port;
  std::string user;
  std::string passwd;
};

class DataBase {
 public:
  std::string name;
  DBConf conf;

 public:
  virtual void Select() = 0;
};

// MySQL
class MySQLDataBase: public DataBase {
 public:
  void Select() {
    std::cout << name << " select data" << std::endl;
  }
};

// Redis
class RedisDataBase: public DataBase {
 public:
  void Select() {
    std::cout << name << " select data" << std::endl;
  }
};

class Factory {
 public:
  virtual std::shared_ptr<DataBase> CreateDataBase() = 0;
};

// 生产MySQL实例的工厂
class MySQLFactory: public Factory {
 public:
  virtual std::shared_ptr<DataBase> CreateDataBase() {
    return std::move(std::make_shared<MySQLDataBase>());
  }
};

// 生产Redis实例的工厂
class RedisFactory: public Factory {
 public:
  virtual std::shared_ptr<DataBase> CreateDataBase() {
    return std::move(std::make_shared<RedisDataBase>());
  }
};

int main() {
  std::shared_ptr<Factory> factory1 = std::make_shared<MySQLFactory>();
  std::shared_ptr<DataBase> database1 = factory1->CreateDataBase();
  database1->name = "MySQL";
  database1->conf = DBConf{"127.0.0.1"};
  database1->Select();

  std::shared_ptr<Factory> factory2 = std::make_shared<RedisFactory>();
  std::shared_ptr<DataBase> database2 = factory2->CreateDataBase();
  database2->name = "Redis";
  database2->conf = DBConf{"127.0.0.2"};
  database2->Select();
  return 0;
}

2.3 抽象工厂模式

抽象工厂模式是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品。如果你有一个基于一组抽象方法的类,且其主要功能因此变得不明确,那么在这种情况下可以考虑使用抽象工厂模式。就是说。如果代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,你不希望代码基于产品的具体类进行构建,在这种情况下你可以使用抽象工厂。例如创建能从数据库中查询数据的对象,数据库有很多种,MySQL,Redis等,只要能提供查询能力的数据库都可以被创建。

  • AbstractFactory:抽象工厂
  • ConcreteFactory:具体工厂
  • AbstractProduct:抽象产品
  • Product:具体产品

image

  • Go版本
type DataBase interface {
	Select()
}

type DBConf struct {
	Host   string
	Port   int
	User   string
	Passwd string
}

// MySQLDataBase MySQL数据库
type MySQLDataBase struct {
	name string
	conf DBConf
}

// Select 数据查询
func (db MySQLDataBase) Select() {
	fmt.Printf("MySQL %s select data\n", db.name)
}

// RedisDataBase Redis数据库
type RedisDataBase struct {
	name string
	conf DBConf
}

// Select 数据查询
func (db RedisDataBase) Select() {
	fmt.Printf("Redis %s select data\n", db.name)
}

func NewMySQLDataBase(name string, conf DBConf) DataBase {
	return MySQLDataBase{name: name, conf: conf}
}

func NewRedisDataBase(name string, conf DBConf) DataBase {
	return RedisDataBase{name: name, conf: conf}
}

func main() {
	mysql := NewMySQLDataBase("mysql", DBConf{Host: "127.0.0.1"})
	mysql.Select()

	redis := NewRedisDataBase("redis", DBConf{Host: "127.0.0.2"})
	redis.Select()
}
  • C++版本
struct DBConf {
 public:
  std::string host;
  int port;
  std::string user;
  std::string passwd;
};

class DB {
 public:
  std::string name;
  DBConf conf;

 public:
  virtual void Select() = 0;
};

class MySQL1: public DB {
 public:
  void Select() {
    std::cout << name << " select data from MySQL1" << std::endl;
  }
};

class MySQL2: public DB {
 public:
  void Select() {
    std::cout << name << " select data from MySQL2" << std::endl;
  }
};

class NoDB {
 public:
  std::string name;
  DBConf conf;

 public:
  virtual void Select() = 0;
};

class Redis1: public NoDB {
 public:
  void Select() {
    std::cout << name << " select data from Redis1" << std::endl;
  }
};

class Redis2: public NoDB {
 public:
  void Select() {
    std::cout << name << " select data from Redis2" << std::endl;
  }
};

class Factory {
 public:
  virtual std::shared_ptr<DB> CreateDB() = 0;
  virtual std::shared_ptr<NoDB> CreateNoDB() = 0;
};

// 工厂1
class Factory1: public Factory {
 public:
  virtual std::shared_ptr<DB> () {
    return std::move(std::make_shared<MySQL1>());
  }
  virtual std::shared_ptr<NoDB> CreateNoDB() {
    return std::move(std::make_shared<Redis1>());
  }
};

// 工厂2
class Factory2: public Factory {
 public:
  virtual std::shared_ptr<DB> CreateDB() {
    return std::move(std::make_shared<MySQL2>());
  }
  virtual std::shared_ptr<NoDB> CreateNoDB() {
    return std::move(std::make_shared<Redis2>());
  }
};

int main() {
  std::shared_ptr<Factory> fact1 = std::make_shared<Factory1>();
  std::shared_ptr<Factory> fact2 = std::make_shared<Factory2>();

  std::shared_ptr<DB> db1 = fact1->CreateDB();
  db1->Select();

  std::shared_ptr<DB> db2 = fact2->CreateDB();
  db2->Select();

  std::shared_ptr<NoDB> nodb1 = fact1->CreateNoDB();
  nodb1->Select();

  std::shared_ptr<NoDB> nodb2 = fact2->CreateNoDB();
  nodb2->Select();
}

3.模板方法模式

模板方法模式是指定义一个算法操作框架,而将算法的一些步骤延迟到子类中实现,允许子类在不修改结构的情况下重写算法的特定步骤,是属于行为型模式。使用场景:当你只希望扩展某个特定算法步骤,而不是整个算法或其结构时,可使用模板方法模式。例如对MySQL、Redis进行连接、读和处理三个步骤。

  • Go版本
type DBProcessor interface {
	connect()
	read()
	process()
}

func do(dbProcessor DBProcessor) {
	dbProcessor.connect()
	dbProcessor.read()
	dbProcessor.process()
}

type MySQL struct {
	name string
}

func (m MySQL) connect() {
	fmt.Printf("mysql name=%s connect\n", m.name)
}

func (m MySQL) read() {
	fmt.Printf("mysql name=%s read\n", m.name)
}
func (m MySQL) process() {
	fmt.Printf("mysql name=%s process\n", m.name)
}

type Redis struct {
	name string
}

func (r Redis) connect() {
	fmt.Printf("redis name=%s connect\n", r.name)
}

func (r Redis) read() {
	fmt.Printf("redis name=%s read\n", r.name)
}

func (r Redis) process() {
	fmt.Printf("redis name=%s process\n", r.name)
}

func main() {
	m := MySQL{name: "mysql1"}
	do(m)

	r := Redis{name: "redis1"}
	do(r)
}
  • C++版本
class DBProcessor {
 public:
  void templateMethod() {
    connect();
    read();
    process();
  }

  virtual void connect() = 0;
  virtual void read() = 0;
  virtual void process() = 0;

};

class MySQL : public DBProcessor {
 public:
  MySQL(std::string n) : name(n) {}
  virtual void connect() {
    std::cout << "MySQL connect name=" << name << std::endl;
  }

  virtual void read() {
    std::cout << "MySQL read name=" << name << std::endl;
  }

  virtual void process() {
    std::cout << "MySQL process name=" << name << std::endl;
  }

 private:
  std::string name;
};

class Redis : public DBProcessor {
 public:
  Redis(std::string n, std::string h) : name(n), host(h) {}
  virtual void connect() {
    std::cout << "Redis connect name=" << name << ", host=" << host << std::endl;
  }

  virtual void read() {
    std::cout << "Redis read name=" << name << ", host=" << host << std::endl;
  }

  virtual void process() {
    std::cout << "Redis process name=" << name << ", host=" << host << std::endl;
  }

 private:
  std::string name;
  std::string host;
};

int main() {
  std::shared_ptr<DBProcessor> p = std::make_shared<MySQL>("mysql1");
  p->templateMethod();
  std::shared_ptr<DBProcessor> p2 = std::make_shared<Redis>("redis1", "127.0.0.2");
  p2->templateMethod();
  return 0;
}

4.观察者模式

观察者模式是一种行为型模式,是对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。该模式可以将观察者和被观察者解耦,并定义了稳定的消息更新传递机制。使用场景:一般用于当一个对象状态改变需要影响到其他对象时,可使用观察者模式。例如实现一个简单的订阅发布模式。

  • Go版本
// Subscriber 订阅者
type Subscriber interface {
	update()
}

type SubscriberA struct {
	name string
}

func (sa *SubscriberA) update() {
	fmt.Printf("Subscriber name=%s, update msg\n", sa.name)
}

type SubscriberB struct {
	name string
	age  int
}

func (sb *SubscriberB) update() {
	fmt.Printf("Subscriber name=%s, age=%d, update msg\n", sb.name, sb.age)
}

// Publisher 发布者
type Publisher struct {
	subscribers []Subscriber
}

func (p *Publisher) AddSubscriber(s Subscriber) {
	p.subscribers = append(p.subscribers, s)
}

func (p *Publisher) MessageCome() {
	// 通知所有订阅者
	fmt.Println("msg come, notify all subscribers!")
	p.notifyAllSubscriber()
}

func (p *Publisher) notifyAllSubscriber() {
	for _, s := range p.subscribers {
		s.update()
	}
}

func main() {
	sa := &SubscriberA{name: "s1"}
	sb := &SubscriberB{name: "s2", age: 10}
	pub := &Publisher{}
	pub.AddSubscriber(sa)
	pub.AddSubscriber(sb)

	pub.MessageCome()
}
  • C++版本
class Subscriber {
 public:
  virtual void update() = 0;
};

class SubscriberA : public Subscriber {
 public:
  SubscriberA(std::string n) : name(n) {}
  void update() {
    std::cout << "Subscriber update msg, name=" << name << std::endl;
  }

 private:
  std::string name;
};

class SubscriberB : public Subscriber {
 public:
  SubscriberB(std::string n, int a) : name(n), age(a) {}
  void update() {
    std::cout << "Subscriber update msg, name=" << name << ", age=" << age << std::endl;
  }

 private:
  std::string name;
  int age;
};

class PublisherBase {
 public:
  virtual void AddSubscriber(Subscriber* s) = 0;
  virtual void MessageCome() = 0;
};

class Publisher : public PublisherBase {
 public:
  void AddSubscriber(Subscriber* s) override {
    subscribers.emplace_back(s);
  }

  void MessageCome() override {
    std::cout << "msg come, notify all subscribers!" << std::endl;
    notifyAllSubscriber();
  }
 private:
  std::list<Subscriber*> subscribers;
  void notifyAllSubscriber() {
    for (auto &item: subscribers) {
      item->update();
    }
  }
};

int main() {
  SubscriberA sa = SubscriberA("s1");
  SubscriberB sb = SubscriberB("s2", 10);
  Publisher pub;
  pub.AddSubscriber(&sa);
  pub.AddSubscriber(&sb);
  pub.MessageCome();
  return 0;
}
  • 黑板报通知机制

5.代理模式

代理模式是一种结构型设计模式,让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。常用于延迟初始化,如果有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时可使用代理模式。或者用于访问控制,如果只希望特定客户端使用服务对象,而客户端则是各种已启动的程序,此时可使用代理模式。例如加载一张很大的图片。

  • Go版本
type Image interface {
	Display()
}

type RealImage struct {
	FileName string
}

func NewRealImage(fileName string) *RealImage {
	ri := &RealImage{FileName: fileName}
	ri.loadFromDisk()
	return ri
}

func (ri *RealImage) Display() {
	fmt.Printf("displaying %s...\n", ri.FileName)
}

func (ri *RealImage) loadFromDisk() {
	fmt.Printf("Loading %s\n", ri.FileName)
}

type ProxyImage struct {
	FileName  string
	realImage *RealImage
}

func (pi *ProxyImage) Display() {
	if pi.realImage == nil {
		pi.realImage = NewRealImage(pi.FileName)
	}
	pi.realImage.Display()
	fmt.Printf("proxy real load %s done\n", pi.FileName)
}

func main() {
	image := ProxyImage{FileName: "test.jpg"}
	image.Display()  // 图像将从磁盘加载
	fmt.Println("again")
	image.Display() // 图像不需要从磁盘加载
}
  • C++版本
class Image {
 public:
  virtual void display() = 0;
};

class RealImage : public Image {
 public:
  RealImage(std::string n) : name(n) {
    loadFromDisk();
  }
  void display() {
    std::cout << "displaying name=" << name << std::endl;
  }

 private:
  std::string name;
  void loadFromDisk() {
    std::cout << "Loading name=" << name << std::endl;
  }
};

class ProxyImage : public Image {
 public:
  explicit ProxyImage(std::string n) : name(n) {}
  void display() {
    if (realImage == nullptr) {
      realImage = new RealImage(name);
    }
    realImage->display();
    std::cout << "proxy real load done, name=" << name << std::endl;
  }

 private:
  std::string name;
  RealImage* realImage;
};

int main() {
  Image* image = new ProxyImage("test.jpg");
  image->display();
  std::cout << "again" << endl;
  image->display();
  return 0;
}

上面两种实现方式只需要第一次调用display函数会加载图片,后面都不会加载,从而减少RealImage对象加载的内存占用。

6.组合模式

组合模式是一种结构型设计模式,可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。它模糊了简单元素和复杂元素的概念,程序可以像处理简单元素一样来处理复杂元素,从而使得程序与复杂元素的内部结构解耦。使用场景:一般在想表示对象的部分与整体层次结构(树形结构)时使用。或者希望用户忽略组合对象与单个对象的不同,将统一地使用组合结构中的所有对象。例如创建一个员工类,该类包含所属员工的对象列表。

  • Go版本
type Employee struct {
	Name         string
	subordinates []*Employee
}

func (e *Employee) AddSubordinate(employee *Employee) {
	e.subordinates = append(e.subordinates, employee)
}
func (e *Employee) RemoveSubordinate(i int) {
	if i < 0 || i >= len(e.subordinates) {
		return
	}
	if i == len(e.subordinates)-1 {
		e.subordinates = e.subordinates[:len(e.subordinates)-1]
	}
	e.subordinates = append(e.subordinates[:i], e.subordinates[i+1:]...)
}

func (e *Employee) GetSubordinates() []*Employee {
	return e.subordinates
}

func PrintEmployee(employee *Employee) {
	fmt.Println("employee name=", employee.Name)
	for i, e := range employee.GetSubordinates() {
		fmt.Printf("employee sub i=%d, sub name=%s\n", i, e.Name)
	}
}

func main() {
	e1 := &Employee{Name: "Amy"}
	e2 := &Employee{Name: "Bob"}
	e3 := &Employee{Name: "Marry"}
	manager := &Employee{Name: "Henry"}
	boss := &Employee{Name: "Boss"}
	boss.AddSubordinate(manager)
	boss.AddSubordinate(e1)
	boss.AddSubordinate(e2)
	boss.AddSubordinate(e3)
	manager.AddSubordinate(e1)
	manager.AddSubordinate(e2)
	PrintEmployee(boss)
	boss.RemoveSubordinate(2) // remove "Bob"
	PrintEmployee(boss)
}
  • C++版本
class Employee {
 public:
  Employee(std::string n) : name(n) {}
  void Add(Employee e) {
    subordinates.emplace_back(e);
  }
  void Remove(int i) {
    if (i >= subordinates.size() || i < 0) {
      return;
    }
    subordinates.erase(subordinates.begin()+i);
  }
  std::vector<Employee> GetSubordinates() {
    return subordinates;
  }
  std::string dump() const {
    std::stringstream ss;
    ss << "Employee name=" << name << endl;
    for (size_t i = 0; i < subordinates.size(); ++i) {
      ss << "employee sub i=" << i << ", sub name=" << subordinates[i].name << endl;
    }
    return ss.str();
  };
 private:
  std::string name;
  std::vector<Employee> subordinates;
};

int main() {
  Employee boss{"Boss"};
  Employee e1{"Amy"};
  Employee e2{"Bob"};
  Employee e3{"Marry"};
  Employee manager{"Henry"};
  boss.Add(manager);
  boss.Add(e1);
  boss.Add(e2);
  boss.Add(e3);
  manager.Add(e1);
  manager.Add(e2);
  std::cout << boss.dump();
  boss.Remove(2);
  std::cout << boss.dump();
  return 0;
}

参考文献

  • 《图说设计模式》

  • 《深入设计模式》

  • 《大话设计模式》

  • C++各类设计模式及实现详解 - 知乎 (zhihu.com)

  • 史上最全设计模式导学目录(完整版)_史上最全设计模式lovelion_LoveLion的博客-CSDN博客

  • 设计模式 | 菜鸟教程 (runoob.com)