Python 设计模式实践教程(五)
十九、模型-视图-控制器模式
总是,世界中的世界。——可利夫·巴克,编织世界
在您作为程序员的旅程中,您开始看到一些其他模式的出现——更像是模式的模式。就像这个。即使不是所有的程序,也是大部分由某种启动程序的外部动作组成。发起动作可能有数据,也可能没有数据,但是如果有,程序会使用这些数据,有时会持久化,有时不会。最后,你的计划对世界产生了影响。基本上就是这样;你将遇到的所有程序都有这种模式。
让我给你一个简单的例子,然后是一个你可能从一开始就看不到的例子。
第一个例子是访问者注册接收来自网站的电子邮件更新。用户如何输入信息并不重要,因为从系统的角度来看,一切都是从点击程序的POST请求开始的。该请求包含姓名和电子邮件地址。现在程序像前面的例子一样被初始化,我们有了一些额外的数据。该程序会检查电子邮件是否已经在数据库中。如果不是,它将与访问者的姓名一起保存。如果是,则不采取进一步的行动。最后,系统返回一个状态代码,警告调用程序一切正常。请注意,程序在两个地方改变了程序本身执行之外的世界,即数据库和对调用系统的响应。
我们也可以看一个游戏,游戏处于某种状态,玩家通过键盘和鼠标与游戏互动。这些外围动作被游戏引擎翻译成命令,这些命令被处理,并且游戏状态被更新,游戏状态以视觉、听觉和可能的触觉反馈的形式被传递回给玩家。流程保持不变:输入、处理、输出。
以这种方式看程序是有帮助的,因为你变得更善于询问过程中每一步发生了什么。遗憾的是,这种观点过于笼统,遗漏了太多内容,因此在实际的具体开发工作中,它并不完全有用。尽管如此,它仍然是一个有用的工具。
这本书叫做实用 Python 设计模式,而不是抽象编程模型,所以让我们实际一点。
没有完全抛弃程序的三个部分的想法,不同类型的程序对这个想法有不同的观点。游戏执行一个游戏循环,而 web 应用接受用户输入,并使用该输入和某种形式的存储数据来产生一个输出,显示在用户的浏览器窗口中。
出于本章的考虑,我们将使用一个命令行虚拟程序来演示我希望您能够理解的想法。一旦您清楚地掌握了基本思想,我们将使用几个 Python 库构建一个基本的 web 应用来实现它们,该应用可以从您的本地机器上提供 web 页面。
我们的第一个程序只是将一个名字作为参数。如果名称已经存储,它将欢迎访问者回来;否则,它会告诉来访者很高兴见到他们。
import sys
def main(name):
try:
with open('names.dat', 'r') as data_file:
names = [x for x in data_file.readlines()]
except FileNotFoundError as e:
with open('names.dat', 'w') as data_file:
data_file.write(name)
names = []
if name in names:
print("Welcome back {}!".format(name))
else:
print("Hi {}, it is good to meet you".format(name))
with open('names.dat', 'a') as data_file:
data_file.write(name)
if __name__ == "__main__":
main(sys.argv[1])
程序使用标准库中的sys包来公开传递给程序的参数。第一个参数是正在执行的脚本的名称。在我们的示例程序中,第一个额外的参数是一个人的名字。我们包含了一些检查,确保一些字符串确实被传递给了程序。接下来,我们检查程序以前是否遇到过这个名字,并显示相关的文本。
现在,由于整本书讨论的所有原因,我们知道这并不好。这个脚本在主函数中做了太多的事情,所以我们继续把它分解成不同的函数。
import sys
import os
def get_append_write(filename):
if os.path.exists(filename):
return 'a'
return 'w'
def name_in_file(filename, name):
if not os.path.exists(filename):
return False
return name in read_names(filename)
def read_names(filename):
with open(filename, 'r') as data_file:
names = data_file.read().split('\n')
return names
def write_name(filename, name):
with open(filename, get_append_write(filename)) as data_file:
data_file.write("{}\n".format(name))
def get_message(name):
if name_in_file('names.dat', name):
return "Welcome back {}!".format(name)
write_name('names.dat', name)
return "Hi {}, it is good to meet you".format(name)
def main(name):
print(get_message(name))
if __name__ == "__main__":
main(sys.argv[1])
我敢肯定,您怀疑在清理这些代码方面还会有更多的工作要做,那么为什么还要再次经历这个过程呢?遵循这些步骤的原因是,我想让你知道什么时候需要解构一个函数或方法。如果所有的程序都保持在项目开始时计划的方式,就不需要这种何时以及如何将事情分成更小部分的意识。按照同样的逻辑,这个过程是渐进的。你可能会像我们一样,把程序分成不同的功能。过了一段时间,程序变得很大,现在单个文件变成了一个负担,这是一个好的迹象,表明您需要将单个文件的程序分解成单独的文件。但是你要怎么做呢?随着我们最初计划的发展,我们将会看到。
他们说永远做你自己,除非你能成为一头狮子,在这种情况下,永远做一头狮子。
因此,本着这种精神,我们现在将扩展程序来显示某些参数的特殊消息,特别是 lion。
import sys
import os
def get_append_write(filename):
if os.path.exists(filename):
return 'a'
return 'w'
def name_in_file(filename, name):
if not os.path.exists(filename):
return False
return name in read_names(filename)
def read_names(filename):
with open(filename, 'r') as data_file:
names = data_file.read().split('\n')
return names
def write_name(filename, name):
with open(filename, get_append_write(filename)) as data_file:
data_file.write("{}\n".format(name))
def get_message(name):
if name == "lion":
return "RRRrrrrroar!"
if name_in_file('names.dat', name):
return "Welcome back {}!".format(name)
write_name('names.dat', name)
return "Hi {}, it is good to meet you".format(name)
def main(name):
print(get_message(name))
if __name__ == "__main__":
main(sys.argv[1])
现在,我们终于开始看到另一种模式的出现。这里我们有三种不同类型的函数,它们是我们尽可能分离关注点的结果。我们最终得到了处理返回问候的代码,将问候写入控制台的代码,以及一些处理程序流的代码,将相关请求发送到数据检索代码,获取问候,并将其发送到最终在控制台上显示它的代码。
模型-视图-控制器框架
总的来说,我们希望在一个可重用的模式中捕获上一段中讨论的模式。我们还想将开闭原则融入到代码中,为此,我们想将程序的关键部分封装在对象中。
在任何事情发生之前,我们的程序必须收到某种形式的请求。这个请求必须被解释,然后开始相关的动作。所有这些都发生在一个只为控制程序流而存在的对象内部。该对象处理请求数据、接收数据和向命令行发送响应等操作。正在讨论的对象都是关于控制的,不出所料,这类对象被称为控制器。它们是将系统结合在一起的粘合剂,通常也是大多数活动发生的地方。
这是我们之前程序中的控制函数。
controller_functions.py
def name_in_file(filename, name):
if not os.path.exists(filename):
return False
return name in read_names(filename)
def get_message(name):
if name_in_file('names.dat', name):
return "Welcome back {}!".format(name)
write_name('names.dat', name)
return "Hi {}, it is good to meet you".format(name)
if __name__ == "__main__":
main(sys.argv[1])
这些还没有封装在一个对象中,但至少它们在一个地方。现在,让我们继续进行系统的下一部分,在我们回到它们并清理这些部分之前,对功能进行分组。
一旦接收到请求,控制器决定必须对请求进行什么处理,就需要来自系统的一些数据。由于我们正在处理共享功能的代码的分组,我们现在将获取所有与数据检索有关的函数,并将它们放入一个函数文件中。通常,数据的结构化表示被称为数据模型,因此程序中处理数据的部分被称为模型。
这是我们之前程序中的模型函数。
model_functions.py
def get_append_write(filename):
if os.path.exists(filename):
return 'a'
return 'w'
def read_names(filename):
with open(filename, 'r') as data_file:
names = data_file.read().split('\n')
return names
def write_name(filename, name):
with open(filename, get_append_write(filename)) as data_file:
data_file.write("{}\n".format(name))
一旦代码被封装到一个对象中,你会发现随着程序的增长,需要不同类型的数据,每一种数据都被放入它自己的model对象中。通常,model对象最终都在各自的文件中。
剩下的是专注于向用户传递某种信息的代码。在我们的例子中,这是通过控制台的标准输出端口完成的。正如您现在已经猜到的,这被称为视图代码。
以下是我们之前程序中剩余的视图函数。
view_functions.py
def main(name):
print(get_message(name))
我相信你已经注意到,职能的划分并不像我们希望的那样明确。理想情况下,您应该能够改变任何一组函数中的任何内容(不改变它们的接口),并且这不应该对其他模块有任何影响。
在我们进行清晰的划分并将这三类功能封装到单独的类中之前,让我们看看进入模式的元素。
控制器
控制器是模式的核心,是编组所有其他类的部分。用户与控制器交互,并通过它控制整个系统。控制器接受用户输入,处理所有的业务逻辑,从模型中获取数据,并将数据发送到视图,以转换成返回给用户的表示。
class GenericController(object):
def __init__(self):
self.model = GenericModel()
self.view = GenericView()
def handle(self, request):
data = self.model.get_data(request)
self.view.generate_response(data)
模型
模型处理数据:获取数据、设置数据、更新数据和删除数据。就这样。你会经常看到模特做的不止这些,这是个错误。您的模型应该是数据的程序端接口,它抽象出与数据存储直接交互的需要,允许您从基于文件的存储切换到某种键值存储或完整的关系数据库系统。
通常,模型会包含用作对象属性的字段,允许您像与任何其他对象一样与数据库进行交互,使用某种save方法将数据保存到数据存储中。
我们的GenericModel类只包含一个简单的方法来返回某种形式的数据。
class GenericModel(object):
def __init__(self):
pass
def get_data(self, request):
return {'request': request}
视图
与模型一样,您不希望视图中有任何业务逻辑。视图应该只处理传递给它的数据的输出或呈现,将其转换成某种返回给用户的格式,可以是对控制台的打印语句或对游戏玩家的 3D 呈现。输出的格式对视图的功能没有太大的影响。您还会尝试为视图添加逻辑;这是一条滑坡,不是你应该走的路。
下面是一个简单的GenericView类,您可以使用它作为构建自己的视图的基础,无论是将 JSON 返回到 HTTP 调用还是绘制图表进行数据分析。
class GenericView(object):
def __init__(self):
pass
def generate_response(self, data):
print(data)
将这一切结合在一起
最后,这里有一个完整的程序,使用模型、视图和控制器来让您了解这些元素是如何交互的。
import sys
class GenericController(object):
def __init__(self):
self.model = GenericModel()
self.view = GenericView()
def handle(self, request):
data = self.model.get_data(request)
self.view.generate_response(data)
class GenericModel(object):
def __init__(self):
pass
def get_data(self, request):
return {'request': request}
class GenericView(object):
def __init__(self):
pass
def generate_response(self, data):
print(data)
def main(name):
request_handler = GenericController()
request_handler.handle(name)
if __name__ == "__main__":
main(sys.argv[1])
现在我们已经清楚了每个对象类应该是什么样子,让我们根据这些对象类来实现本章示例中的代码。
正如我们到目前为止所做的,我们将从Controller对象开始。
controller.py
import sys
from model import NameModel
from view import GreetingView
class GreetingController(object):
def __init__(self):
self.model = NameModel()
self.view = GreetingView()
def handle(self, request):
if request in self.model.get_name_list():
self.view.generate_greeting(name=request, known=True)
else:
self.model.save_name(request)
self.view.generate_greeting(name=request, known=False)
def main(main):
request_handler = GreetingController()
Request_handler.handle(name)
if __name__ == "__main__":
main(sys.argv[1])
接下来,我们创建Model对象,该对象从文件中检索问候语或返回标准问候语。
model.py
import os
class NameModel(object):
def __init__(self):
self.filename = 'names.dat'
def _get_append_write(self):
if os.path.exists(self.filename):
return 'a'
return 'w'
def get_name_list(self):
if not os.path.exists(self.filename):
return False
with open(self.filename, 'r') as data_file:
names = data_file.read().split('\n')
return names
def save_name(self, name):
with open(self.filename, self._get_append_write()) as data_file:
data_file.write("{}\n".format(name))
最后,我们创建一个View对象来向用户显示问候。
view.py
class GreetingView(object):
def __init__(self):
pass
def generate_greeting(self, name, known):
if name == "lion":
print("RRRrrrrroar!")
return
if known:
print("Welcome back {}!".format(name))
else:
print("Hi {}, it is good to meet you".format(name))
太好了。如果您想提出请求,请像这样启动程序:
$ python controller.py YOUR_NAME
第一次运行该脚本时,响应与预期的一样,如下所示:
Hi YOUR_NAME, it is good to meet you
在我们继续之前,我想让你明白为什么这是个好主意。假设我们增加了我们的程序,并且在某个时候我们决定根据一天中的时间来包含不同类型的问候。为此,我们可以向系统添加一个时间模型,然后系统将检索系统时间,并根据时间决定是上午、下午还是晚上。该信息与姓名数据一起发送给视图,以生成相关的问候语。
controller.py
class GreetingController(object):
def __init__(self):
self.name_model = NameModel()
self.time_model = TimeModel()
self.view = GreetingView()
def handle(self, request):
if request in self.name_model.get_name_list():
self.view.generate_greeting(
name=request,
time_of_day=self.time_model.get_time_of_day(),
known=True
)
else:
self.name_model.save_name(request)
self.view.generate_greeting(
name=request,
time_of_day=self.time_model.get_time_of_day(),
known=False
)
view.py
class GreetingView(object):
def __init__(self):
pass
def generate_greeting(self, name, time_of_day, known):
if name == "lion":
print("RRRrrrrroar!")
return
if known:
print("Good {} welcome back {}!".format(time_of_day, name))
else:
print("Good {} {}, it is good to meet you".format(time_of_day, name))
models.py
class NameModel(object):
def __init__(self):
self.filename = 'names.dat'
def _get_append_write(self):
if os.path.exists(self.filename):
return 'a'
return 'w'
def get_name_list(self):
if not os.path.exists(self.filename):
return False
with open(self.filename, 'r') as data_file:
names = data_file.read().split('\n')
return names
def save_name(self, name):
with open(self.filename, self._get_append_write()) as data_file:
data_file.write("{}\n".format(name))
class TimeModel(object):
def __init__(self):
pass
def get_time_of_day(self):
time = datetime.datetime.now()
if time.hour < 12:
return "morning"
if 12 <= time.hour < 18:
return "afternoon"
if time.hour >= 18:
return "evening"
或者,我们可以使用数据库来存储问候,而不是将它们保存在不同的文件中。这一切的美妙之处在于,我们在存储方面所做的事情与系统的其他部分无关。为了说明这一事实,我们将修改我们的模型,将问候语存储在一个 JSON 文件中,而不是存储在纯文本中。
实现这个功能需要标准库中的json包。
将这种方法应用到编程中最困难的部分是,您必须决定什么去哪里。您在模型中放置了多少内容,或者在视图中进行了什么级别的处理?在这里,我喜欢胖控制器和瘦模型和视图的概念。我们希望模型只处理数据的创建、检索、更新和删除。视图应该只关心显示数据。其他的都应该放在控制器里。有时,您可能会为控制器实现助手类,这很好,但是不要让自己受到诱惑,在模型或视图中做了不应该做的事情。
一个模型做得太多的经典例子是一个模型需要另一个模型,比如一个UserProfile类需要一个User类,这样你就知道这个概要文件属于哪个用户。诱惑在于将所有信息发送给UserProfile模型的构造器,让它动态创建User实例;毕竟都是数据。
这将是一个错误,因为您将让模型控制程序的某些流程,而这些流程应该依赖于控制器及其助手类。更好的解决方案是让控制器创建User实例,然后强制它将User对象传递给UserProfile模型类的构造函数。
同样的问题也可能出现在图片的视图侧。要记住的关键是,每个对象都应该有一个责任,而且只能有一个责任。如果它向用户显示信息,它不应该对信息进行计算;如果它正在处理数据,它不应该关心如何动态地创建缺失的部分;如果它在控制流程,它就不应该关注数据是如何存储或检索的,或者数据是如何格式化或显示的。
临别赠言
您在对象中分离关注点越干净,并且您越好地隔离它们的目的,维护您的代码、更新它或者发现和修复 bug 就越容易。
在你迈向编程大师的旅程中,你应该带着的最重要的概念之一是忒修斯之船的悖论。悖论是这样陈述的:想象你有一艘船,船上有足够的木材来完全重建这艘船。船出发了,一路上,船上的每一块木材都被船上的一些存货所替代。被替换的部分被丢弃。当船到达目的地时,原来船上的一块木板都没有了。问题是,你在什么时候有了一艘新船?这艘船什么时候不再是你出发时的船了?
这和编程有什么关系?
你应该以这样一种方式构建你的程序,即你可以不断地交换程序的一些部分,而不改变代码的任何其他部分。每个时期——取决于项目的规模和范围,这可能意味着一年、五年或六个月——您都希望淘汰所有旧代码,并用更好、更优雅、更高效的代码替换它。
不断地问自己,用完全不同的东西替换你当前正在处理的代码部分会有多痛苦。如果答案是您希望在继续前进之前不需要这样的替换,那么撕掉代码,重新开始。
练习
- 从问候程序的最后一个实现中换出视图,以在图形弹出窗口中显示其输出;您可以查看 pygame 或 pyqt,或者任何其他可用的图形包。
- 将模型改为读写 JSON 文件,而不是平面文本文件
- 在本地系统上安装 SQLAlchemy 和 SQLite,并换出
model对象,这样它就使用数据库而不是文件系统。
二十、发布-订阅模式
大喊大叫是出版的一种形式。—玛格丽特·阿特伍德
如果你回想一下我们在第十四章中看到的观察者模式,你会记得我们有一个Observable类和一些Observer类。每当Observable对象改变其状态并被轮询是否有变化时,它会提醒所有向它注册的观察者,他们需要激活一个回调。
没有必要跳回那一章,因为我们正在讨论的代码在下面的代码片段中:
class ConcreteObserver(object):
def update(self, observed):
print("Observing: {}".format(observed))
class Observable(object):
def __init__(self):
self.callbacks = set()
self.changed = False
def register(self, callback):
self.callbacks.add(callback)
def unregister(self, callback):
self.callbacks.discard(callback)
def unregister_all(self):
self.callbacks = set()
def poll_for_change(self):
if self.changed:
self.update_all
def update_all(self):
for callback in self.callbacks:
callback(self)
尽管观察者模式允许我们将被观察的对象从了解观察它们的对象中分离出来,但是观察者对象仍然需要知道他们需要观察哪些对象。因此,与上一章相比,我们仍然有更多的耦合,在上一章中,我们能够在不需要改变控制器或模型代码的情况下改变视图代码,模型代码也是如此。即使是位于模型和视图之间的控制器,也与两者完全分离。当我们改变视图时,我们不需要更新控制器,反之亦然。
当观察者需要注册的潜在可观察类的数量有限时,我们之前处理的观察者模式工作得很好。与编程中的许多事情一样,我们经常面临不符合理想情况的挑战,因此我们需要适应。
在这一章中,我们要寻找的是一种将观察者从可观察事物中分离出来的方法。我们不希望观察者和被观察者知道彼此的任何事情。每个类及其实例都应该能够改变,而不需要等式另一端的任何改变。新的观察者和可观察对象不应该要求修改系统中其他类的代码,因为这会导致我们的代码不太灵活。为了达到这一理想状态,我们将寻找将观察者模式的一对多方法扩展到可观察对象和观察者之间的多对多关系的方法,这样一类对象不需要太了解另一类。
这些盲目的观察者将被称为发布者,而断开的观察者将被称为订阅者。
第一步可能看起来微不足道,但你很快就会意识到改变你对事物的看法是多么简单,只要改变你对它们的称呼。
class Subscriber(object):
def update(self, observed):
print("Observing: {}".format(observed))
class Publisher(object):
def __init__(self):
self.callbacks = set()
self.changed = False
def register(self, callback):
self.callbacks.add(callback)
def unregister(self, callback):
self.callbacks.discard(callback)
def unregister_all(self):
self.callbacks = set()
def poll_for_change(self):
if self.changed:
self.update_all
def update_all(self):
for callback in self.callbacks:
callback(self)
就像我说的,这对于程序实际上能做什么没有影响,但是它确实能帮助我们以不同的方式看待事物。我们现在可以向前迈进一步,重命名这两个类中的方法,以更好地反映发布者-订阅者模型。
class Subscriber(object):
def process(self, observed):
print("Observing: {}".format(observed))
class Publisher(object):
def __init__(self):
self.subscribers = set()
def subscribe(self, subscriber):
self.subscribers.add(subscriber)
def unsubscribe(self, subscriber):
self.subscribers.discard(subscriber)
def unsubscribe_all(self):
self.subscribers = set()
def publish(self):
for subscriber in self.subscribers:
subscriber.process(self)
现在很清楚,我们正在处理两个类,一个处理发布,另一个完全专注于处理某些东西。在代码片段中,还不清楚应该发布什么,应该处理什么。我们继续尝试通过添加一个可以发布和处理的Message类来清除模型定义中的任何不确定性。
class Message(object):
def __init__(self):
self.payload = None
class Subscriber(object):
def process(self, message):
print("Message: {}".format(message.payload))
class Publisher(object):
def __init__(self):
self.subscribers = set()
def subscribe(self, subscriber):
self.subscribers.add(subscriber)
def unsubscribe(self, subscriber):
self.subscribers.discard(subscriber)
def unsubscribe_all(self):
self.subscribers = set()
def publish(self, message):
for subscriber in self.subscribers:
subscriber.process(message)
接下来,我们通过添加一个所有消息都将通过的 dispatcher 类来删除发布者和订阅者之间的最后一个链接。目标是有一个发布者发送消息的单一位置。同一位置保存了所有订户的索引。结果是发布者和订阅者的数量可以变化,而不会对系统的其余部分产生任何影响。这给了我们一个非常干净和分离的架构。
class Message(object):
def __init__(self):
self.payload = None
class Subscriber(object):
def __init__(self, dispatcher):
dispatcher.subscribe(self)
def process(self, message):
print("Message: {}".format(message.payload))
class Publisher(object):
def __init__(self, dispatcher):
self.dispatcher = dispatcher
def publish(self, message):
self.dispatcher.send(message)
class Dispatcher(object):
def __init__(self):
self.subscribers = set()
def subscribe(self, subscriber):
self.subscribers.add(subscriber)
def unsubscribe(self, subscriber):
self.subscribers.discard(subscriber)
def unsubscribe_all(self):
self.subscribers = set()
def send(self, message):
for subscriber in self.subscribers:
subscriber.process(message)
并非所有订阅者都对来自所有发布者的所有消息感兴趣。因为整个练习的目的是将订阅者和发布者分离,所以我们不想以任何方式将它们耦合在一起。我们正在寻找的解决方案应该允许调度程序向特定的订阅者发送消息类别。我们将在邮件中添加一个主题。发布者现在将在他们发布的消息中添加一个主题,然后订阅者可以订阅特定的主题。
class Message(object):
def __init__(self):
self.payload = None
self.topic = "all"
class Subscriber(object):
def __init__(self, dispatcher, topic):
dispatcher.subscribe(self, topic)
def process(self, message):
print("Message: {}".format(message.payload))
class Publisher(object):
def __init__(self, dispatcher):
self.dispatcher = dispatcher
def publish(self, message):
self.dispatcher.send(message)
class Dispatcher(object):
def __init__(self):
self.topic_subscribers = dict()
def subscribe(self, subscriber, topic):
self.topic_subscribers.setdefault(topic, set()).add(subscriber)
def unsubscribe(self, subscriber, topic):
self.topic_subscribers.setdefault(topic, set()).discard(subscriber)
def unsubscribe_all(self, topic):
self.subscribers = self.topic_subscribers[topic] = set()
def send(self, message):
for subscriber in self.topic_subscribers[message.topic]:
subscriber.process(message)
def main():
dispatcher = Dispatcher()
publisher_1 = Publisher(dispatcher)
subscriber_1 = Subscriber(dispatcher, 'topic1')
message = Message()
message.payload = "My Payload"
message.topic = 'topic1'
publisher_1.publish(message)
if __name__ == "__main__":
main()
现在我们看到了订户打印的消息(如下所示)中的代码。
Message: My Payload
分布式消息发送器
从许多机器发送信息。现在我们已经清楚地实现了 PubSub 模式,让我们使用目前为止开发的思想来构建我们自己的简单消息发送器。为此,我们将使用 Python 标准库中的socket包。我们使用 XML-RPC 来处理远程连接;它允许我们调用远程过程,就好像它们是本地系统的一部分。为了让我们的 dispatcher 处理消息,我们需要将字典作为参数发送,而不是作为 Python 对象,我们就是这样做的。下面的代码说明了我们如何做到这一点。
dispatcher.py
from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
class Dispatcher(SimpleXMLRPCServer):
def __init__(self):
self.topic_subscribers = dict()
super(Dispatcher, self).__init__(("localhost", 9000))
print("Listening on port 9000...")
self.register_function(self.subscribe, "subscribe")
self.register_function(self.unsubscribe, "unsubscribe")
self.register_function(self.unsubscribe_all, "unsubscribe_all")
self.register_function(self.send, "send")
def subscribe(self, subscriber, topic):
print('Subscribing {} to {}'.format(subscriber, topic))
self.topic_subscribers.setdefault(topic, set()).add(subscriber)
return "OK"
def unsubscribe(self, subscriber, topic):
print('Unsubscribing {} from {}'.format(subscriber, topic))
self.topic_subscribers.setdefault(topic, set()).discard(subscriber)
return "OK"
def unsubscribe_all(self, topic):
print('unsubscribing all from {}'.format(topic))
self.subscribers = self.topic_subscribers[topic] = set()
return "OK"
def send(self, message):
print("Sending Message:\nTopic: {}\nPayload: {}".format(message["topic"], message["payload"]))
for subscriber in self.topic_subscribers[message.get("topic", "all")]:
with ServerProxy(subscriber) as subscriber_proxy:
subscriber_proxy.process(message)
return "OK"
def main():
dispatch_server = Dispatcher()
dispatch_server.serve_forever()
if __name__ == "__main__":
main()
publisher.py
from xmlrpc.client import ServerProxy
class Publisher(object):
def __init__(self, dispatcher):
self.dispatcher = dispatcher
def publish(self, message):
with ServerProxy(self.dispatcher) as dispatch:
dispatch.send(message)
def main():
message = {"topic": "MessageTopic", "payload": "This is an awesome payload"}
publisher = Publisher("http://localhost:9000")
publisher.publish(message)
if __name__ == "__main__":
main()
subscriber.py
from xmlrpc.client import ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
class Subscriber(SimpleXMLRPCServer):
def __init__(self, dispatcher, topic):
super(Subscriber, self).__init__(("localhost", 9001))
print("Listening on port 9001...")
self.register_function(self.process, "process")
self.subscribe(dispatcher, topic)
def subscribe(self, dispatcher, topic):
with ServerProxy(dispatcher) as dispatch:
dispatch.subscribe("http://localhost:9001", topic)
def process(self, message):
print("Message: {}".format(message.get("payload", "Default message")))
return "OK"
def main():
subscriber_server = Subscriber("http://localhost:9000", "MessageTopic")
subscriber_server.serve_forever()
if __name__ == "__main__":
main()
要运行程序并看到它的真实效果,您必须打开三个终端窗口,并在每个窗口中为您的代码激活虚拟环境。我们将在自己的窗口中运行应用的每个部分,以模拟独立系统之间的交互。下面的每个命令都假设 virtualenv 已经被激活。
窗口 1:调度员
$ python dispatcher.py
窗口 2:订户
$ python subscriber.py
窗口 3:发布者
$ python publisher.py
随着每个窗口运行它的进程,让我们在 publisher 窗口中输入一条消息,看看会发生什么。
我们看到调度程序收到了消息并将其发送给了订阅者。
Listening on port 9000...
Subscribing http://localhost:9001 to MessageTopic
127.0.0.1 - - [16/Aug/2017 20:59:06] "POST /RPC2 HTTP/1.1" 200 -
Sending Message:
Topic: MessageTopic
Payload: This is an awesome payload
127.0.0.1 - - [16/Aug/2017 20:59:09] "POST /RPC2 HTTP/1.1" 200 -
订户接收消息并打印该消息。
Listening on port 9001...
Message: This is an awesome payload
127.0.0.1 - - [16/Aug/2017 20:59:09] "POST /RPC2 HTTP/1.1" 200 -
临别赠言
PubSub 模式有许多用途,尤其是当系统扩展并发展到云中时。这种成长、扩展和进化的过程是我选择从我们在第十四章中看到的观察者模式的简单实现开始,然后通过改进设计的步骤移动到最终的完整 PubSub 实现的原因之一。这是你将在自己的项目中重复的相同的增长模式。一个简单的项目增长了一点点,随着你添加的每一点功能,你应该问自己从这本书或其他地方得到的什么想法会使这个项目在将来更容易维护。一旦你有了这样的想法,不要犹豫——改进代码。您将很快意识到,在项目的末尾花一点时间会使您在项目的未来阶段的开发时间显著缩短。
正如 Jocko Willink(海豹突击队海军 Bruser 特遣部队指挥官)喜欢说的,纪律等于自由。您应用到编码实践中的纪律将使您从维护繁琐系统的痛苦中解脱出来。您还将避免不可避免的转储和重写,这是大多数软件产品从编写第一行代码开始就要做的事情。
练习
- 看看能否找到关于 proto 缓冲区的信息,以及如何使用它们向远程过程调用发送数据。
- 阅读 ZeroMQ 和 RabbitMQ 等排队软件,以及芹菜等软件包。使用其中之一实现简单的聊天应用,而不是本章中的 Python 实现。
- 就如何使用发布-订阅模式将游戏化添加到现有的“软件即服务- SaaS”应用中写一份建议书。