0%

多线程设计模式类别概括

  • 不可变对象模式(Immutable Object)

线程安全性指的是多个线程对共享资源同时进行访问时,防止数据发生未定义的变化。

所谓的不可变对象就是没有机会去修改它,每一次修改都会产生一个新的对象。

  • 保护性暂挂模式(Guarded Suspension)

核心思想是仅当服务进程准备好时,才提供服务。

该模式主要成员有:Request、RequestQueue、ClientThread、ServerThread。

  1. Request:表示客户端请求
  2. RequestQueue:用于保存客户端请求队列
  3. ClientThread:客户端进程
  4. ServerThread:服务器进程

适用场景:一项任务被分解为多个不同的部分,每个线程完成不同的部分,这些线程相互协作时,会出现一个线程等待另一个线程一定操作后才能进行的场景。

  • 两阶段终止模式(Two-phase Termination)
  • 承诺模式(Promise)
  • 生产者-消费者模式(Producer-Consumer)
  • 主动对象模式(Active Object)
  • 线程池模式(Thread Poll)
  • 线程特有存储(Thread Specific Storage)
  • 串行线程封闭模式(Serial Thread Confinement)
  • 主仆模式(Master-Slave)
  • 流水线模式(Pipeline)
  • 半同步/半异步模式(Half-sync/Half-async)

Use Case是什么?

用况的概念最早是Jacobson提出的,他给出的定义是:一个用况是通过使用系统功能的某些部分而使用系统的一种具体形式。每个用况包括一个由参与者发动的完整事件过程。它详细说明了参与者与系统之间发生的交互。因此,一个用况是一个由参与者和系统在一次对话中执行的特定的相关事物序列。

《面向对象分析与设计》给出的定义是:参与者使用系统的一项功能时所进行的交互过程的描述,其中包括由双方交替执行的一系列动作。

理解Use Case定义

这两者的定义是有些许差别的,我更倾向于使用后者的定义。因为前者认为用况是由参与者发动的完整事件过程。但是如果系统内部发现异常,那么系统需要作为动作发起者,通知参与者做出外部干预,那么这一系列动作的发起者不是外部参与者,而是系统本身,所以前者定义不够全面。

回到第二个用况定义,深入理解它所表达的含义。

  • 一个用况只描述参与者对单独一项功能的使用情况。如果一个功能可以拆成许多较小的功能(仍是完整的),则应该使用多个用况描述。举一个简单的例子,银行系统中的资金管理功能,这不是一个用况能描述清楚的。
  • 用况陈述了参与者与系统在交互过程中双方所做的事,而不是单独一方所做的事。如果多个功能都进行了同样的操作,而这个操作不能被操作者直接启动的话,书中是不建议单独分出一个用况的(不必要的),我觉得有时分出来也不是完全不行(暂时理解不到为什么把这种情况给去掉了)

avatar

  • 在所有用况中,由参与者发起的对话最为常见,但是有些对话也可能是系统发起的。
  • 用况的典型用法是描述参与者和系统彼此为了对方做了什么,而是描述怎么做!
  • 一个用况可以由多种参与者使用。
  • 一项系统功能需要多个参与者同时交互才能使用。

如何定义Use Case

要想分析用况,首先需要将自己当作参与者,然后与设想中的系统进行交互,考虑以下几个问题:

  • 进行这种交互是为了使用系统的什么功能
  • 使用该功能的目的是什么
  • 为达目的,需要向系统输入什么信息(信息的传递过程即为交互
  • 希望系统进行怎样的处理并从它那得到什么结果(系统内部行为

交互行为连接了功能分析和用况分析,即参与者/系统会主动做出那些交互行为,从交互起点入手、分析以此为起点的一系列动作。

要点:

  1. 全面的了解和收集用户所要求的各项系统功能,确定系统边界,找出所有参与者,向用户和有关专家了解各项功能的相关业务流程。
  2. 把用户提出的功能组织成合适的单位,即:一次交互完成一个完整而独立的功能。(需要多次交互才能完成的功能可以认为是前置的多个交互(用况)完成后的单次交互结果,比如:第一,拍照功能。你需要打开相机、调整焦距、按下快门。那么拍照这个功能可以分解为3个用况,因为你进行了3次交互。第二,银行的转账,这之间其实也包含了多次交互,输入目标账号、金额,点击转账,输入密码,点击确定。那么这个需要分成4个用况吗?分的太散显然是不合理的,因为输入密码和点击确认虽然是两次交互动作,但是这两个动作是密不可分的,所以可以是转账用况(基用况)包含了密码确认用况(包含用况))。
  3. 穷举每一类参与者所使用的每一项系统功能,定义相应的用况。
  4. 检查用户对系统的各项功能需求是否都通过用况得到了描述。

个人感觉从不同的参与者需要与系统进行交互的种类为入手点好思考一些。因为交互连接了系统与参与者,每次交互都是为了完成某一项功能,它连接了需求分析和功能分析。

之后还有一些用况之间的关系,比如包含、延伸、泛化等,这些分类方法可以在已有用况中分离部分动作作为基础用况,让整体的模型更为生动。当然泛化关系是不建议使用,因为这种机制难以描述清楚在什么位置、什么条件下插入被包含的用况,倒不如使用包含的关系,由一个基用况来判断在什么条件下使用哪一个子用况。

Use Case图绘制要素

一个完整的用况图应该包含系统、参与者、用况和关系。

Systems

系统就是开发的所有东西。它可以是网站、软件组件、应用程序或者其他。

通常使用一个矩形来代表系统。

示意图

这个矩形定义了此系统的范围,此矩形内的所有内容都在银行这个应用中发生。

Actors

通常使用一个火柴人来表示参与者。

示意图

Actor将是系统实现目标的某人、某物甚至外系统。

在银行App中常见的参与者有顾客和银行本身。那么顾客和银行有什么区别呢?谁是主要的使用者,谁是被动接受者呢?

在系统中 Primary Actors是 Initiates the use of the system,而 Secondary Actors 是 Reactionary。比如此例中顾客是主要参与者,银行是次要参与者。顾客开始提出存款、取款、查询等要求,银行被动给予回应。

示意图

一般将主要参与者放置在左侧,次要参与者放置在右侧。

Use Cases

用况是真正开始需求建模的地方。一般用一个椭圆形代表一个用况,表示要做的一件工作。

示意图

那么银行App能做些什么呢?这里将功能进行简化。

  • 允许客户登录,检查账户余额,转账,存款,按发票付款。

示意图

每个用况的开头都是动词。如果想对系统中的用例进行更为详细的描述,比如此用况已经发生,那么对于分析来说十分混乱,所以最好按逻辑顺序排列用况。如上所述,log in是其他所有用况的起点,但是在描述其他用况的时候不会再提及登录操作,并且将log in放在最前面,因为这事顾客使用App时需要做的第一件事。。

Relationships

最后一个要素是关系。参与者使用系统来实现需求,因此每个参与者必须与系统中至少一个用况进行交互。在上述示例中,顾客登录到银行应用程序,因此在顾客和log in用况之间用直线相连——关联关系。

示意图

除去关联关系外还有包含关系、延伸关系和泛化关系。

在顾客输入他们的登录信息时,银行应用会验证密码,如果密码不正确,银行将向应用程序发送一条错误信息。

所以我们需要再加上两个用例,检查密码正确性和发送密码错误信息。

档顾客想要转账时,银行首先需要验证账号里的钱是否充足,因此需要再加上一个用例。

示意图

那么密码验证用况和整个图表的其他部分有什么关系呢?我们的参与者都没有直接发起这项动作,但是每次进行登录操作时它的确会运行。所以这就是一个包含关系,意思是每当执行主要用况时,也会执行包含的用况。一般使用指向包含用况的虚线箭头表示这种包含关系。

另一种重要的关系时延伸关系。延伸包含两方,基本用况和延伸用况。当执行基本用况时,有时会发生用况延伸,有时不会,仅当满足特定条件才会使用延伸用况。比如上述用况中的展示密码错误用况。延伸关系使用一条指向基本用况的带箭头的虚线表示。

示意图

延伸与包含的关系也可以简单描述为包含关系的两个用况一定同时运行,延伸关系的两个用况不一定同时运行。

值得一提的是,多个基本用况可以包含相同的扩展用况。如转账和支付都需要检查账号的钱够不够。

示意图

Use Cases的好处

  • 用例图是一种功能需求文档编辑技术,它将功能作为一个黑盒子引出,其中包含所有具有访问权限或角色的用户
  • 以一种简单且非技术性的方式呈现,易于所有技术和业务用户理解
  • 将客户和所有其他用户带到了同一个界面上,使交流变得容易
  • 将一个大型复杂项目表示为一组小型功能
  • 从最终用户的角度呈现,便于开发人员理解业务目的
  • 参与者和其他外部应用程序之间的关联使系统健康验证所需的验证和检查更加清晰
  • 用例驱动的项目开发和跟踪方法有助于从功能准备的角度评估项目的进度。关键开发活动状态使项目负责人能够从客户可交付成果的角度展示准备情况
  • 项目开发可根据关键的可交付功能进行优先排序,以便于更好地控制和管理项目收入

Multiplicity Of Use Case And Actor

用例可以和多个参与者关联,这叫做用况的多重性。如下图的例子,查看课程可以和两个参与者关联——“新用户”和“注册用户”

示意图

Multiplicity Of  Actor

  • 参与者的多重性是一个由数字表示的关联,可以是0到任何数字
  • 多重性为零——代表用况可能没有参与者
  1. 当通过现金支付处理课程支付用例时,将不需要银行支付服务。因此,参与者“银行支付服务”的多重性可以为零
  2. 要查看课程时,必须有一名参与者“新用户”,因此此关联的多重性为一
  • 多重性为一——代表用况必须有一个参与者

  • 多重性大于一——意味着用况实例中可能涉及多个参与者。多个参与者可以同时关联,也可以在不同的时间点或顺序关联

  1. 参与者超过一个的情况是罕见的。考虑一个马拉松竞赛游戏的用例图,其中多个玩家在给定的比赛实例中同时运行。(这里的描述是转载自<网络链接>。虽然参与者实例不止一个,但是多个玩家都是一类,都是参赛者,所以此处的多重性的分离究竟意义多大呢)
  2. 考虑一个象棋游戏的用况。在国际象棋比赛中,两名棋手将按顺序关联,因为每个棋手采取的步骤不是平行的,而是顺序的。
  3. 在描述单个接力赛团队活动的用例图中,多个玩家将在不同的时间点关联。在一次比赛中,一个队的所有队员在不同的时间点都处于活动的状态。(如果以一个队为单位呢,参与者是一个队呢?

绘制用例图之前的准备

  1. 将项目分解为多个小功能
  • 了解大型复杂项目,将其分解为多个功能,并开始记录每个功能的细节
  1. 确定目标并确定优先顺序
  • 开始列出每个功能,并确定该功能要实现的目标

  • 根据业务可交付成果计划对已识别的功能进行优先顺序

  1. 功能范围

了解功能的范围并绘制系统边界

  • 识别所有需要称为系统一部分以实现目标的用况

  • 列出在系统中具有角色的所有参与者(用户和服务)。

  1. 确定关联和关系
  • 明确用况和参与者之间的关系和相互依赖性
  1. 确定包含用况和延伸用况
  • 列出所有带有延伸的用况/包含的用况
  1. 识别用况/参与者多样性

  2. 命名用况和参与者

  • 遵循命名用况和参与者的标准

  • 在文档的特定部分总结用况的功能的简要细节以及可以访问用况的参与者

  1. 重要注意事项
  • 使用注释强调要点
  1. 回顾检查
  • 开始绘制用例之前,检查并验证文档

实例

项目名称:在线培训网站

  • 项目中的参与者列表

示意图

示意图

  • 项目中的用况列表

示意图

  • 系统列表

示意图

  • 绘制用况图:分步指南

第一步:

  • 绘制系统边界并命名系统

示意图

第二步:

  • 通过参考“List of System”部分中的“Allowed Actor”绘制参与者,并根据项目标准对其命名
  • 参与者“New-User”、“Registered-User”和“Employee-Cashier”是系统的主要参与者
  • 另外两个支持服务参与者,即“Bank-Payment-Service”(银行支付服务)和“User-Authentication-Service”(用户认证服务)是支持参与者

示意图

第三步:

通过参考“List of System”中的“Use Case names”,在系统范围内绘制用例

示意图

第四步:

通过参考文档中的“List of Use Cases”部分,为范围内用况添加包含和延伸用况。“Join-a- Course”包含“Course-payment”和“View-Courses”。

用“Register-help”和“Location-Search-help”帮助描述“Register-User”

如图所示,添加note功能以提供详细信息

示意图

第五步:

在参与者和用况之间建立联系。文档中的“List of Use Cases”中的“Allowed Actors/Multiplicity number of Actor”给出了所有参与者和用况之间的关联。

用况允许某些参与者在系统中没有任何的职责,比如说讲师,他们可以访问用况“查看课程”,但是当前描述的系统中没有任何职责。

示意图

我与编程-Matlab

编程是一件让人可以偷懒的能力,这是我最开始学习编程的原因。大概是2018年,我进入现在的课题组开始研究生的生活。什么让我最困扰呢?大量等待着处理的数据,这些数据的格式都是差不多的,前期处理方法也是千篇一律,按道理来说是很简单的任务,但是量实在是太大了,往往一天的数据足够一个人处理两三天,而且是重复着的机械般的动作。这个时候我就想能不能把这些固定步骤的计算交给计算机做呢,于是开始了我的编程之路。

首先进入我眼中的是Matlab,因为大学的时候简单的学习过它,对它有一定了解,所以首先从Matlab入手了。我记得当时遇到的第一个困难是如何将大量的txt 文本数据读进Matlab的工作空间(当时实在是太菜了)。

示意图

然后按照一定的顺序将每个文本的数据内容做一些加减乘除运算,然后对目标数据进行一个单指数拟合,最终得到系数。就是这样的一个功能,我写了大约3天。

示意图

不过当时我可自豪了,产生了想将课题组的数据处理方法整合起来的想法,也就是构建一个的数据处理软件。为了让大家更好的使用,我必须要考虑图形界面,于是我开始了Matlab GUI的学习(花费了2个星期左右)。从啥也不明白到了解了每一个控件就是调用一个函数,要想设置控件就必须要得到控件的句柄,得到句柄后查看帮助文档了解如何对控件进行操作,即属性和回调函数的设置。到这里我写了一个简单的计算器,一个处理气溶胶数据的简单运算的图形界面(从没见人用过)。

示意图

后面因为我自己的实验就中断了编程的学习,直到光学部分已经看到部分成果后,我决定开始二维光谱图像的处理,也就是编写一个光谱仪信息采集处理软件,首选平台Matlab(因为我就会这个)。于是找了相机的教程,把教程的代码段抄下来,勉勉强强完成了设置相机参数、打开光谱仪、采集数据、显示数据等。但不是程序一会死机了(现在明白了是内存溢出了),就是运行着程序越来越慢。于是我开始网上搜怎样才能同时做到这几件事呢?我认为的答案是多线程技术,可是Matlab只支持并行计算,并不支持多线程编程,没办法我只能另求出路,java、python、C++,我选择了C++,因为它是最难的。

我与编程-C++

开始的我不知天高地厚,天真的以为自学了Matlab,那么C++也同样不在话下!真的好傻啊。最开始我买了本c++ primer直接开啃,慢慢的我学习语法两个多月后,看了忘,忘了看,连网上随随便便一道算法题都完不成后,我开始思考我是不是学错了,到底怎么学习?当时恰逢春节,我回家之后和我表弟打赌,比的是谁能完成一个c++课程设计,是一个公司管理系统,一个总公司管理着下属的20个子公司。我先完成了,用的手法就很low啦,这是我第一次在c++平台上完成一件事,对我的激励还挺强的。

示意图

不过现在看来还是写的很差的,不过好歹实现了不是。

对学习编程来说,真正第一次有所感悟是学习Stanford CS106B,因为这门课是将算法、数据结构和语法放在一起讲的,循序渐进,从一个一个问题中,我慢慢理解到了数据结构的美,算法的精妙之处,如何实现模版类之类的知识让我对STL之类的类库再也没有那么害怕。

中介者模式

不同类之间的通信大部分是通过直接引用或者指针引用的方式,但是在某些情况下,我们不希望对象之间知道彼此的存在。比如飞机场的降落管控事件,我们不希望不同的飞机间通过相互商议的形式决定谁先降落、谁后降落,而是希望他们听从地面管控中心的统一管理。这就是中介者模式机制所面向的问题。即一个对象管理多个对象协同工作>的关系。

网络聊天室

网络聊天室是一个经典案例。

参与者

聊天室中的参与者可以简单描述为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person{
Person(const string& name);
string name;
ChatRoom* room = nullptr;
vector<string>chat_log;
void receive(const string& origin, const string& message){
string s{ origin + ": \"" + message + "\"" };
cout << "[" << name << "'s chat session] " << s << endl;
chat_log.emplace_back(s);
};
void say(const string& message) const{
room->broadcast(name, message);
};
void pm(const string& who, const string& message) const{
room->message(name, who, message);
};
};

这个类包含一个构造函数、名字、指向聊天室的指针、聊天记录、三个成员函数。

receive()允许我们接收消息,通常还会在屏幕上显示以及将内容保存到消息日志中。

say()允许此人向房间中每个人发送消息。

pm()是私人消息传递功能,您需要指定消息所针对的人的姓名。

say()和pm()会将操作转到聊天室。

聊天室

那么让我们来实现一个简单的聊天室。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ChatRoom{
vector<Person*> people; // assume append-only
void join(Person* p){
string join_msg = p->name + " joins the chat";
broadcast("room", join_msg);
p->room = this;
people.push_back(p);
};
void broadcast(const string& origin, const
string& message){
for (auto p : people)
if (p->name != origin)
p->receive(origin, message);
};
void message(const string& origin, const string& who,const string& message){
auto target = find_if(begin(people), end(people),[&](const Person* p) { return p->name == who; });
if (target != end(people)){
(*target)->receive(origin, message);
}
};
};

ChatRoom API非常简单,join()让一个人加入房间(暂时只进不出,后面讨论线程安全问题时再考虑)broadcast()将消息发送给其他所有人、message()发送私人消息。

源代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>
#include<vector>
#include<string>

using namespace std;

//前置声明,因为ChatRoom需要使用Person类的成员
class Person;

//聊天室类,保存有当前聊天室内人员信息,允许某个成员发送全体消息,允许人员加入,允许人员发送私信
class ChatRoom{
public:
vector<Person*> people;
void join(Person* p);
//接口函数声明,类中声明与实现分离,在编译器编译时不会报错,因为实现的代码在Person类完整声明之后
void broadcast(const string& origin, const
string& message);
void message(const string& origin, const string&who,const string& message);
};

//参与者类,可以发送消息、接受消息、保存聊天记录,拥有个人标识以及所在房间标识
class Person{
public:
Person(const string& name):name(name){};
string name;
ChatRoom*room = nullptr;
vector<string>chat_log;
void receive(const string& origin,const string& message){
string s{origin + ": \"" + message + "\""};
cout<< "[" << name << "'s chat session] " << s << endl;
chat_log.emplace_back(s);
};
void say(const string& message){
room->broadcast(name, message);
};
void pm(const string& who, const string& message){
room->message(name, who, message);
};
};
//类方法实现部分
void ChatRoom::join(Person *p){
string join_msg = p->name + " joins the chat";
broadcast("room", join_msg);
p->room = this;
people.push_back(p);
};
void ChatRoom::broadcast(const string &origin, const string &message){
for (auto p : people)
if (p->name != origin)
p->receive(origin, message);
};
void ChatRoom::message(const string &origin, const string &who, const string &message){
auto target = find_if(begin(people), end(people),[&](const Person* p) { return p->name == who; });
if (target != end(people)){
(*target)->receive(origin, message);
}
};
//主函数部分
int main()
{
ChatRoom room;
Person john{ "john" };
Person jane{ "jane" };
room.join(&john);
room.join(&jane);
john.say("hi room");
jane.say("oh, hey john");
Person simon("simon");
room.join(&simon);
simon.say("hi everyone!");
jane.pm("simon", "glad you could join us, simon");
return 0;
}

在聊天室的示例中,每当有人发布消息时,参与者都需要通知。即是说:中介者拥有一个所有参与者共享的事件,参与者可以订阅该事件以接收通知,他们也可以触发该事件,从而触发通知。

优点

中介者模式建议停止组件之间的相互联系而使它们相互独立。这些组件之间的交流必须调用特殊的中介者对象, 通过中介者对象重定向调用行为, 以间接的方式进行合作。 最终, 组件仅依赖于一个中介者类, 无需与多个其他组件相耦合。能让你减少对象之间混乱无序的依赖关系。

如资料编辑表单, 对话框 (Dialog) 类本身将作为中介者。

示意图
绝大部分重要的修改都在实际表单元素中进行。 如提交按钮,当用户点击按钮后, 它必须对所有表单元素数值进行校验。如果使用中介者模式,它只需要发送消息给中介者即可,中介者会自行校验数值并将任务委派下去,这样一来,按钮不再与其他表单元素相关联。 在 MVC 模式中, 控制器是中介者的同义词。在 C++ 代码中中介者模式最常用于帮助程序 GUI 组件之间的通信。

下面以真实编码示例中的UI中各个控件之间的中介者模式编码为例。

1
google

Observer模式

本文参考现代C++设计模式-观察者模式章节

观察者模式是一种非常流行的、实用的设计模式。它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个对象,当这个对象的状态发生变化时,会通知所有观察者,使它们能够自动更新自己。

尽管观察者模式极为重要,但C++和标准库中都没有现成的实现。

下面将就一个安全的、正确实现的观察者模式的结构进行深入阐述。

以生日为例,当某人长大一岁时,我们可能要祝贺她的生日,但是要怎么做呢?

1
2
3
4
5
6
7
8
9
class Person{
public:
explicit Person(int age) : age{age} {};
void set_age(int value){
age = value;
};
int get_age(){
return age;
};

当年龄发生改变时,我们希望在set_age的地方对所有关心她的人发消息,然后做出一系列不同的操作,那么怎么连接关心她的人和她呢?这就是设计模式——观察者模式解决的问题:一对多的依赖关系

观察者

方法是定义某种基类,任何对她生日感兴趣的人都继承该基类。

1
2
3
4
5
template<typename T>class Observer{
virtual void field_changed(T& source,const string&field_name){
//doing_something();
}
};

上述代码是一个类模版的写法,Observer类有一个名为T的模版类型参数,用来表示Observer保存的元素类型,其中第一个参数是对其字段实际更改的对象的引用,第二个是字段的名称。如此,继承该模版类的实现将允许其他对象观察到该类的更改。

观察者类

1
2
3
4
5
class PersonObserver:Observer<Person>{
void field_changed(Person& source, const string&field_name) override{
cout<<"Person's " << field_name << " has changed to "<<source.get_age()<<endl;
}
};

继承Oberver类的类中方法可以被重写,进而用于观察多个类的属性变化。代码示例如下:

1
2
3
4
5
class PersonObserver:Observer<Person>,Observer<Creature>
{
void field_changed(Person& source, ...) { ... }
void field_changed(Creature& source, ...) { ... }
};

以上是对观察者类的定义,即观察别人的类的描写,那么被观察者应该做些什么呢?

被观察者

在此问题中应考虑的是Person类的职责。

  • 内部保存一个所有对Person类的改变感兴趣的观察者列表
  • 提供增加/删除订阅者的操作
  • 在字段发生改变时通知所有观察者

当然这些功能都可以很好的被移动到一个单独的基类中,以增加代码的复用性。

1
2
3
4
5
6
7
8
9
10
template <typename T> public class Observable{
void notify(T& source, const string& name) {
for(auto obs:observers)
obs->field_changed(source,"age");
}
void subscribe(Observer<T>* f) { observers.push_back(f); }
void unsubscribe(Observer<T>* f) { ... }
private:
vector<Observer<T>*> observers;
};

当然仅仅是继承该类还是不够的,我们还需要在字段更改时调用notify方法,Person类应被改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person:public Observable<Person>{
public:
explicit Person(int age):age(age){};
void set_age(int age){
if(this->age==age)return;
this->age=age;
notify(*this,"age");
}
int get_age(){
return age;
}

private:
int age;
};

现在我们就可以在这些基础上实现 观察者模式了。

源代码实现

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <vector>

using namespace std;

//观察者模版类,职责是当被观察者类发出消息时执行所对应的操作
template<typename T> class Observer{
public:
virtual void field_changed(T& source, const string&field_name) = 0;
};
//被观察者模版,职责是存放对被观察者感兴趣的观察者列表,维护列表的增删改查,通知观察者
template <typename T> class Observable{
public:
void notify(T& source, const string& name) {
for(auto obs:observers)
obs->field_changed(source,"age");
}
void subscribe(Observer<T>* f) { observers.push_back(f);}
void unsubscribe(Observer<T>* f) {}
private:
vector<Observer<T>*> observers;
};
//继承被观察者类模版的类,注意要想在外部使用模版类的函数,必须使用公有继承。在需要通知的地方调用通知函数
class Person:public Observable<Person>{
public:
explicit Person(int age):age(age){};
void set_age(int age){
if(this->age==age)return;
this->age=age;
notify(*this,"age");
}
int get_age(){
return age;
}
private:
int age;
};
//继承观察者类模版的类,同样注意公有继承。override的字段使得其改变了模版类中的函数格式。
class PersonObserver:public Observer<Person>{
void field_changed(Person& source, const string&field_name) override{
cout<<"Person's " << field_name << " has changed to "<<source.get_age()<<endl;
}
};
//主函数部分,构建了观察者与被观察者。
int main()
{
Person p(20);
PersonObserver cpo;
p.subscribe(&cpo);
p.set_age(21);
p.set_age(23);
return 0;
}

显示结果为:

1
2
Person's age has changed to 21
Person's age has changed to 23

如果不关心属性依赖关系、线程安全和可复用问题的话,上面的实现模式可以很好的被应用。由于某些原因,我很关心线程安全问题。

线程安全问题

上面的实现中忽略的问题是:观察者如何取消对某一被观察对象的订阅。这在单线程中十分简单。

1
2
3
4
5
6
void unsubscribe(Observer<T>* observer)
{
observers.erase(
remove(observers.begin(), observers.end(), observer),
observers.end());
};

虽然在技术上是正确的,但是在多线程中同时调用订阅函数和取消订阅函数会导致未定义行为出现,这可能出现意想不到的后果。

书中提到了两种解决方案:

第一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class Observalbe{
void notify(T&source,const string& name){
scoped_lock<mutex> lock{ mtx };
}
void subscribe(Observer<T>* f){
scoped_lock<mutex> lock{ mtx };
}
void unsubscribe(Observer<T>* o){
scoped_lock<mutex> lock{ mtx };
}
private:
vector<Observer<T>*> observers;
mutex mtx;
}

第二种:

实用TPL/PPL中concurrent_vector之类的东西,当然,这个时候你添加对象的顺序不是他们得到通知的顺序,好处是不用再管理锁了。

值得一提的是:如果需要被订阅的对象和订阅对象的关系是一个不变量的话,线程安全问题就不需要考虑了。

一些问题

上述源代码毫无疑问是实现了观察者模式、而且是使用类模版这种十分轻松的方式,但是我在实际问题的使用中遇到了一些困难。比如:

老师在考场监督学生这一情景。我的思路是构建老师类和学生类,分别继承观察者类模版和被观察者类模版。

然后在notify函数的具体使用时发现了问题,观察者和被观察者都是Person类型的。所以在notify的模版类定义中不需要考虑notify第一项的指针类型,因为观察者和被观察者都是同一类型的。但是学生与老师这个问题中,被观察者模版中使用的应该是学生类(可被学生类观察),而且notify的自我指针引用时应该是老师类,但在类模版声明是是学生类,所以编译器无法通过。

我想的办法是将被观察者模版类的类型参数变为两个,一个是被观察者自身,一个是观察者,这样就可以将两者分开。我添加之后是这样的情况:

avatar

avatar

这里我其实有点不理解,this指针是Teacher类之中的,为什么是Student类型的指针??

我对模版类的理解还是太浅了,所以想用模版类实现不同类型的观察者对同一类型的被观察者暂时放置一边,而采用抽象类接口的方式实现观察者模式。

抽象类继承实现

思路:

  • 将需要被观察的量抽象为被观察者抽象类,包含添加观察者、删除观察者、通知观察者和内部的观察者信息
  • 将观察者抽象为观察者类,具体为根据不同的对象,重载不同的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//观察者抽象类
class Observer{
public:
virtual void updata(const std::string) = 0;
};
//被观察者抽象类
class Observable{
public:
void notify(std::string name){
for(auto obs:observers){
obs->updata(name);
}
}
void addObserver(Observer* ob){
observers.push_back(ob);
}
private:
std::vector<Observer*> observers;
};

这样就构建了两个抽象类,分别是观察者和被观察者,它们的职责是很好理解的,那么下面我将实现不同类型的观察者对同一被观察者的同一个动作做出不同的回应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Teacher:public Observable{
public:
void exam_start(){
std::cout<<"exam start"<<std::endl;
notify("exam_start");
}
};

class Student:public Observer{
public:
void updata(std::string str){
if(str =="exam_start"){
std::cout<<name<<" start_write"<<std::endl;
}
}
std::string name;
explicit Student(std::string name):name(name){};
};

class bigTeacher:public Observer{
public:
void updata(std::string str){
if(str =="exam_start"){
std::cout<<name<<" I know"<<std::endl;
}
}
std::string name;
explicit bigTeacher(std::string name):name(name){};
};

int main()
{
Teacher a;
Student b = Student("zhang");
Student c = Student("Li");
bigTeacher d = bigTeacher("hah");
a.addObserver(&b);
a.addObserver(&c);
a.addObserver(&d);
a.exam_start();
return 0;
}

上面我实现了两个不同的观察者,学生和巡考老师,他们监听考试开始的信号,并作出回应。

以上的这种方式可以看成某个对象的自身状态的改变而引起其他对象做出相应的回应这样一种写法。这个被观察者可以是某个实体对象的某个属性,也可以是某个动作,某个事件。

观察者模式应用场景

当某一方的行为依赖另一方的行为或者状态时,可以使用观察者模式松耦合联动双方,使得一方的变动可以通知到感兴趣的另一方对象,从而让另一方对象做出回应。

适用情形:

  1. 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  2. 当一个模型有两个方面,其中一个依赖另一个时,可将两者封装在独立的对象中使它们可以各自独立的改变和复用。
  3. 实现类似广播机制的功能,不需要知道具体收听者,只需要发广播
  4. 多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域通知(超过两种观察者)