Python-设计模式学习指南第二版-全-

Python 设计模式学习指南第二版(全)

原文:zh.annas-archive.org/md5/1a7ba6927088f234ef6923ffc2ff7c93

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

设计模式是构建大型软件系统中最强大的方法之一。随着对优化软件架构和设计的关注度不断提高,软件架构师在设计或架构层面考虑对象创建、代码结构和对象间交互的优化变得尤为重要。这确保了软件维护成本低,代码易于重用且易于适应变化。此外,提供可重用性和关注点分离的框架对于当今的软件开发至关重要。

本书涵盖内容

第一章, 设计模式简介,详细介绍了面向对象编程的基础,并讨论了面向对象设计原则。本章简要介绍了设计模式的概念,以便你能够理解设计模式在软件开发中的背景和应用。

第二章, 单例设计模式,涵盖了在应用开发中使用的最简单且广为人知的创建型设计模式之一——单例设计模式。本章还介绍了在 Python 中创建单例模式的不同方法,并附有示例。本章还涵盖了单态(或博格)设计模式,它是单例设计模式的一种变体。

第三章, 工厂模式 – 构建工厂以创建对象,讨论了另一种创建型设计模式,即工厂模式。你还将通过 UML 图、现实场景和 Python 3.5 实现学习工厂方法模式和抽象工厂模式。

第四章, 外观模式 – 使用外观模式进行适应性设计,展示了另一种类型的设计模式,即结构型设计模式。我们将介绍外观的概念,并借助外观设计模式了解其在软件开发中的应用。你还将通过一个使用现实场景的示例 Python 应用程序学习其实现方式。

第五章, 代理模式 – 控制对象访问,处理的是属于结构设计模式的代理模式。我们将介绍代理作为一个概念,并讨论设计模式以及它在软件开发中的应用。你还将了解代理模式的多种变体——虚拟代理、智能代理、远程代理和保护代理。

第六章, 观察者模式 – 保持对象知情,讨论了第三种设计模式——行为设计模式。我们将通过示例介绍观察者设计模式。在本章中,你将学习如何实现观察者模式的推送和拉取模型以及松耦合的原则。我们还将看到当应用于云应用和分布式系统时,这个模式是如何至关重要的。

第七章, 命令模式 – 封装调用,介绍了命令设计模式。我们将介绍命令设计模式,并讨论它在软件开发中的应用,包括实际场景和 Python 实现。我们还将研究命令模式的两个主要方面——重做/回滚操作的实施和异步任务执行。

第八章, 模板方法模式 – 封装算法,讨论了模板设计模式。与命令模式一样,模板模式属于行为模式。在这里,我们讨论模板方法模式,并会通过实现了解钩子。我们还将涵盖好莱坞原则,这有助于我们更好地欣赏这个模式。

第九章, 模型-视图-控制器 – 复合模式,讨论了复合模式。我们将介绍模型-视图-控制器设计模式,并讨论它在软件开发中的应用。MVC 可能是最常用的设计模式之一;实际上,许多 Python 框架都是基于这个原则。你将通过一个用 Python Tornado(Facebook 使用的框架)编写的示例应用程序来了解 MVC 实现的细节。

第十章, 状态设计模式,向你介绍状态设计模式,它属于行为模式类别,就像命令或模板设计模式一样。我们将讨论它在软件应用开发中的应用。

第十一章, 反模式,告诉你关于反模式——作为建筑师或软件工程师,我们不应该做什么。

你需要这本书的内容

你只需要 Python v3.5,你可以从www.python.org/downloads/下载它。

这本书面向的对象

这本书是为关心软件设计原则和 Python 应用开发细节的 Python 开发人员和软件架构师而写的。它需要基本的编程概念理解以及入门级的 Python 开发经验。对于在实时学习环境中的学生和教师也会有帮助。

规范

在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称显示如下:“Car对象将具有fuel levelisSedanspeedsteering wheelcoordinates等属性。”

代码块设置为如下:

class Person(object):
    def __init__(self, name, age):  #constructor
        self.name = name    #data members/ attributes
        self.age = age
    def get_person(self,):   # member function
         return "<Person (%s, %s)>" % (self.name, self.age)

p = Person("John", 32)    # p is an object of type Person
print("Type of Object:", type(p), "Memory Address:", id(p))

新术语重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“在 Python 中,封装(数据和方法隐藏)的概念不是隐式的,因为它没有像 C++或 Java 这样的语言中所需的publicprivateprotected等关键字来支持封装。”

注意

警告或重要注意事项以如下框的形式出现。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们的读者反馈总是受欢迎的。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出你真正能从中获得最大价值的标题。

要给我们发送一般性反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果你在某个主题上具有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从你的购买中获得最大价值。

下载示例代码

您可以从www.packtpub.com下载示例代码文件,这是您购买的所有 Packt Publishing 书籍的账户。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support,并注册以将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

侵权

互联网上对版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 与我们联系,并提供疑似侵权材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面所提供的帮助。

询问

如果您对本书的任何方面有问题,您可以联系我们的 <questions@packtpub.com>,我们将尽力解决问题。

第一章:设计模式简介

在本章中,我们将介绍面向对象编程的基础,并详细讨论面向对象设计原则。这将为我们准备书中后面章节涉及的高级主题。本章还将简要介绍设计模式的概念,以便您能够欣赏设计模式在软件开发中的背景和应用。在这里,我们也将设计模式分为三个主要方面——创建型模式、结构型模式和和行为型模式。因此,本质上,本章将涵盖以下内容:

  • 理解面向对象编程

  • 讨论面向对象设计原则

  • 理解设计模式及其分类和背景的概念

  • 讨论动态语言的模式

  • 对模式进行分类——创建型模式、结构型模式和和行为型模式

理解面向对象编程

在开始学习设计模式之前,了解基础知识并熟悉 Python 中的面向对象范式总是好的。面向对象的世界提出了具有属性(数据成员)和程序(成员函数)的对象概念。这些函数负责操作属性。例如,以Car对象为例。Car对象将具有fuel level(燃油水平)、isSedan(是否为轿车)、speed(速度)和steering wheel(方向盘)以及coordinates(坐标)等属性,方法包括accelerate()(加速)以增加速度和takeLeft()(向左转)以使汽车向左转。Python 自从首次发布以来就是面向对象的编程语言。正如他们所说,Python 中的万物都是对象。每个类实例或变量都有自己的内存地址或标识符。作为类的实例,对象之间相互交互,以服务于正在开发的应用程序。理解面向对象编程的核心概念涉及理解对象、类和方法的概念。

对象

以下要点描述对象:

  • 它们代表您正在开发的应用程序中的实体。

  • 实体之间相互交互以解决现实世界问题。

  • 例如,Person 是一个实体,Car 也是一个实体。Person 驾驶 Car 从一个地点移动到另一个地点。

类帮助开发者表示现实世界实体:

  • 类通过属性和行为定义对象。属性是数据成员,行为通过成员函数体现

  • 类由构造函数组成,这些构造函数为这些对象提供初始状态

  • 类就像模板,因此可以很容易地重用

例如,Person类将具有name(姓名)和age(年龄)属性以及gotoOffice()(去办公室)成员函数,该函数定义了他去办公室工作的行为。

方法

以下要点讨论面向对象世界中方法的作用:

  • 它们代表对象的行为

  • 方法在属性上工作,并实现所需的功能

这里给出了在 Python v3.5 中创建的一个类和对象的良好示例:

class Person(object):
    def __init__(self, name, age):  #constructor
        self.name = name    #data members/ attributes
        self.age = age
    def get_person(self,):   # member function
         return "<Person (%s, %s)>" % (self.name, self.age)

p = Person("John", 32)    # p is an object of type Person
print("Type of Object:", type(p), "Memory Address:", id(p))

前面代码的输出应该如下所示:

方法

面向对象编程的主要方面

现在我们已经了解了面向对象编程的基础,让我们深入了解面向对象编程的主要方面。

封装

封装的关键特性如下:

  • 对象的行为对外部世界或对象保持隐藏,或者对象保持其状态信息私有。

  • 客户端不能通过直接作用于对象来改变对象的内部状态;相反,客户端通过发送消息来请求对象。根据请求的类型,对象可能会通过使用特殊的成员函数(如 getset)来改变其内部状态。

  • 在 Python 中,封装(数据和方法隐藏)的概念不是隐式的,因为它没有像 C++ 或 Java 这样的语言中所需的 publicprivateprotected 等关键字来支持封装。当然,可以通过在变量或函数名前加 __ 来使访问性变为私有。

多态

多态的主要特性如下:

  • 多态可以是两种类型:

    • 对象根据输入参数提供方法的不同实现

    • 同一个接口可以被不同类型的对象使用

  • 在 Python 中,多态是语言内置的特性。例如,+ 运算符可以对两个整数进行相加,也可以用于字符串的连接。

在下面的示例中,字符串、元组或列表都可以使用整数索引进行访问。这展示了 Python 如何在内置类型中演示多态:

a = "John"
b = (1,2,3)
c = [3,4,6,8,9]
print(a[1], b[0], c[2])

继承

以下要点有助于我们更好地理解继承过程:

  • 继承表示一个类从父类继承(大部分)功能。

  • 继承被描述为一种重用基类中定义的功能的选项,并允许独立扩展原始软件实现。

  • 继承通过不同类对象之间的关系创建层次结构。Python 与 Java 不同,支持多重继承(从多个基类继承)。

在下面的代码示例中,class A 是基类,class Bclass A 继承其特性。因此,class A 的方法可以通过 class B 的对象访问:

class A:
    def a1(self):
        print("a1")

class B(A):
    def b(self):
        print("b")

b = B()
b.a1()

抽象

抽象的关键特性如下:

  • 它为客户端提供了一个简单的接口,客户端可以通过这个接口与类对象交互并调用接口中定义的方法。

  • 它通过接口抽象了内部类的复杂性,这样客户端就不必了解内部实现。

在下面的示例中,Adder 类的内部细节通过 add() 方法进行了抽象:

class Adder:
    def __init__(self):
        self.sum = 0
    def add(self, value):
        self.sum += value

acc = Adder()
for i in range(99):
    acc.add(i)

print(acc.sum)

组合

组合指的是以下要点:

  • 这是一种将对象或类组合成更复杂的数据结构或软件实现的方法

  • 在组合中,一个对象用于调用其他模块中的成员函数,从而在不使用继承的情况下使基本功能在模块间可用

在以下示例中,class A 的对象被组合到 class B 中:

class A(object):
    def a1(self):
        print("a1")

class B(object):
    def b(self):
        print("b")
        A().a1()

objectB = B()
objectB.b()

面向对象设计原则

现在,让我们谈谈另一组对我们至关重要的概念。这些概念不过是面向对象的设计原则,它们将作为我们在详细学习设计模式时的工具箱。

开放/关闭原则

开放/关闭原则指出,“类或对象和方法应该对扩展开放,但对修改关闭。”

用简单的话来说,这意味着当你开发软件应用程序时,确保你以通用方式编写你的类或模块,这样当你觉得需要扩展类或对象的行为时,你就不需要更改类本身。相反,对类的简单扩展应该帮助你构建新的行为。

例如,开放/关闭原则在用户必须通过扩展抽象基类来创建类实现以实现所需行为而不是更改抽象类的情况下体现出来。

此设计原则的优势如下:

  • 现有的类不会被修改,因此回归的风险较低

  • 它还有助于保持先前代码的向后兼容性

控制反转原则

控制反转原则指出,“高级模块不应该依赖于低级模块;它们都应该依赖于抽象。细节应该依赖于抽象,而不是相反。”

此原则建议,任何两个模块都不应该紧密依赖于对方。事实上,基础模块和依赖模块应该通过中间的抽象层解耦。

此原则还建议,你类的细节应该代表抽象。在某些情况下,哲学被颠倒了,实现细节本身决定了抽象,这应该避免。

控制反转原则的优势如下:

  • 模块的紧密耦合不再普遍,因此系统中没有复杂性/刚性

  • 由于存在清晰的抽象层(由钩子或参数提供),因此更容易以更好的方式处理模块间的依赖关系

接口隔离原则

正如接口隔离原则所指出,“客户端不应该被迫依赖于它们不使用的接口。”

这个原则讨论的是软件开发者如何编写良好的接口。例如,它提醒开发者/架构师开发与功能相关的函数。如果有任何与接口无关的函数,依赖于接口的类就必须不必要地实现它。

例如,一个Pizza接口不应该有一个名为add_chicken()的方法。基于Pizza接口的Veg Pizza类不应该被迫实现这个方法。

这个设计原则的优点如下:

  • 它迫使开发者编写瘦接口,并拥有特定于接口的方法

  • 它帮助您不要通过添加无意的方法来填充接口

单一职责原则

正如单一职责原则所阐述的,一个类应该只有一个改变的理由

这个原则说,当我们开发类时,它应该很好地满足给定的功能。如果一个类负责两个功能,最好是将其拆分。它将功能视为改变的理由。例如,一个类可能因为期望的行为差异而发生变化,但如果一个类因为两个原因(基本上,两个功能的变化)而发生变化,那么这个类肯定应该被拆分。

这个设计原则的优点如下:

  • 每当有一个功能发生变化时,这个特定的类就需要改变,其他什么也不需要

  • 此外,如果一个类有多个功能,依赖于这个类的其他类将不得不因为多个原因进行更改,这可以避免

替换原则

替换原则指出,派生类必须能够完全替换基类

这个原则在意义上非常直接,它说当应用程序开发者编写派生类时,他们应该扩展基类。它还建议派生类应该尽可能接近基类,以至于派生类本身应该能够在不进行任何代码更改的情况下替换基类。

设计模式的概念

最后,现在是我们开始讨论设计模式的时候了!什么是设计模式?

设计模式最初是由GoF四人帮)引入的,他们将它们描述为给定问题的解决方案。如果您想了解更多信息,GoF 指的是书籍《设计模式:可复用面向对象软件元素》的四个作者,Erich GammaRichard HelmRalph Johnson,和John Vlissides,由Grady Booch撰写序言。这本书涵盖了软件工程在软件设计中的常见问题解决方案。最初确定了 23 个设计模式,第一个实现是针对 Java 程序语言的。设计模式是发现,而不是发明本身。

设计模式的关键特性如下:

  • 它们是语言无关的,可以在多种语言中实现

  • 它们是动态的,因为不时会引入新的模式

  • 它们是可定制的,因此对开发者很有用

初始时,当你听到设计模式时,你可能会有以下感觉:

  • 它是解决你迄今为止遇到的所有设计问题的万能药

  • 它是一种解决问题的非常巧妙、特别的方法

  • 软件开发世界的许多专家都同意这些解决方案

  • 设计中存在某种可重复性,因此有“模式”这个词

你也一定尝试过解决设计模式试图解决的问题,但也许你的解决方案是不完整的,而我们寻求的完整性是设计模式中固有的或隐含的。当我们说完整性时,它可以指许多因素,如设计、可扩展性、重用、内存利用等。本质上,设计模式是关于从他人的成功中学习,而不是从自己的失败中学习!

关于设计模式的另一个有趣讨论是——我何时使用它们?是在软件开发生命周期(SDLC)的分析或设计阶段吗?

有趣的是,设计模式是已知问题的解决方案。因此,它们可以在分析或设计阶段非常广泛地使用,并且由于与应用程序代码的直接关系,在开发阶段也可以预期使用。

设计模式的优势

设计模式的优势如下:

  • 它们可以在多个项目中重用

  • 可以解决架构级别的问题

  • 它们是经过时间考验和充分验证的,这是开发者和架构师的经验

  • 它们具有可靠性和依赖性

设计模式的分类

并非每一块代码或设计都可以称为设计模式。例如,解决一个问题的编程结构或数据结构不能称为模式。以下以非常简单的方式理解这些术语:

  • 代码片段:这是为特定目的编写的某些语言的代码,例如,Python 中的数据库连接可以是一个代码片段。

  • 设计:这是解决这个特定问题的更好方案

  • 标准:这是一种解决某些问题的方法,可以非常通用,适用于当前的情况

  • 模式:这是一个经过时间考验、高效且可扩展的解决方案,将解决整个已知问题类别

上下文 – 设计模式的适用性

为了有效地使用设计模式,应用程序开发者必须了解设计模式适用的上下文。我们可以将这些上下文分为以下主要类别:

  • 参与者:它们是在设计模式中使用的类。类在模式中扮演不同的角色,以实现多个目标。

  • 非功能性需求:如内存优化、可用性和性能等需求属于此类。这些因素影响整个软件解决方案,因此至关重要。

  • 权衡:并非所有设计模式都适合直接应用于应用开发,权衡是必要的。这些是在使用设计模式时所做的决策。

  • 结果:如果上下文不适当,设计模式可能会对代码的其他部分产生负面影响。开发者应该了解后果和使用设计模式。

动态语言的模式

Python 是一种像 Lisp 一样的动态语言。Python 的动态特性可以表示如下:

  • 类型或类在运行时是对象。

  • 变量可以作为值具有类型,并且可以在运行时修改。例如,a = 5a = "John",变量a在运行时被分配,类型也发生了变化。

  • 动态语言在类限制方面具有更多灵活性。

  • 例如,在 Python 中,多态是语言内置的,没有像privateprotected这样的关键字,默认情况下一切都是公开的。

  • 代表了设计模式在动态语言中易于实现的情况。

模式分类

GoF 关于设计模式的书籍讨论了 23 种设计模式,并将它们分为三大类:

  • 创建型模式

  • 结构型模式

  • 行为型模式

模式的分类主要基于对象是如何被创建的,如何在软件应用程序中组织类和对象,以及对象之间相互交互的方式。让我们在本节中详细讨论每个类别。

创建型模式:

以下是创建型模式的特性:

  • 它们基于对象如何被创建的原则工作

  • 它们隔离了对象创建的细节

  • 代码独立于要创建的对象类型

创建型模式的一个例子是单例模式。

结构型模式

以下是结构型模式的特性:

  • 它们设计对象和类的结构,以便它们可以组合以实现更大的结果

  • 重点在于简化结构和识别类与对象之间的关系

  • 它们关注类继承和组合

行为型模式的一个例子是适配器模式。

行为型模式

以下是行为型模式的特性:

  • 它们关注对象之间的交互和对象的责任

  • 对象应该能够交互,同时仍然保持松散耦合

行为型模式的一个例子是观察者模式。

概述

在本章中,你学习了面向对象编程的基本概念,例如对象、类、变量,以及代码示例中的多态、继承和抽象等特性。

我们现在也意识到,作为开发者/架构师,在设计应用程序时,我们应该考虑面向对象设计原则。

最后,我们继续探索设计模式及其应用和它们可以应用的上下文,并讨论了它们的分类。

在本章结束时,我们现在已经准备好迈出下一步,详细研究设计模式。

第二章:单例设计模式

在上一章中,我们探讨了设计模式和它们的分类。正如我们所知,设计模式可以分为三大类:结构型、行为型和创建型模式。

在本章中,我们将探讨单例设计模式——这是在应用程序开发中使用的最简单和最知名创建型设计模式之一。本章将简要介绍单例模式,带您通过一个可以使用此模式的真实世界示例,并使用 Python 实现来详细解释它。您将了解单态(或 Borg)模式,这是单例模式的变体。

在本章中,我们将简要介绍以下内容:

  • 理解单例设计模式

  • 单例模式的真实世界示例

  • Python 中的单例模式实现

  • 单态(Borg)模式

在本章末尾,我们将对单例进行简要总结。这将帮助您独立思考单例设计模式的一些方面。

理解单例设计模式

单例提供了一种机制,确保只有一个给定类型的对象被创建,并提供了一个全局访问点。因此,单例通常用于需要跨应用程序只有一个实例以避免对同一资源冲突请求的场景,例如日志记录或数据库操作、打印机打印队列等。例如,我们可能希望使用一个数据库对象来对数据库进行操作以维护数据一致性,或者在一个多服务中跨多个服务使用一个日志类对象来顺序地写入特定的日志文件。

简而言之,单例设计模式的意图如下:

  • 确保只有一个类的对象被创建

  • 为全局程序提供一个对象的访问点

  • 控制对共享资源的并发访问

下面的 UML 图是单例的:

理解单例设计模式

实现单例的一种简单方法是通过使构造函数私有,并创建一个静态方法来完成对象初始化。这样,在第一次调用时创建一个对象,之后类返回相同的对象。

在 Python 中,我们将以不同的方式实现它,因为没有创建私有构造函数的选项。让我们看看 Python 语言中单例是如何实现的。

在 Python 中实现经典的单例

这里是 Python 3.5 版本中单例模式的一个示例代码。在这个例子中,我们将做两件主要的事情:

  1. 我们将允许创建Singleton类的一个实例。

  2. 如果存在实例,我们将再次提供相同的对象。

以下代码展示了这一点:

class Singleton(object):
     def __new__(cls):
       if not hasattr(cls, 'instance'):
         cls.instance = super(Singleton, cls).__new__(cls)
       return cls.instance

s = Singleton()
print("Object created", s)

s1 = Singleton()
print("Object created", s1)

通过覆盖__new__方法(Python 中用于实例化对象的特殊方法)来控制对象创建。s对象通过__new__方法创建,但在创建之前,它会检查对象是否已经存在。使用hasattr方法(Python 中用于判断对象是否具有特定属性的特殊方法)来查看cls对象是否具有实例属性,这会检查类是否已经有一个对象。直到请求s1对象时,hasattr()会检测到已经存在一个对象,因此s1分配了现有的对象实例(位于0x102078ba8)。

单例模式中的延迟实例化

单例模式的一个用例是延迟实例化。例如,在模块导入的情况下,我们可能会在不必要的时候意外创建一个对象。延迟实例化确保对象仅在真正需要时才被创建。将延迟实例化视为一种以减少资源使用并仅在需要时创建资源的方式。

在下面的代码示例中,当我们说s=Singleton()时,它调用__init__方法,但没有创建新的对象。然而,实际的对象创建发生在我们调用Singleton.getInstance()时。这就是如何实现延迟实例化的。

class Singleton:
    __instance = None
    def __init__(self):
        if not Singleton.__instance:
            print(" __init__ method called..")
        else:
            print("Instance already created:", self.getInstance())
    @classmethod
    def getInstance(cls):
        if not cls.__instance:
            cls.__instance = Singleton()
        return cls.__instance

s = Singleton() ## class initialized, but object not created
print("Object created", Singleton.getInstance()) # Object gets created here
s1 = Singleton() ## instance already created

模块级单例

由于 Python 的导入行为,所有模块默认都是单例。Python 以以下方式工作:

  1. 检查 Python 模块是否已被导入。

  2. 如果已导入,则返回模块的对象。如果尚未导入,则导入并实例化它。

  3. 因此,当一个模块被导入时,它会被初始化。然而,当相同的模块再次被导入时,它不会被再次初始化,这与单例行为有关,即只有一个对象并返回相同的对象。

单态单例模式

我们在第一章中讨论了四人帮及其书籍,《设计模式简介》。四人帮的单例设计模式指出,一个类应该只有一个对象。然而,根据亚历克斯·马尔蒂尼的观点,程序员通常需要的是具有相同状态的实例。他建议开发者应该关注状态和行为,而不是身份。由于这个概念基于所有对象共享相同的状态,它也被称为单态模式。

在 Python 中,单态模式可以通过非常简单的方式实现。在下面的代码中,我们将 Python 的特殊变量__dict__(一个特殊的 Python 变量)与__shared_state类变量关联。Python 使用__dict__来存储类中每个对象的状态。在下面的代码中,我们故意将__shared_state分配给所有创建的实例。因此,当我们创建两个实例'b''b1'时,我们得到两个不同的对象,这与单例不同,我们只有一个对象。然而,对象状态b.__dict__b1.__dict__是相同的。现在,即使对象变量x对对象b发生变化,这个变化也会复制到所有对象共享的__dict__变量中,甚至b1也会从x设置的一个变为四:

class Borg:
    __shared_state = {"1":"2"}
    def __init__(self):
        self.x = 1
        self.__dict__ = self.__shared_state
        pass

b = Borg()
b1 = Borg()
b.x = 4

print("Borg Object 'b': ", b) ## b and b1 are distinct objects
print("Borg Object 'b1': ", b1)
print("Object State 'b':", b.__dict__)## b and b1 share same state
print("Object State 'b1':", b1.__dict__)

实现 Borg 模式的另一种方法是调整__new__方法本身。正如我们所知,__new__方法负责对象的创建:

class Borg(object):
     _shared_state = {}
     def __new__(cls, *args, **kwargs):
       obj = super(Borg, cls).__new__(cls, *args, **kwargs)
       obj.__dict__ = cls._shared_state
       return obj

单例和元类

让我们从对元类的简要介绍开始。元类是类的类,这意味着类是其元类的实例。通过元类,程序员有机会从预定义的 Python 类中创建他们自己的类型的类。例如,如果你有一个对象MyClass,你可以创建一个元类MyKls,它重新定义了MyClass的行为,使其符合你的需求。让我们详细了解一下。

在 Python 中,一切都是对象。如果我们说a=5,那么type(a)返回<type 'int'>,这意味着a是 int 类型。然而,type(int)返回<type 'type'>,这表明存在一个元类,因为 int 是type类型的类。

类的定义由其元类决定,因此当我们使用class A创建一个类时,Python 通过A = type(name, bases, dict)来创建它:

  • name: 这是类的名称

  • base: 这是基类

  • dict: 这是属性变量

现在,如果一个类有一个预定义的元类(名为MetaKls),Python 将通过A = MetaKls(name, bases, dict)来创建这个类。

让我们看看 Python 3.5 中的一个示例元类实现:

class MyInt(type):
    def __call__(cls, *args, **kwds):
        print("***** Here's My int *****", args)
        print("Now do whatever you want with these objects...")
        return type.__call__(cls, *args, **kwds)

class int(metaclass=MyInt):
    def __init__(self, x, y):
        self.x = x
        self.y = y

i = int(4,5)

以下为上述代码的输出:

单例和元类

当需要为已存在的类创建对象时,Python 的特殊__call__方法会被调用。在这段代码中,当我们使用int(4,5)实例化int类时,MyInt元类的__call__方法会被调用,这意味着元类现在控制了对象的实例化。哇,这不是很棒吗?!

上述哲学也用于单例设计模式。由于元类对类创建和对象实例化有更多的控制,它可以用来创建单例。(注意:为了控制类的创建和初始化,元类会覆盖__new____init__方法。)

使用元类实现的单例可以通过以下示例代码更好地解释:

class MetaSingleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(MetaSingleton, \
                cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class Logger(metaclass=MetaSingleton):
    pass

logger1 = Logger()
logger2 = Logger()
print(logger1, logger2)

真实场景 - Singleton 模式,第一部分

作为实际用例,我们将通过一个数据库应用程序来展示 Singleton 的使用。考虑一个涉及数据库多次读写操作的云服务示例。完整的云服务被分割成多个执行数据库操作的服务。UI(Web 应用程序)上的操作在内部会调用 API,最终导致数据库操作。

很明显,跨不同服务的共享资源是数据库本身。因此,如果我们需要更好地设计云服务,以下这些点必须注意:

  • 数据库操作的一致性——一个操作不应该与其他操作发生冲突

  • 内存和 CPU 利用率应该是最优的,以便处理数据库上的多个操作

这里提供了一个 Python 实现的示例:

import sqlite3
class MetaSingleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(MetaSingleton, \
                cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class Database(metaclass=MetaSingleton):
  connection = None
  def connect(self):
    if self.connection is None:
        self.connection = sqlite3.connect("db.sqlite3")
        self.cursorobj = self.connection.cursor()
    return self.cursorobj

db1 = Database().connect()
db2 = Database().connect()

print ("Database Objects DB1", db1)
print ("Database Objects DB2", db2)

上述代码的输出如下:

真实场景 - Singleton 模式,第一部分

在前面的代码中,我们可以看到以下要点:

  1. 我们创建了一个名为MetaSingleton的元类。正如我们在上一节中解释的,Python 的特殊__call__方法在元类中被用来创建 Singleton。

  2. database类被MetaSingleton类装饰,开始像 Singleton 一样工作。因此,当实例化database类时,它只会创建一个对象。

  3. 当 Web 应用程序想要在数据库上执行某些操作时,它会多次实例化数据库类,但实际上只创建了一个对象。由于只有一个对象,对数据库的调用是同步的。此外,这不会消耗太多系统资源,我们可以避免内存或 CPU 资源不足的情况。

假设我们不是只有一个 webapp,而是一个由多个 webapp 组成的集群设置,但只有一个数据库。现在,这并不是 Singleton 的好情况,因为随着每个 webapp 的增加,一个新的 Singleton 会被创建,并且会添加一个新的查询数据库的对象。这会导致数据库操作不同步,并且资源消耗很大。在这种情况下,数据库连接池比实现 Singleton 更好。

真实场景 - Singleton 模式,第二部分

让我们考虑另一个场景,其中我们为我们的基础设施实现健康检查服务(如 Nagios)。我们创建了一个HealthCheck类,它被实现为 Singleton。我们还维护了一个需要运行健康检查的服务器列表。如果从该列表中删除服务器,健康检查软件应该检测到并将其从配置为检查的服务器中删除。

在下面的代码中,hc1hc2对象与 Singleton 中的类相同。

使用addServer()方法将服务器添加到基础设施中,以进行健康检查。首先,健康检查的迭代运行在这些服务器上。changeServer()方法移除最后一个服务器,并将一个新的服务器添加到为健康检查而计划的基础设施中。因此,当健康检查在第二次迭代中运行时,它会选择更改后的服务器列表。

所有这些都可以通过 Singleton 实现。当服务器被添加或移除时,健康检查必须是知道对基础设施所做的更改的对象:

class HealthCheck:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not HealthCheck._instance:
            HealthCheck._instance = super(HealthCheck, \
                cls).__new__(cls, *args, **kwargs)
        return HealthCheck._instance
    def __init__(self):
        self._servers = []
    def addServer(self):
        self._servers.append("Server 1")
        self._servers.append("Server 2")
        self._servers.append("Server 3")
        self._servers.append("Server 4")
    def changeServer(self):
        self._servers.pop()
        self._servers.append("Server 5")

hc1 = HealthCheck()
hc2 = HealthCheck()

hc1.addServer()
print("Schedule health check for servers (1)..")
for i in range(4):
    print("Checking ", hc1._servers[i])

hc2.changeServer()
print("Schedule health check for servers (2)..")
for i in range(4):
    print("Checking ", hc2._servers[i])

代码的输出如下:

现实世界场景 - Singleton 模式,第二部分

Singleton 模式的缺点

虽然 Singleton 在多个地方被有效使用,但这个模式可能存在一些陷阱。由于 Singleton 具有全局访问点,以下问题可能会发生:

  • 全局变量可能会在一个地方被错误地更改,由于开发者可能认为它们保持不变,变量在应用程序的其他地方被使用。

  • 可能会创建对同一对象的多个引用。由于 Singleton 只创建一个对象,因此此时可以创建对同一对象的多个引用。

  • 所有依赖于全局变量的类都会紧密耦合,因为一个类对全局数据的更改可能会无意中影响其他类。

注意

作为本章的一部分,你学习了关于 Singleton 的很多内容。以下是我们应该记住的关于 Singleton 的几个要点:

  • 在许多现实世界的应用中,我们只需要创建一个对象,例如线程池、缓存、对话框、注册设置等。如果我们为每个这些应用创建多个实例,将导致资源的过度使用。Singleton 在这种情况下工作得非常好。

  • Singleton;这是一个经过时间考验且被证明的,在无需许多缺点的情况下提供全局访问点的方法。

  • 当然,也有一些缺点;Singleton 在与全局变量一起工作或实例化资源密集型类时可能会产生意外影响,而这些类最终没有使用这些资源。

摘要

在本章中,你学习了 Singleton 设计模式及其使用上下文。我们了解到,当需要为类只有一个对象时,会使用 Singleton。

我们还探讨了在 Python 中实现 Singleton 的各种方法。经典实现允许多次实例化尝试,但返回相同的对象。

我们还讨论了 Borg 或 Monostate 模式,这是 Singleton 模式的一种变体。Borg 允许创建多个对象,这些对象共享相同的状态,与 GoF 描述的单例模式不同。

我们继续探索了在多个服务中应用 Singleton 以实现一致数据库操作的 webapp 应用。

最后,我们还探讨了 Singleton 可能出错的情况以及开发者需要避免的情况。

到本章结束时,我们已经足够熟悉,可以继续下一步,研究其他创建型模式并从中受益。

在下一章中,我们将探讨另一种创建型设计模式,即工厂设计模式。我们将介绍Factory方法和抽象工厂模式,并在 Python 实现中理解它们。

第三章:工厂模式——构建工厂以创建对象

在上一章中,您学习了单例设计模式——它们是什么以及如何在现实世界中使用,包括 Python 实现。单例设计模式是创建型设计模式之一。在本章中,我们将继续学习另一个创建型模式,即工厂模式。

工厂模式可以说是最常用的设计模式。在本章中,我们将了解工厂的概念,并学习简单工厂模式。然后,您将学习工厂方法模式和抽象工厂模式,包括 UML 图、现实世界场景和 Python 3.5 实现。我们还将比较工厂方法和抽象工厂方法。

在本章中,我们将简要介绍以下内容:

  • 理解简单工厂设计模式

  • 讨论工厂方法和抽象工厂方法及其区别

  • 使用 Python 代码实现来模拟现实世界场景

  • 讨论模式的优缺点及其比较

理解工厂模式

在面向对象编程中,工厂这个术语指的是一个负责创建其他类型对象的类。通常,充当工厂的类有一个与之关联的对象和方法。客户端使用某些参数调用此方法;然后依次创建所需类型的对象,并由工厂返回给客户端。

所以这里真正的问题是,当客户端可以直接创建对象时,我们为什么还需要工厂?答案是,工厂提供了一些优势,如下列所示:

  • 第一个优势是松散耦合,其中对象的创建可以独立于类实现。

  • 客户端不需要知道创建对象的类,而该类反过来被客户端使用。只需知道接口、方法和需要传递的参数,以便创建所需类型的对象。这简化了客户端的实现。

  • 向工厂添加另一个类以创建另一种类型的对象可以很容易地完成,而无需客户端更改代码。至少,客户端需要传递另一个参数。

  • 工厂还可以重用现有对象。然而,当客户端直接创建对象时,这总是创建一个新的对象。

让我们考虑一个制造玩具的公司的情况,该公司制造玩具车或娃娃。假设公司中的一台机器目前正在制造玩具车。然后,公司的 CEO 觉得根据市场需求,迫切需要制造娃娃。这种情况需要工厂模式。在这种情况下,机器成为接口,CEO 是客户端。CEO 只关心要制造的物体(或玩具),并且知道可以创建该对象的接口——机器。

工厂模式有三个变体:

  • 简单工厂模式:这允许接口创建对象而不暴露对象创建逻辑。

  • 工厂方法模式:这允许接口创建对象,但将决定创建哪个类的决策推迟到子类。

  • 抽象工厂模式:抽象工厂是一个创建相关对象的接口,而不指定/暴露它们的类。该模式提供另一个工厂的对象,该工厂内部创建其他对象。

简单工厂模式

Animal. Animal is an abstract base class (ABCMeta is Python's special metaclass to make a class Abstract) and has the do_say() method. We create two products (Cat and Dog) from the Animal interface and implement do_say() with appropriate sounds that these animals make. ForestFactory is a factory that has the make_sound() method. Based on the type of argument passed by the client, an appropriate Animal instance is created at runtime and the right sound is printed out:
from abc import ABCMeta, abstractmethod

class Animal(metaclass = ABCMeta):
    @abstractmethod
    def do_say(self):
        pass

class Dog(Animal):
    def do_say(self):
        print("Bhow Bhow!!")

class Cat(Animal):
    def do_say(self):
        print("Meow Meow!!")

## forest factory defined
class ForestFactory(object):
    def make_sound(self, object_type):
        return eval(object_type)().do_say()

## client code
if __name__ == '__main__':
    ff = ForestFactory()
    animal = input("Which animal should make_sound Dog or Cat?")
    ff.make_sound(animal)

工厂方法模式

以下要点有助于我们理解工厂方法模式:

  • 我们定义了一个创建对象的接口,但工厂不负责对象创建,而是将责任推迟到决定要实例化哪个类的子类。

  • 工厂方法的创建是通过继承而不是通过实例化。

  • 工厂方法使设计更具可定制性。它可以返回相同的实例或子类,而不是特定类型的对象(如简单工厂方法)。工厂方法模式

    工厂方法的 UML 图

在前面的 UML 图中,我们有一个包含 factoryMethod() 的抽象类 CreatorfactoryMethod() 方法负责创建特定类型的对象。ConcreteCreator 类实现了 Creator 抽象类的 factoryMethod(),并且这个方法可以在运行时改变创建的对象。ConcreteCreator 创建 ConcreteProduct 并确保它创建的对象实现了 Product 类,并为 Product 接口中的所有方法提供了实现。

简而言之,Creator 接口和 ConcreteCreator 类的 factoryMethod() 决定了要创建哪个 Product 子类。因此,工厂方法模式定义了一个创建对象的接口,但将决定实例化哪个类的决策推迟到其子类。

实现工厂方法

让我们通过一个现实世界的场景来理解工厂方法实现。假设我们想要为个人或公司创建不同类型的社交网络配置文件,如 LinkedIn 和 Facebook。现在,每个配置文件都会有某些部分。在 LinkedIn 上,你会看到一个关于个人已申请的专利或已发表的出版物部分。在 Facebook 上,你会在相册中看到你最近访问度假地的照片部分。此外,在这两个配置文件中,都会有关于个人信息的公共部分。简而言之,我们想要创建具有正确部分添加的不同类型的配置文件。

现在我们来看一下实现方式。在下面的代码示例中,我们首先定义 Product 接口。我们将创建一个 Section 抽象类,它定义了如何定义一个部分。我们将保持它非常简单,并提供一个抽象方法,describe()

现在我们创建了多个 ConcreteProduct 类,PersonalSectionAlbumSectionPatentSectionPublicationSection。这些类实现了 describe() 抽象方法,并打印它们各自的分区名称:

from abc import ABCMeta, abstractmethod

class Section(metaclass=ABCMeta):
    @abstractmethod
    def describe(self):
        pass

class PersonalSection(Section):
    def describe(self):
        print("Personal Section")

class AlbumSection(Section):
    def describe(self):
        print("Album Section")

class PatentSection(Section):
    def describe(self):
        print("Patent Section")

class PublicationSection(Section):
    def describe(self):
        print("Publication Section")

我们创建一个名为 ProfileCreator 抽象类。Profile [Creator] 抽象类提供了一个工厂方法,createProfile()createProfile() 方法应由 ConcreteClass 实现,以实际创建具有适当部分的配置文件。Profile 抽象类不知道每个配置文件应该有哪些部分。例如,Facebook 配置文件应该有个人信息和相册部分。因此,我们将让子类来决定这一点。

我们创建了两个 ConcreteCreator 类,linkedinfacebook。这些类中的每一个都实现了 createProfile() 抽象方法,该方法在运行时实际创建(实例化)多个部分(ConcreteProducts):

class Profile(metaclass=ABCMeta):
    def __init__(self):
        self.sections = []
        self.createProfile()
    @abstractmethod
    def createProfile(self):
        pass
    def getSections(self):
        return self.sections
    def addSections(self, section):
        self.sections.append(section)

class linkedin(Profile):
    def createProfile(self):
        self.addSections(PersonalSection())
        self.addSections(PatentSection())
        self.addSections(PublicationSection())

class facebook(Profile):
    def createProfile(self):
        self.addSections(PersonalSection())
        self.addSections(AlbumSection())

最后,我们编写客户端代码,以确定要实例化哪个 Creator 类来创建所需选择的配置文件:

if __name__ == '__main__':
    profile_type = input("Which Profile you'd like to create? [LinkedIn or FaceBook]")
    profile = eval(profile_type.lower())()
    print("Creating Profile..", type(profile).__name__)
    print("Profile has sections --", profile.getSections())

工厂方法模式的优点

既然你已经学习了工厂方法模式及其实现方法,让我们来看看工厂方法模式的优点:

  • 它带来了很多灵活性,并使代码通用,不受特定类实例化的限制。这样,我们依赖于接口(Product),而不是 ConcreteProduct 类。

  • 由于创建对象的代码与使用它的代码是分离的,因此存在松散耦合。客户端无需担心传递什么参数以及实例化哪个类。添加新类很容易,并且维护成本低。

抽象工厂模式

抽象工厂模式的主要目标是提供一个接口来创建一系列相关的对象,而不指定具体的类。虽然工厂方法将实例的创建推迟到子类中,但抽象工厂方法的目标是创建一系列相关的对象:

抽象工厂模式

抽象工厂模式的 UML 图

如图中所示,ConcreteFactory1ConcreteFactory2 是从 AbstractFactory 接口创建的。该接口有创建多个产品的方法。

ConcreteFactory1ConcreteFactory2 实现了抽象工厂,并创建了 ConcreteProduct1ConcreteProduct2AnotherConcreteProduct1AnotherConcreteProduct2 的实例。

ConcreteProduct1ConcreteProduct2是从AbstractProduct接口创建的,而AnotherConcreteProduct1AnotherConcreteProduct2是从AnotherAbstractProduct接口创建的。

实际上,抽象工厂模式确保客户端与对象的创建相隔离,但允许使用创建的对象。客户端只能通过接口访问对象。如果需要使用同一家族的产品,抽象工厂模式帮助客户端一次使用一个家族的对象。例如,如果一个正在开发的应用程序应该是平台无关的,那么它需要抽象出依赖项,如操作系统、文件系统调用等。抽象工厂模式负责创建整个平台所需的服务,以便客户端不必直接创建平台对象。

实现抽象工厂模式

考虑你最喜欢的披萨店的例子。它提供多种类型的披萨,对吧?等等,我知道你马上就想点一个,但现在让我们先回到例子上!

现在,想象一下我们创建了一个披萨店,你可以品尝到美味的印度和美国披萨。为此,我们首先创建了一个抽象基类,PizzaFactory(在先前的 UML 图中为AbstractFactory)。PizzaFactory类有两个抽象方法,createVegPizza()createNonVegPizza(),需要由ConcreteFactory实现。在这个例子中,我们创建了两个具体工厂,即IndianPizzaFactoryUSPizzaFactory。请看以下具体工厂的代码实现:

from abc import ABCMeta, abstractmethod

class PizzaFactory(metaclass=ABCMeta):

    @abstractmethod
    def createVegPizza(self):
        pass

    @abstractmethod
    def createNonVegPizza(self):
        pass

class IndianPizzaFactory(PizzaFactory):

    def createVegPizza(self):
        return DeluxVeggiePizza()

    def createNonVegPizza(self):
        return ChickenPizza()

class USPizzaFactory(PizzaFactory):

    def createVegPizza(self):
        return MexicanVegPizza()

    def createNonVegPizza(self):
        return HamPizza()

现在,让我们继续前进并定义AbstractProducts。在下面的代码中,我们创建了两个抽象类,VegPizzaNonVegPizza(在先前的 UML 图中为AbstractProductAnotherAbstractProduct)。它们各自定义了一个方法,prepare()serve()

这里的思路是,素食披萨使用适当的饼底、蔬菜和调味料制作,而非素食披萨则在素食披萨上加上非素食配料。

然后,我们为每个AbstractProducts定义ConcreteProducts。现在,在这种情况下,我们创建了DeluxVeggiePizzaMexicanVegPizza并实现了prepare()方法。ConcreteProducts1ConcreteProducts2将代表 UML 图中的这些类。

之后,我们定义了ChickenPizzaHamPizza并实现了serve()方法——这些代表AnotherConcreteProducts1AnotherConcreteProducts2

class VegPizza(metaclass=ABCMeta):
    @abstractmethod
    def prepare(self, VegPizza):
        pass

class NonVegPizza(metaclass=ABCMeta):
    @abstractmethod
    def serve(self, VegPizza):
        pass

class DeluxVeggiePizza(VegPizza):
    def prepare(self):
        print("Prepare ", type(self).__name__)

class ChickenPizza(NonVegPizza):
    def serve(self, VegPizza):
        print(type(self).__name__, " is served with Chicken on ", type(VegPizza).__name__)

class MexicanVegPizza(VegPizza):
    def prepare(self):
        print("Prepare ", type(self).__name__)

class HamPizza(NonVegPizza):
    def serve(self, VegPizza):
        print(type(self).__name__, " is served with Ham on ", type(VegPizza).__name__)

当最终用户接近PizzaStore并要求一份美国非素食披萨时,USPizzaFactory负责准备素食披萨作为底料,并在上面加上火腿来提供非素食披萨!

class PizzaStore:
    def __init__(self):
        pass
    def makePizzas(self):
        for factory in [IndianPizzaFactory(), USPizzaFactory()]:
            self.factory = factory
            self.NonVegPizza = self.factory.createNonVegPizza()
            self.VegPizza = self.factory.createVegPizza()
            self.VegPizza.prepare()
            self.NonVegPizza.serve(self.VegPizza)

pizza = PizzaStore()
pizza.makePizzas()

以下是在先前的代码示例中的输出:

实现抽象工厂模式

工厂方法与抽象工厂方法

现在你已经学习了工厂方法和抽象工厂方法,让我们看看这两种方法的比较:

工厂方法 抽象工厂方法
这向客户端公开了一个创建对象的方法 抽象工厂方法包含一个或多个工厂方法来创建一个相关对象系列
这使用继承和子类来决定创建哪个对象 这使用组合来委托创建另一个类的对象的责任
工厂方法用于创建一个产品 抽象工厂方法关于创建相关产品系列

摘要

在本章中,你了解了工厂设计模式及其使用场景。我们了解了工厂的基本原理,以及它在软件架构中的有效应用。

我们探讨了简单工厂模式,其中根据客户端传递的参数类型在运行时创建一个适当的实例。

我们还讨论了工厂方法模式,这是简单工厂模式的一种变体。在这个模式中,我们定义了一个接口来创建对象,但对象的创建被延迟到子类中。

我们继续探索抽象工厂方法,它提供了一个创建相关对象系列的接口,而不指定具体的类。

我们还为所有三种模式实现了实际的 Python 实现,并比较了工厂方法和抽象工厂方法。

在本章结束时,我们现在准备迈出下一步,研究其他类型的模式,所以请保持关注。

第四章. 外观模式 – 使用外观实现适应性

在上一章中,你学习了工厂设计模式。我们讨论了三种变体——简单工厂、工厂方法和抽象工厂模式。你还学习了它们在现实世界中的应用,并查看了解决方案。我们还比较了工厂方法和抽象工厂模式,并列出了它们的优缺点。正如我们所知,工厂设计模式和单例设计模式(第二章,单例设计模式)都被归类为创建型设计模式。

在本章中,我们将继续学习另一种类型的设计模式,即结构设计模式。我们将介绍外观设计模式及其在软件开发中的应用。我们将通过一个示例用例并使用 Python v3.5 来实现它。

简要来说,在本章中我们将涵盖以下主题:

  • 结构设计模式的简介

  • 使用 UML 图理解外观设计模式

  • 一个带有 Python v3.5 代码实现的现实世界用例

  • 外观模式与最少知识原则

理解结构设计模式

以下要点将帮助我们更好地理解结构模式:

  • 结构模式描述了对象和类如何组合成更大的结构。

  • 结构模式可以被视为通过识别实现或展示实体之间关系更简单的方法来简化设计的设计模式。实体在面向对象世界中意味着对象或类。

  • 当类模式通过继承帮助描述抽象并提供更有用的程序接口时,对象模式描述了对象如何关联和组合以形成更大的对象。结构模式是类模式和对象模式的结合。

以下是一些不同结构模式的示例。你会注意到,这些模式中的每一个都涉及到对象或类之间的交互,以实现高级设计或架构目标。

结构设计模式的以下是一些示例:

  • 适配器模式:将一个接口适配到另一个接口,以满足客户端的期望。它试图根据客户端的需求匹配不同类的接口。

  • 桥接模式:将对象的接口与其实现解耦,以便它们可以独立工作。

  • 装饰器模式:在运行时或动态地为对象定义额外的职责。我们通过接口为对象添加某些属性。

在这本书中,你还将学习到一些更多的结构模式。因此,让我们首先从外观设计模式开始。

理解外观设计模式

门面通常被称为建筑的立面,尤其是吸引人的立面。它也可以指一种行为或外观,给人一种关于某人真实感受或情况的错误印象。当人们走过门面时,他们可以欣赏到外观,但并不了解内部结构的复杂性。这就是门面模式的使用方式。门面隐藏了内部系统的复杂性,并为客户端提供了一个可以非常简化的方式访问系统的接口。

考虑一个店主的例子。现在,当你作为顾客去商店购买某些商品时,你并不了解商店的布局。你通常会去找店主,店主对商店系统非常了解。根据你的需求,店主挑选商品并将其交给你。这不是很容易吗?顾客不需要知道商店的外观,他们可以通过一个简单的接口,即店主,来完成所需的事情。

门面设计模式本质上执行以下操作:

  • 它提供了一个统一的接口,用于访问子系统中的多个接口,并定义了一个高级接口,帮助客户端以简单的方式使用子系统。

  • 门面讨论了用一个单一的接口对象来表示复杂的子系统。它并不封装子系统,而是实际上组合了底层子系统。

  • 它促进了实现与多个客户端的解耦。

UML 类图

我们现在将借助以下 UML 图来讨论门面模式:

UML 类图

当我们观察 UML 图时,你会意识到在这个模式中有三个主要参与者:

  • 门面:门面的主要责任是将一组复杂的子系统封装起来,以便它可以为外界提供一个令人愉悦的外观。

  • 系统:这代表了一组不同的子系统,使整个系统变得复杂,难以观察或操作。

  • 客户端:客户端与门面交互,以便它可以轻松地与子系统通信并完成工作。它不必担心系统的复杂性质。

你现在将从数据结构的角度了解三个主要参与者的一些更多信息。

门面

以下要点将使我们更好地了解门面:

  • 它是一个接口,知道哪些子系统负责处理请求

  • 它通过组合使用将客户端的请求委派给适当的子系统对象。

例如,如果客户端需要完成某些工作,它不需要去访问各个子系统,而可以直接联系完成工作的接口(门面)。

系统

在门面世界中,系统是一个执行以下功能的实体:

  • 它实现了子系统功能,并由一个类表示。理想情况下,系统由一组负责不同操作的类组成。

  • 它处理外观对象分配的工作,但没有关于外观的知识,也不保留对其的引用。

例如,当客户端请求外观提供某种服务时,外观会根据服务类型选择提供该服务的正确子系统。

客户端

下面我们来描述一下客户端:

  • 客户端是一个实例化外观的类

  • 客户端会向外观发送请求,以从子系统完成工作

在现实世界中实现外观模式

为了展示外观模式的用途,让我们举一个我们在生活中可能遇到过的例子。

假设你家里有一场婚礼,你负责所有的安排。哇!这是一项艰巨的任务。你必须预订婚礼的酒店或场所,与餐饮服务商沟通食物安排,组织花卉装饰,最后处理活动所需的音乐安排。

在过去,你可能需要自己完成所有这些工作,例如通过与相关人员交谈、与他们协调、协商价格,但现在生活更简单了。你去找一个活动经理,他会为你处理这些事情。他/她会确保与个别服务提供商交谈,为你争取到最好的交易。

从外观模式的角度来看:

  • 客户端:就是你,需要在婚礼前及时完成所有的婚礼准备工作。这些准备工作应该是顶级的,客人应该喜欢庆祝活动。

  • 外观:负责与所有需要处理特定安排的人(如食物、花卉装饰等)进行沟通的事件经理

  • 子系统:它们代表提供餐饮、酒店管理和花卉装饰等服务系统

让我们用 Python 3.5 开发一个应用程序并实现这个用例。我们首先从客户端开始。记住,你就是那个被赋予确保婚礼准备工作完成并确保活动顺利进行的人!

现在我们继续前进,谈谈外观类。如前所述,外观类简化了客户端的接口。在这种情况下,EventManager充当外观,简化了你的工作。外观与子系统交谈,代表你完成婚礼的所有预订和准备工作。以下是EventManager类的 Python 代码:

class EventManager(object):

    def __init__(self):
        print("Event Manager:: Let me talk to the folks\n")

    def arrange(self):
        self.hotelier = Hotelier()
        self.hotelier.bookHotel()

        self.florist = Florist()
        self.florist.setFlowerRequirements()

        self.caterer = Caterer()
        self.caterer.setCuisine()

        self.musician = Musician()
        self.musician.setMusicType()

现在我们已经完成了外观和客户端的开发,让我们深入到子系统。为此场景,我们已经开发了以下类:

  • Hotelier用于酒店预订。它有一个方法来检查那天酒店是否空闲(__isAvailable)。

  • Florist类负责花卉装饰。Florist 类有setFlowerRequirements()方法,用于设置对婚礼装饰所需花卉的期望。

  • Caterer 类用于处理餐饮服务,并负责食物安排。Caterer 提供了 setCuisine() 方法来接受婚礼上要提供的菜系。

  • Musician 类是为婚礼音乐编排设计的。它使用 setMusicType() 方法来理解活动的音乐需求。

让我们现在看看 Hotelier 对象,然后是 Florist 对象及其方法:

class Hotelier(object):
    def __init__(self):
        print("Arranging the Hotel for Marriage? --")

    def __isAvailable(self):
        print("Is the Hotel free for the event on given day?")
        return True

    def bookHotel(self):
        if self.__isAvailable():
            print("Registered the Booking\n\n")

class Florist(object):
    def __init__(self):
        print("Flower Decorations for the Event? --")

    def setFlowerRequirements(self):
        print("Carnations, Roses and Lilies would be used for Decorations\n\n")

class Caterer(object):
    def __init__(self):
        print("Food Arrangements for the Event --")

    def setCuisine(self):
        print("Chinese & Continental Cuisine to be served\n\n")

class Musician(object):
    def __init__(self):
        print("Musical Arrangements for the Marriage --")

    def setMusicType(self):
        print("Jazz and Classical will be played\n\n")

然而,你在这里很聪明,把责任转给了事件经理,不是吗?现在让我们看看 You 类。在这个例子中,你创建了一个 EventManager 类的实例,这样经理就可以在婚礼准备期间与相关人员合作,而你则可以放松。

class You(object):
    def __init__(self):
        print("You:: Whoa! Marriage Arrangements??!!!")
    def askEventManager(self):
        print("You:: Let's Contact the Event Manager\n\n")
        em = EventManager()
        em.arrange()
    def __del__(self):
        print("You:: Thanks to Event Manager, all preparations done! Phew!")

you = You()
you.askEventManager()

上述代码的输出如下所示:

在现实世界中实现外观模式

我们可以通过以下方式将外观模式与现实世界场景联系起来:

  • EventManager 类是简化 You 接口的外观

  • EventManager 使用组合来创建子系统如 HotelierCaterer 等的对象

最少知识原则

正如你在本章的初始部分所学的,外观提供了一个统一的系统,使得子系统易于使用。它还使客户端与组件子系统解耦。外观模式背后的设计原则是最少知识原则

最少知识原则引导我们减少对象之间的交互,仅限于几个足够接近你的“朋友”。在实际情况中,这意味着以下内容:

  • 在设计系统时,对于每个创建的对象,都应该考虑它与多少个类交互以及交互的方式。

  • 遵循此原则,确保我们避免出现创建许多紧密耦合的类的情况。

  • 如果类之间存在很多依赖关系,系统就难以维护。系统任何一部分的变化都可能导致其他部分出现意外的变化,这意味着系统容易受到回归的影响,这应该避免。

常见问题

Q1. 迪米特法则是什么,它与工厂模式有何关系?

A:迪米特法则是一种设计指南,它讨论以下内容:

  1. 每个单元应该只对系统中的其他单元有有限的知识

  2. 一个单元应该只与其朋友交流

  3. 一个单元不应该了解它所操作对象的内部细节

最少知识原则和迪米特法则相同,两者都指向松耦合的哲学。最少知识原则符合外观模式的使用场景,因为名称直观,原则一词作为指导,不是严格的,只有在需要时才有用。

Q2. 一个子系统可以有多个外观吗?

A: 是的,可以为一组子系统实现多个外观。

Q3. 最少知识原则的缺点是什么?

A: 面向对象提供了一个简化的接口,供客户端与子系统交互。在提供简化接口的精神下,一个应用程序可能包含多个不必要的接口,这些接口增加了系统的复杂性并降低了运行时性能。

Q4. 客户能否独立访问子系统?

A: 是的,事实上,外观模式提供了简化的接口,这样客户端就不必担心子系统的复杂性。

Q5. 外观是否添加了任何自己的功能?

A: 外观可以为子系统添加其“思考”,例如确保子系统的创新顺序可以由外观来决定。

摘要

我们首先通过理解结构设计模式开始了本章。然后,您学习了外观设计模式及其使用场景。我们了解了外观的基础以及它在软件架构中的有效应用。我们探讨了外观设计模式如何为客户创建一个简化的接口。它们简化了子系统的复杂性,从而使客户端受益。

外观并没有封装子系统,客户端可以自由访问子系统,甚至无需通过外观。您还通过 UML 图和 Python 3.5 的示例代码实现了该模式。我们理解了最少知识原则及其哲学如何指导外观设计模式。

我们还涵盖了一个关于常见问题解答的部分,这将帮助您获得更多关于该模式和其可能缺点的想法。我们现在准备在接下来的章节中学习更多的结构模式。

第五章. 代理模式 – 控制对象访问

在上一章中,我们简要介绍了结构模式,并继续讨论了外观设计模式。我们通过 UML 图理解了外观模式的概念,并借助 Python 实现学习了它在现实世界中的应用。在常见问题解答部分,你了解了外观模式的优缺点。

在本章中,我们向前迈进一步,处理属于结构设计模式范畴下的代理模式。我们将首先了解代理模式作为一个概念,然后讨论设计模式并探讨其在软件开发中的应用。我们将通过一个示例用例并使用 Python 3.5 来实现它。

在本章中,我们将简要介绍以下内容:

  • 代理和代理设计模式简介

  • 代理模式的 UML 图

  • 代理模式的变体

  • 使用 Python 3.5 代码实现的现实世界用例

  • 代理模式的优点

  • 比较 - 外观模式和代理模式

  • 常见问题

理解代理设计模式

代理,从一般意义上讲,是一个在寻求者和提供者之间进行中介的系统。寻求者是提出请求的一方,而提供者则根据请求提供资源。在互联网世界中,我们可以将此与代理服务器联系起来。当客户端(万维网中的用户)向网站提出请求时,首先连接到代理服务器,请求资源,如网页。代理服务器内部评估这个请求,将其发送到适当的服务器,并获取响应,然后将响应传递给客户端。因此,代理服务器封装了请求,提供了隐私保护,并在分布式架构中运行良好。

在设计模式语境中,Proxy是一个充当真实对象接口的类。对象可以是多种类型,如网络连接、内存中的大对象和文件等。简而言之,Proxy是一个包装器或代理对象,它包装了真实的服务对象。代理可以为它包装的对象提供额外的功能,而不改变对象的代码。代理模式的主要目的是提供一个替代或占位符对象,以便控制对真实对象的访问。

代理模式在以下多个场景中被使用:

  • 它以更简单的方式表示复杂系统。例如,涉及多个复杂计算或程序的系统应该有一个更简单的接口,可以作为代理为客户端提供便利。

  • 它为现有的真实对象增加了安全性。在许多情况下,客户端不允许直接访问真实对象。这是因为真实对象可能会受到恶意活动的损害。这样,代理就充当了恶意意图的盾牌,保护了真实对象。

  • 它为不同服务器上的远程对象提供了一个本地接口。一个明显的例子是与分布式系统一起使用时,客户端希望在远程系统上运行某些命令,但客户端可能没有直接权限来实现这一点。因此,它联系一个本地对象(代理)来提出请求,然后代理在远程机器上执行该请求。

  • 它为高内存消耗的对象提供了一个轻量级的处理方式。有时,你可能不希望在不必要的情况下加载主要对象。这是因为真实对象非常重,可能需要高资源利用率。一个经典的例子是网站上用户的个人资料图片。在列表视图中显示较小的个人资料图片会更好,但当然,你需要加载实际图片来显示用户个人资料的详细视图。

让我们用一个简单的例子来理解这个模式。考虑一个Actor和他的Agent的例子。当制片厂想要与一个Actor合作拍电影时,通常,他们会与Agent交谈,而不是直接与Actor交谈。根据Actor的日程和其他活动,Agent会向制片厂反馈Actor的可用性和对参与电影工作的兴趣。现在,在这种情况下,制片厂不是直接与Actor交谈,而是Agent作为Proxy来处理所有关于Actor的调度和支付。

以下 Python 代码实现了这个场景,其中ActorProxy。使用Agent对象来找出Actor是否忙碌。如果Actor忙碌,则调用Actor().occupied()方法;如果Actor不忙碌,则返回Actor().available()方法。

class Actor(object):
    def __init__(self):
        self.isBusy = False

    def occupied(self):
        self.isBusy = True
        print(type(self).__name__ , "is occupied with current movie")

    def available(self):
        self.isBusy = False
        print(type(self).__name__ , "is free for the movie")

    def getStatus(self):
        return self.isBusy

class Agent(object):
    def __init__(self):
        self.principal = None

    def work(self):
        self.actor = Actor()
        if self.actor.getStatus():
            self.actor.occupied()
        else:
            self.actor.available()

if __name__ == '__main__':
    r = Agent()
    r.work()

代理设计模式本质上执行以下操作:

  • 它为另一个对象提供了一个替代品,以便你可以控制对原始对象的访问

  • 它用作层或接口以支持分布式访问

  • 它增加了委派并保护真实组件免受不期望的影响

代理模式的 UML 类图

我们现在将借助以下 UML 图来讨论代理模式。正如我们在上一段中讨论的,代理模式有三个主要参与者:制片厂、AgentActor。让我们将这些放入 UML 图中,看看类是如何表现的:

UML 类图表示代理模式

如我们观察 UML 图,你会意识到这个模式有三个主要参与者:

  • 代理:它维护一个引用,允许Proxy访问真实对象。它提供了一个与Subject相同的接口,以便Proxy可以替代真实主题。代理还负责创建和删除RealSubject

  • 主题:它为RealSubjectProxy都提供了表示。由于ProxyRealSubject实现了Subject,因此Proxy可以在期望RealSubject的地方使用。

  • RealSubject: 它定义了Proxy所代表的真实对象。

从数据结构的角度来看,UML 图可以表示如下:

  • 代理: 这是一个控制对RealSubject类访问的类。它处理客户端的请求,并负责创建或删除RealSubject

  • Subject/RealSubject: Subject是一个接口,定义了RealSubjectProxy的外观。RealSubjectSubject接口的实际实现。它提供了客户端使用的真实功能。

  • 客户端: 它访问Proxy类以完成工作。Proxy类内部控制对RealSubject的访问,并指导客户端请求的工作。

理解不同类型的代理

在许多常见情况下使用代理。我们在本章的开头讨论了一些。根据代理的使用方式,我们可以将它们分类为虚拟代理、远程代理、保护代理和智能代理。在本节中,让我们更深入地了解它们。

一个虚拟代理

在这里,你将详细了解虚拟代理。它是为那些非常难以实例化的对象提供的占位符。例如,你希望在网站上加载一个大型图像。现在这个请求将需要很长时间才能加载。通常,开发者会在网页上创建一个占位图标,提示有图像存在。然而,图像只有在用户实际点击图标时才会加载,从而节省在内存中加载大型图像的成本。因此,在虚拟代理中,实际对象是在客户端首次请求或访问对象时创建的。

一个远程代理

远程代理可以按以下术语定义。它提供了一个位于远程服务器或不同地址空间上的实际对象的本地表示。例如,你想要为你的应用程序构建一个监控系统,该系统具有多个 Web 服务器、数据库服务器、Celery 任务服务器、缓存服务器等。如果我们想要监控这些服务器的 CPU 和磁盘利用率,我们需要一个在监控应用程序运行上下文中可用的对象,但可以执行远程命令以获取实际参数值。在这种情况下,拥有一个远程代理对象,它是远程对象的本地表示,将有所帮助。

一个保护代理

你将通过以下要点更深入地了解保护代理。这个代理控制对 RealSubject 的敏感对象访问。例如,在当今的分布式系统世界中,Web 应用程序有多个服务协同工作以提供功能。现在,在这样的系统中,一个身份验证服务充当保护代理服务器,负责身份验证和授权。在这种情况下,代理内部帮助保护网站的核心功能,防止未识别或未经授权的代理。因此,代理对象检查调用者是否有转发请求所需的访问权限。

一个智能代理。

智能代理在访问对象时执行额外的操作。例如,考虑系统中有一个核心组件,它在集中位置存储状态。通常,这样的组件会被多个不同的服务调用以完成它们的工作,这可能导致共享资源的问题。而不是让服务直接调用核心组件,内置了一个智能代理,在访问之前检查真实对象是否被锁定,以确保没有其他对象可以更改它。

代理模式在现实世界中的应用。

我们将采用支付用例来展示代理模式在现实世界中的应用场景。假设你去了商场购物,看中了一件漂亮的牛仔衬衫。你想购买这件衬衫,但你没有足够的现金。

在过去,你会去自动柜员机取钱,然后去商场支付,甚至更早的时候,你有一张银行支票,你必须去银行取款,然后回来支付你的费用。

感谢银行,我们现在有一种叫做借记卡的东西。所以现在,当你想购买某样东西时,你向商家出示你的借记卡。当你输入你的卡信息时,钱就会从商家的账户中扣除以支付你的费用。

让我们在 Python 3.5 中开发一个应用程序并实现上述用例。我们首先从客户端开始。你去了购物中心,现在想买一件漂亮的牛仔衬衫。让我们看看 Client 代码是如何编写的:

  • 你的行为由 You 类(客户端)表示。

  • 为了购买衬衫,类提供了 make_payment() 方法。

  • 特殊的 __init__() 方法调用代理并实例化它。

  • make_payment() 方法在内部调用代理的方法以完成支付。

  • 如果支付成功,__del__() 方法会返回。

因此,代码示例如下:

class You:
    def __init__(self):
        print("You:: Lets buy the Denim shirt")
        self.debitCard = DebitCard()
        self.isPurchased = None

    def make_payment(self):
        self.isPurchased = self.debitCard.do_pay()

    def __del__(self):
        if self.isPurchased:
            print("You:: Wow! Denim shirt is Mine :-)")
        else:
            print("You:: I should earn more :(")

you = You()
you.make_payment()

现在我们来谈谈 Subject 类。众所周知,Subject 类是一个接口,由 ProxyRealSubject 实现。

  • 在这个例子中,主题是 Payment 类。它是一个抽象基类,代表一个接口。

  • Payment 类有一个需要由 ProxyRealSubject 实现的 do_pay() 方法。

让我们看看以下代码中这些方法是如何发挥作用的:

from abc import ABCMeta, abstractmethod

class Payment(metaclass=ABCMeta):

    @abstractmethod
    def do_pay(self):
        pass

我们还开发了代表此场景中的RealSubjectBank类:

  • Bank实际上会从商家的账户中从你的账户中支付。

  • Bank有多个方法来处理支付。Proxy使用setCard()方法将借记卡详情发送到银行。

  • __getAccount()方法是Bank的私有方法,用于获取借记卡持有人的账户详情。为了简单起见,我们强制借记卡号与账户号相同。

  • Bank也有__hasFunds()方法来查看账户持有人是否有足够的资金支付衬衫。

  • Bank类(从支付接口)实现的do_pay()方法实际上负责根据可用资金向商家支付:

    class Bank(Payment):
    
        def __init__(self):
            self.card = None
            self.account = None
    
        def __getAccount(self):
            self.account = self.card # Assume card number is account number
            return self.account
    
        def __hasFunds(self):
            print("Bank:: Checking if Account", self.__getAccount(), "has enough funds")
            return True
    
        def setCard(self, card):
            self.card = card
    
        def do_pay(self):
            if self.__hasFunds():
                print("Bank:: Paying the merchant")
                return True
            else:
                print("Bank:: Sorry, not enough funds!")
                return False
    

让我们现在来理解最后一部分,即Proxy

  • DebitCard类在这里是Proxy。当You想要进行支付时,它会调用do_pay()方法。这是因为You不想去银行取款然后支付商家。

  • DebitCard类作为RealSubjectBank的代理。

  • payWithCard()方法内部控制RealSubjectBank类对象的创建,并将卡详情展示给Bank

  • Bank将执行之前代码片段中描述的内部账户检查和支付:

    class DebitCard(Payment):
    
        def __init__(self):
            self.bank = Bank()
    
        def do_pay(self):
            card = input("Proxy:: Punch in Card Number: ")
            self.bank.setCard(card)
            return self.bank.do_pay()
    

对于一个正面案例,当资金充足时,输出如下:

现实世界中的代理模式

对于一个负面案例——资金不足,输出如下:

现实世界中的代理模式

代理模式的优点

如我们所见,代理模式在现实世界中的工作方式,让我们浏览一下代理模式的优点:

  • 代理可以通过缓存重对象或通常频繁访问的对象来帮助提高应用程序的性能

  • 代理还授权对RealSubject的访问;因此,只有当权限正确时,此模式才有助于委托

  • 远程代理也便于与远程服务器交互,这些服务器可以作为网络连接和数据库连接使用,并可用于监控系统

比较外观模式和代理模式

外观模式和代理模式都是结构型设计模式。它们在某种意义上是相似的,因为它们都在真实对象前面有一个代理/外观对象。不同之处实际上在于模式的意图或目的,如下表所示:

代理模式 外观模式
它为你提供了一个代理或占位符来控制对另一个对象的访问 它为你提供了一个接口,用于访问大型类子系统
代理对象具有与目标对象相同的接口,并持有对目标对象的引用 它最小化了子系统之间的通信和依赖关系
它充当客户端和被包装对象之间的中介 门面(Façade)对象提供了一个单一、简化的接口

常见问题

Q1. 装饰器模式(Decorator pattern)和代理模式(Proxy pattern)之间的区别是什么?

A: 装饰器在运行时向被装饰的对象添加行为,而代理控制对对象的访问。代理和 RealSubject 之间的关系是在编译时而非动态的。

Q2. 代理模式的缺点是什么?

A: 代理模式可以增加响应时间。例如,如果代理架构不佳或存在一些性能问题,它可能会增加 RealSubject 的响应时间。通常,这都取决于代理编写的好坏。

Q3. 客户能否独立访问 RealSubject

A: 是的,但代理提供了一些特定的优势,如虚拟、远程等,因此使用代理模式是有利的。

Q4. 代理是否添加了它自己的任何功能?

A: 代理可以在不改变对象代码的情况下为 RealSubject 添加额外的功能。代理和 RealSubject 将实现相同的接口。

概述

我们从理解代理(Proxies)是什么开始本章。我们了解了代理的基本概念以及它在软件架构中的有效使用。然后,你学习了代理设计模式及其使用场景。我们探讨了代理设计模式如何控制对提供所需功能的真实对象的访问。

我们还通过 UML 图和 Python 3.5 的示例代码实现了该模式。

代理模式有四种不同的实现方式:虚拟代理、远程代理、保护代理和智能代理。你通过实际场景学习了这些内容。

我们比较了门面和代理设计模式,以便让你清楚地了解它们的使用场景和意图。

我们还涵盖了一个关于常见问题的部分,这将帮助你获得更多关于该模式和其可能的优势/劣势的灵感。

在本章结束时,我们现在准备在接下来的章节中学习更多的结构化模式。

第六章。观察者模式——保持对象知情

在上一章中,我们从对代理的简要介绍开始,然后讨论了代理设计模式。我们通过 UML 图理解了代理模式的概念,并学习了如何借助 Python 实现将其应用于现实世界。你在 FAQ 部分学习了代理模式的优缺点。

在本章中,我们将讨论第三种设计模式——行为设计模式。我们将介绍观察者设计模式,它属于行为模式范畴。我们将讨论观察者设计模式在软件开发中的应用。我们将使用一个示例用例并在 Python 3.5 中实现它。

在本章中,我们将简要介绍以下主题:

  • 行为设计模式的介绍

  • 观察者模式及其 UML 图

  • 带有 Python 3.5 代码实现的现实世界用例

  • 松散耦合的力量

  • 常见问题解答

在本章结束时,我们将总结整个讨论——请将此视为要点。

介绍行为模式

在本书的前几章中,你学习了创建模式(单例)和结构模式(外观)。在本节中,我们将简要了解行为模式。

创建模式基于对象如何被创建。它们隔离了对象创建的细节。代码与要创建的对象类型无关。结构模式设计对象和类的结构,以便它们可以一起工作以实现更大的结果。它们的主要重点是简化结构并识别类与对象之间的关系。

如其名所示,行为模式关注对象所具有的职责。它们处理对象之间的交互以实现更大的功能。行为模式建议,虽然对象应该能够相互交互,但它们仍然应该是松散耦合的。我们将在本章后面学习松散耦合的原则。

观察者设计模式是行为模式中最简单的一种。因此,让我们做好准备,更深入地了解它们。

理解观察者设计模式

在观察者设计模式中,一个对象(主题)维护一个依赖者(观察者)列表,以便主题可以使用观察者定义的任何方法通知所有观察者它所经历的变化。

在分布式应用的世界中,多个服务相互交互以执行用户想要实现的大操作。服务可以执行多个操作,但它们执行的操作直接或严重依赖于与之交互的服务对象的状态。

考虑一个用户注册的使用场景,其中用户服务负责网站上的用户操作。假设我们还有一个名为电子邮件服务的其他服务,该服务观察用户状态并向用户发送电子邮件。例如,如果用户刚刚注册,用户服务将调用电子邮件服务的一个方法,向用户发送账户验证的电子邮件。如果账户已验证但信用额度较少,电子邮件服务将监控用户服务并向用户发送低信用额的电子邮件警报。

因此,如果应用程序中有一个核心服务,许多其他服务都依赖于它,那么这个核心服务就成为了需要被观察者观察/监控以进行变更的主题。观察者反过来应该根据主题发生的变更对其自身对象的状态进行更改或采取某些行动。上述场景,即依赖服务监控核心服务状态变更,是观察者设计模式的经典案例。

在广播或发布/订阅系统中,你会找到观察者设计模式的使用。考虑一个博客的例子。假设你是一个技术爱好者,喜欢阅读这个博客上关于 Python 的最新文章。你会怎么做?你会订阅这个博客。像你一样,还有多个订阅者也注册了这个博客。所以,每当有新的博客文章时,你会收到通知,或者如果已发布的博客有变更,你也会被告知编辑。你收到变更通知的方式可以是电子邮件。现在,如果你将这个场景应用到观察者模式中,博客就是维护订阅者或观察者列表的主题。所以,当博客中添加新条目时,所有观察者都会通过电子邮件或其他通知机制(如观察者定义的)收到通知。

观察者模式的主要意图如下:

  • 它定义了对象之间的一对多依赖关系,以便任何对象的变更都会自动通知其他依赖对象

  • 它封装了主题的核心组件

观察者模式在以下多个场景中使用:

  • 分布式系统中事件服务的实现

  • 新闻机构的框架

  • 股市也是观察者模式的一个很好的案例

以下 Python 代码实现了观察者设计模式:

class Subject:
    def __init__(self):
        self.__observers = []

    def register(self, observer):
        self.__observers.append(observer)

    def notifyAll(self, *args, **kwargs):
        for observer in self.__observers:
            observer.notify(self, *args, **kwargs)

class Observer1:
    def __init__(self, subject):
        subject.register(self)

    def notify(self, subject, *args):
        print(type(self).__name__,':: Got', args, 'From', subject)

class Observer2:
    def __init__(self, subject):
        subject.register(self)

    def notify(self, subject, *args):
        print(type(self).__name__, ':: Got', args, 'From', subject)

subject = Subject()
observer1 = Observer1(subject)
observer2 = Observer2(subject)
subject.notifyAll('notification')

上述代码的输出如下:

理解观察者设计模式

观察者模式的 UML 类图

现在让我们借助以下 UML 图来更深入地了解观察者模式。

如前一段所述,观察者模式有两个主要角色:SubjectObserver。让我们将这些放入 UML 图中,看看类是如何表现的:

观察者模式的 UML 类图

当我们查看 UML 图时,你会意识到这个模式中有三个主要参与者:

  • SubjectSubject 类了解 ObserverSubject 类具有 register()deregister() 等方法,这些方法由 Observers 用于将自己注册到 Subject 类。因此,Subject 可以处理多个 Observers

  • Observer:它定义了一个接口,供观察 Subject 的对象使用。它定义了 Observer 需要实现的方法,以便在 Subject 发生变化时得到通知。

  • ConcreteObserver:它存储应与 Subject 的状态一致的状态。它通过实现 Observer 接口来保持状态与 Subject 的变化一致。

流程很简单。ConcreteObservers 通过实现 Observer 提供的接口来将自己注册到主体。每当状态发生变化时,主体通过 Observers 提供的 notify 方法通知所有 ConcreteObservers

实际世界中的观察者模式

我们将采用一个新闻机构的案例来展示观察者模式在实际世界中的应用场景。新闻机构通常从各个地点收集新闻,并将其发布给订阅者。让我们看看这个用例的设计考虑因素。

由于信息是实时发送/接收的,新闻机构应该能够尽快将其新闻发布给订阅者。此外,由于技术行业的进步,不仅报纸,订阅者也可以是不同类型,如电子邮件、手机、短信或语音通话。我们还应该能够添加任何其他类型的订阅者,并为任何新技术预留预算。

让我们在 Python 3.5 中开发一个应用程序并实现前面的用例。我们将从主体,即新闻发布者开始:

  • 主体行为由 NewsPublisher 类表示。

  • NewsPublisher 提供了一个接口,使订阅者可以与之交互。

  • attach() 方法由 Observer 用于注册到 NewsPublisher,而 detach() 方法有助于注销 Observer

  • subscriber() 方法返回已经注册到 Subject 的所有订阅者的列表。

  • notifySubscriber() 方法遍历所有已注册到 NewsPublisher 的订阅者。

  • 发布者使用 addNews() 方法创建新的新闻,而 getNews() 用于返回最新的新闻,然后通过 Observer 通知。

让我们先看看 NewsPublisher 类:

class NewsPublisher:
    def __init__(self):
        self.__subscribers = []
        self.__latestNews = None

    def attach(self, subscriber):
        self.__subscribers.append(subscriber)

    def detach(self):
        return self.__subscribers.pop()

    def subscribers(self):
        return [type(x).__name__ for x in self.__subscribers]

    def notifySubscribers(self):
        for sub in self.__subscribers:
            sub.update()

    def addNews(self, news):
        self.__latestNews = news

    def getNews(self):
        return "Got News:", self.__latestNews

现在我们来谈谈 Observer 接口:

  • 在这个例子中,Subscriber 代表 Observer。它是一个抽象基类,代表任何其他 ConcreteObserver

  • Subscriber 有一个 update() 方法,需要由 ConcreteObservers 实现。

  • update() 方法由 ConcreteObserver 实现,以便它们能够从 Subject (NewsPublishers) 接收任何新闻发布的通知。

让我们现在看看 Subscriber 抽象类的代码:

from abc import ABCMeta, abstractmethod

class Subscriber(metaclass=ABCMeta):

    @abstractmethod
    def update(self):
        pass

我们还开发了一些代表 ConcreteObserver 的类:

  • 在这种情况下,我们有两个主要的观察者:实现了订阅者接口的 EmailSubscriberSMSSubscriber

  • 除了这两个之外,我们还有一个名为 AnyOtherObserver 的其他观察者,它展示了 ObserversSubject 的松耦合。

  • 每个 ConcreteObserver__init__() 方法使用 attach() 方法将其注册到 NewsPublisher

  • ConcreteObserverupdate() 方法由 NewsPublisher 内部使用来通知新闻的增加。

下面是如何实现 SMSSubscriber 类的示例:

class SMSSubscriber:
    def __init__(self, publisher):
        self.publisher = publisher
        self.publisher.attach(self)

    def update(self):
        print(type(self).__name__, self.publisher.getNews())

class EmailSubscriber:
    def __init__(self, publisher):
        self.publisher = publisher
        self.publisher.attach(self)

    def update(self):
        print(type(self).__name__, self.publisher.getNews())

class AnyOtherSubscriber:
    def __init__(self, publisher):
        self.publisher = publisher
        self.publisher.attach(self)

    def update(self):
        print(type(self).__name__, self.publisher.getNews())

现在所有必需的订阅者都已实现,让我们看看 NewsPublisherSMSSubscribers 类的实际应用:

  • 客户端创建一个 NewsPublisher 对象,该对象被 ConcreteObservers 用于各种操作。

  • SMSSubscriberEmailSubscriberAnyOtherSubscriber 类使用发布者对象进行初始化。

  • 在 Python 中,当我们创建对象时,会调用 __init__() 方法。在 ConcreteObserver 类中,__init__() 方法内部使用 NewsPublisherattach() 方法来注册自己以接收新闻更新。

  • 然后我们打印出所有已注册到 Subject 的订阅者(ConcreteObservers)的列表。

  • 使用 addNews() 方法通过 NewsPublisher(news_publisher)对象创建新的新闻。

  • NewsPublishernotifySubscribers() 方法用于通知所有订阅者新闻的增加。notifySubscribers() 方法内部调用 ConcreteObserver 实现的 update() 方法,以便它们获取最新的新闻。

  • NewsPublisher 还有一个 detach() 方法,用于从已注册订阅者列表中删除订阅者。

以下代码实现表示了 SubjectObservers 之间的交互:

if __name__ == '__main__':
    news_publisher = NewsPublisher()

    for Subscribers in [SMSSubscriber, EmailSubscriber, AnyOtherSubscriber]:
        Subscribers(news_publisher)
    print("\nSubscribers:", news_publisher.subscribers())

    news_publisher.addNews('Hello World!')
    news_publisher.notifySubscribers()

    print("\nDetached:", type(news_publisher.detach()).__name__)
    print("\nSubscribers:", news_publisher.subscribers())

    news_publisher.addNews('My second news!')
    news_publisher.notifySubscribers()

上述代码的输出如下:

现实世界中的观察者模式

观察者模式的方法

通知 Observer 关于 Subject 中发生的变化有两种不同的方式。它们可以被归类为推送或拉模型。

拉模型

在拉模型中,Observers 扮演着以下积极的角色:

  • 当有任何变化时,Subject 向所有已注册的 Observers 广播。

  • 当有变更时,Observer 负责获取变更或从订阅者那里拉取数据。

  • 拉模型是无效的,因为它涉及两个步骤——第一步是 Subject 通知 Observer,第二步是 ObserverSubject 中拉取所需的数据。

推送模型

在推送模型中,Subject 扮演着主导角色如下:

  • 与拉模型不同,更改是由Subject推送到Observer的。

  • 在这个模型中,Subject可以向Observer发送详细的信息(即使可能不需要)。当Subject发送大量数据时,这可能会导致响应时间缓慢,但这些数据实际上从未被Observer使用。

  • 只从Subject发送所需的数据,从而提高性能。

松散耦合与观察者模式

松散耦合是一个重要的设计原则,应该在软件应用中使用。松散耦合的主要目的是努力在相互交互的对象之间实现松散耦合的设计。耦合指的是一个对象对其交互对象所具有的知识程度。

松散耦合的设计使我们能够构建灵活的面向对象系统,可以处理变化,因为它们减少了多个对象之间的依赖性。

松散耦合架构确保以下特性:

  • 它降低了在一个元素内部做出的更改可能对其他元素产生未预期影响的风险

  • 它简化了测试、维护和故障排除问题

  • 系统可以轻松地分解为可定义的元素

观察者模式为你提供了一个对象设计,其中SubjectObserver是松散耦合的。以下要点将更好地解释这一点:

  • SubjectObserver的唯一了解是它实现了一个特定的接口。它不需要知道ConcreteObserver类。

  • 任何新的Observer都可以在任何时候添加(正如我们在本章前面的示例中所看到的)。

  • 添加任何新的Observer不需要修改Subject。在示例中,我们看到AnyOtherObserver可以添加/删除,而无需对Subject进行任何更改。

  • SubjectObserver之间没有绑定,可以独立使用。因此,如果需要,Observer可以在任何其他地方重用。

  • SubjectObserver的变化不会相互影响。由于两者都是独立的或松散耦合的,它们可以自由地进行自己的更改。

观察者模式 - 优点和缺点

观察者模式为你提供了以下优点:

  • 它支持对象之间松散耦合的原则

  • 它允许在SubjectObserver类不发生变化的情况下,有效地将数据发送到其他对象

  • Observers可以在任何时间添加/删除

以下列举了观察者模式的缺点:

  • ConcreteObserver必须实现观察者接口,这涉及到继承。没有组合的选项,因为观察者接口可以被实例化。

  • 如果没有正确实现,Observer可能会增加复杂性并导致意外的性能问题。

  • 在软件应用中,通知有时可能不可靠,导致竞争条件或不一致性。

常见问题解答

Q1. 是否可以有多个主题(Subjects)和观察者(Observers)?

A:对于软件应用来说,存在一个情况是拥有多个主题(Subjects)和观察者。为了使这可行,观察者需要被通知主题(Subjects)的变化以及哪个主题发生了变化。

Q2. 谁负责触发更新?

A:正如你之前所学的,观察者模式可以在推送和拉取模型中工作。通常,当有变化时,主题(Subject)会触发更新方法,但有时根据应用需求,观察者(Observer)也可以触发通知。然而,需要注意频率不应过高,否则可能会导致性能下降,尤其是在主题更新频率较低时。

Q3. 主题(Subject)或观察者能否用于其他用例的访问?

A:是的,这就是观察者模式中体现的松耦合的力量。主题(Subject)/观察者都可以独立使用。

摘要

我们从理解行为设计模式开始本章。我们了解了观察者模式的基础以及它在软件架构中的有效应用。我们探讨了如何使用观察者设计模式来通知主题(Subject)的变化。它们管理对象之间的交互,并管理对象的一对多依赖关系。

你还学习了使用 UML 图和 Python 3.5 的示例代码实现该模式。

观察者模式有两种不同的实现方式:推送和拉取模型。你学习了这些模式,并讨论了它们的实现和性能影响。

我们理解了软件设计中的松耦合原则,以及观察者模式如何利用这一原则进行应用开发。

我们还涵盖了一个关于常见问题解答(FAQs)的部分,这将帮助你获得更多关于该模式及其可能的优势/劣势的想法。

在本章结束时,我们现在准备在接下来的章节中学习更多的行为模式。

第七章:命令模式 – 封装调用

在上一章中,我们开始介绍行为设计模式。你学习了观察者的概念,并讨论了观察者设计模式。我们通过 UML 图理解了观察者设计模式,并借助 Python 实现学习了它在现实世界中的应用。我们讨论了观察者模式的优缺点。你还在本章的常见问题解答部分学习了观察者模式,并在章节末尾总结了讨论内容。

在本章中,我们将讨论命令设计模式。与观察者模式一样,命令模式属于行为模式范畴。我们将介绍命令设计模式,并讨论它在软件开发中的应用。我们将通过一个示例用例并使用 Python 3.5 实现它。

本章将简要介绍以下内容:

  • 命令设计模式的简介

  • 命令模式及其 UML 图

  • 使用 Python 3.5 代码实现的现实世界用例

  • 命令模式的优缺点

  • 常见问题解答

介绍命令模式

正如我们在上一章中看到的,行为模式关注对象所拥有的职责。它处理对象之间的交互以实现更大的功能。命令模式是一种行为设计模式,其中使用一个对象来封装执行动作或触发事件所需的所有信息,以便在稍后时间执行。这些信息包括以下内容:

  • 方法名称

  • 拥有方法的对象

  • 方法参数的值

让我们通过一个非常简单的软件示例来理解这个模式。考虑安装向导的情况。一个典型的向导可能包含多个阶段或屏幕,用于捕获用户的偏好。当用户浏览向导时,他们会做出某些选择。向导通常使用命令模式实现。向导首先由一个名为Command的对象启动。用户在向导的多个阶段做出的偏好或选择随后被存储在Command对象中。当用户在向导的最后屏幕上点击完成按钮时,Command对象运行一个execute()方法,该方法考虑所有存储的选择并运行适当的安装程序。因此,所有关于选择的信息都被封装在一个对象中,可以在以后采取行动时使用。

另一个简单的例子是打印机打印队列。打印队列可以作为存储诸如页面类型(A5-A1)、纵向/横向、整理/未整理等信息的一个Command对象实现。当用户打印某些内容(例如,一张图片)时,打印队列在Command对象上运行execute()方法,并按照设定的偏好打印图片。

理解命令设计模式

命令模式与以下术语一起工作—CommandReceiverInvokerClient

  • 一个Command对象了解Receiver对象并调用Receiver对象的方法。

  • 接收者方法参数的值存储在Command对象中

  • Invoker知道如何执行一个命令

  • 客户端创建一个Command对象并设置其接收者

命令模式的主要意图如下:

  • 将请求封装为对象

  • 允许客户端使用不同的请求进行参数化

  • 允许将请求保存到队列中(我们将在本章后面讨论这个问题)

  • 提供面向对象的回调

命令模式可以在以下多个场景中使用:

  • 根据要执行的操作参数化对象

  • 在队列中添加动作并在不同的点执行请求

  • 为基于较小操作的高级操作创建结构

以下 Python 代码实现了命令设计模式。我们在本章前面讨论了巫师的例子。假设我们想要开发一个用于安装或通常所说的安装程序。通常,安装意味着根据用户的选择在文件系统中复制或移动文件。在下面的例子中,在客户端代码中,我们首先创建Wizard对象并使用preferences()方法,该方法存储用户在巫师的各种屏幕上做出的选择。在巫师中,当点击完成按钮时,调用execute()方法。execute()方法获取偏好设置并开始安装:

class Wizard():

    def __init__(self, src, rootdir):
        self.choices = []
        self.rootdir = rootdir
        self.src = src

    def preferences(self, command):
        self.choices.append(command)

    def execute(self):
        for choice in self.choices:
            if list(choice.values())[0]:
                print("Copying binaries --", self.src, " to ", self.rootdir)
            else:
                print("No Operation")

if __name__ == '__main__':
  ## Client code
  wizard = Wizard('python3.5.gzip', '/usr/bin/')
  ## Users chooses to install Python only
  wizard.preferences({'python':True})
  wizard.preferences({'java':False})
  wizard.execute()

上述代码的输出如下:

理解命令设计模式

命令模式的 UML 类图

让我们借助以下 UML 图更深入地了解命令模式。

如前一段所述,命令模式有这些主要参与者:CommandConcreteCommandReceiverInvokerClient。让我们将这些放入 UML 图中,看看类是如何看起来:

命令模式的 UML 类图

当我们查看 UML 图时,你会意识到这个模式中有五个主要参与者:

  • Command:这个声明了一个执行操作的接口

  • ConcreteCommand:这个定义了Receiver对象和动作之间的绑定

  • Client:这个创建一个ConcreteCommand对象并设置其接收者

  • Invoker:这个请求ConcreteCommand执行请求

  • Receiver:这个知道如何执行与执行请求相关的操作

流程很简单。客户端请求执行一个命令。调用者接收命令,封装它,并将其放入队列。ConcreteCommand类负责请求的命令,并要求接收者执行给定的操作。以下代码示例用于理解所有参与者都参与的模式:

from abc import ABCMeta, abstractmethod

class Command(metaclass=ABCMeta):
    def __init__(self, recv):
        self.recv = recv

    def execute(self):
        pass

class ConcreteCommand(Command):
    def __init__(self, recv):
        self.recv = recv

    def execute(self):
        self.recv.action()

class Receiver:
    def action(self):
        print("Receiver Action")

class Invoker:
    def command(self, cmd):
        self.cmd = cmd

    def execute(self):
        self.cmd.execute()

if __name__ == '__main__':
    recv = Receiver()
    cmd = ConcreteCommand(recv)
    invoker = Invoker()
    invoker.command(cmd)
    invoker.execute()

在现实世界中实现命令模式

我们将以股票交易所(在互联网世界中广为人知)为例,演示命令模式的实现。在股票交易所中会发生什么?作为股票交易所的用户,你创建买卖股票的订单。通常,你不会直接买卖股票;这是代理或经纪人作为你在股票交易所之间的中介。代理负责将你的请求带到股票交易所并完成工作。想象一下,你想在周一早上交易所开盘时卖出股票。即使交易所还没有开盘,你仍然可以在周日晚上向你的代理提出卖出股票的请求。然后代理将这个请求排队,以便在交易所开盘进行交易时执行。这是一个经典的命令模式案例。

设计考虑因素

根据 UML 图,你了解到命令模式有四个主要参与者——CommandConcreteCommandInvokerReceiver。对于前面的场景,我们应该创建一个Order接口来定义客户放置的订单。我们应该定义用于买卖股票的ConcreteCommand类。还需要定义一个代表证券交易所的类。我们应该定义一个将实际执行交易和代理(也称为调用者)的Receiver类,该代理调用订单并通过接收者执行它。

让我们在 Python 3.5 中开发一个应用程序并实现前面的用例。我们首先从Command对象Order开始:

  • Command对象由Order类表示

  • Order提供了一个接口(Python 的抽象基类),以便ConcreteCommand可以实现行为

  • execute()方法是ConcreteCommand类需要定义的抽象方法,用于执行Order

以下代码表示抽象类Order和抽象方法execute()

from abc import ABCMeta, abstractmethod

class Order(metaclass=ABCMeta):

    @abstractmethod
    def execute(self):
        pass

我们还开发了一些代表ConcreteCommand的类:

  • 在这种情况下,我们有两个主要的具体类:BuyStockOrderSellStockOrder,它们实现了Order接口

  • 两个ConcreteCommand类都使用股票交易系统的对象,以便它们可以为交易系统定义适当的操作

  • 这些ConcreteCommand类的每个execute()方法都使用股票交易对象来执行买卖操作

现在让我们看看实现该接口的具体类:

class BuyStockOrder(Order):
    def __init__(self, stock):
        self.stock = stock

    def execute(self):
        self.stock.buy()

class SellStockOrder(Order):
    def __init__(self, stock):
        self.stock = stock

    def execute(self):
        self.stock.sell()

现在,让我们谈谈股票交易系统及其实现方式:

  • 在这个例子中,StockTrade类代表Receiver对象

  • 它定义了多个方法(动作)来执行ConcreteCommand对象放置的订单

  • buy()sell()方法由接收者定义,分别由BuyStockOrderSellStockOrder调用以在交易所买卖股票

让我们来看看StockTrade类:

class StockTrade:
    def buy(self):
        print("You will buy stocks")

    def sell(self):
        print("You will sell stocks")

实现的另一个部分是调用者:

  • Agent类代表调用者。

  • Agent是客户端和StockExchange之间的中介,执行客户端放置的订单。

  • Agent定义了一个数据成员__orderQueue(一个列表),它充当队列。任何由客户端放置的新订单都会添加到队列中。

  • AgentplaceOrder()方法负责排队订单并执行订单。

以下代码描述了执行Invoker角色的Agent类:

class Agent:
    def __init__(self):
        self.__orderQueue = []

    def placeOrder(self, order):
        self.__orderQueue.append(order)
        order.execute()

现在我们将所有上述类放在一起,看看客户端是如何实现的:

  • 客户端首先设置其接收者,即StockTrade

  • 使用BuyStockOrderSellStockOrderConcreteCommand)创建买卖股票的订单,这些订单在StockTrade上执行动作

  • 通过实例化Agent类创建调用者对象

  • AgentplaceOrder()方法用于获取客户端放置的订单

以下是为客户端实现的代码:

if __name__ == '__main__':
    #Client
    stock = StockTrade()
    buyStock = BuyStockOrder(stock)
    sellStock = SellStockOrder(stock)

    #Invoker
    agent = Agent()
    agent.placeOrder(buyStock)
    agent.placeOrder(sellStock)

以下是前面代码的输出:

设计考虑因素

命令模式在软件应用中有多种使用方式。我们将讨论两个与云应用非常相关的特定实现:

  • 重做或回滚操作:

    • 在实现回滚或重做操作时,开发者可以执行两种不同的操作。

    • 这些是为了在文件系统或内存中创建一个快照,当需要回滚时,可以恢复到这个快照。

    • 使用命令模式,你可以存储命令序列,当需要重做时,重新执行相同的动作集。

  • 异步任务执行:

    • 在分布式系统中,我们经常需要执行异步任务的功能,这样核心服务在更多请求的情况下永远不会被阻塞。

    • 在命令模式中,调用者对象可以维护一个请求队列,并将这些任务发送到Receiver对象,以便它们可以在主应用程序线程之外独立执行。

命令模式的优缺点

命令模式有以下优点:

  • 它将调用操作的对象与知道如何执行操作的对象解耦

  • 它通过提供队列系统允许你创建命令序列

  • 添加新命令的扩展很容易,并且可以在不更改现有代码的情况下完成

  • 你也可以使用命令模式定义一个回滚系统,例如,在向导示例中,我们可以编写一个回滚方法

以下是指令模式的缺点:

  • 有许多类和对象协同工作以实现一个目标。应用开发者需要小心地正确开发这些类。

  • 每个单独的命令都是一个ConcreteCommand类,这增加了实现和维护的类数量。

常见问题

Q1. 是否可以没有ReceiverConcreteCommand实现execute方法?

A: 当然,这样做是完全可能的。许多软件应用也以这种方式使用命令模式。这里需要注意的是调用者和接收者之间的交互。如果接收者未定义,解耦程度会降低;此外,参数化命令的功能也会丢失。

Q2. 我在调用者对象中实现队列机制时使用什么数据结构?

A: 在本章前面我们研究的股票交易所示例中,我们使用列表来实现队列。然而,命令模式讨论的是堆栈实现,这在重做或回滚开发的情况下非常有帮助。

摘要

我们从理解命令设计模式及其在软件架构中的有效应用开始本章。

我们探讨了如何使用命令设计模式来封装触发事件或动作所需的所有信息,以便在稍后的时间点执行。

您还通过 UML 图和 Python v3.5 的示例代码实现以及解释学习了该模式。

我们还涵盖了一个常见问题解答部分,这将帮助您获得更多关于该模式及其可能的优势/劣势的想法。

我们将在接下来的章节中探讨其他行为设计模式。

第八章. 模板方法模式 – 封装算法

在上一章中,我们从一个介绍命令设计模式开始,其中使用一个对象来封装执行动作或触发事件所需的所有信息。我们通过 UML 图理解了命令设计模式的概念,并看到了如何通过 Python 实现将其应用于现实世界。我们讨论了命令模式的优缺点,在常见问题解答部分进行了更深入的探讨,并在章节末尾总结了讨论。

在本章中,我们将讨论模板设计模式,例如命令模式和属于行为模式之下的模板模式。我们将介绍模板设计模式,并讨论它在软件开发中的应用。我们还将处理一个示例用例,并在 Python v3.5 中实现它。

在本章中,我们将简要介绍以下主题:

  • 模板方法设计模式的介绍

  • 模板模式及其 UML 图

  • 带有 Python v3.5 代码实现的现实世界用例

  • 模板模式 – 优点和缺点

  • 好莱坞原则、模板方法和模板钩子

  • 常见问题解答

在本章结束时,你将能够分析适用于模板设计模式的情况,并有效地使用它们来解决与设计相关的问题。我们还将总结关于模板方法模式的整个讨论作为总结。

定义模板方法模式

正如我们在上一章所看到的,行为模式关注对象所具有的职责。它处理对象之间的交互以实现更大的功能。模板方法模式是一种行为设计模式,它在一个称为模板方法的方法中定义了程序的骨架或算法。例如,你可以将准备饮料的步骤定义为一个模板方法中的算法。模板方法模式还通过将某些步骤的实现推迟到子类中来帮助重新定义或定制算法的某些步骤。这意味着子类可以重新定义它们自己的行为。例如,在这种情况下,子类可以使用模板方法来准备饮料,实现准备茶的步骤。需要注意的是,步骤的变化(由子类执行)不会影响原始算法的结构。因此,模板方法模式中子类覆盖的功能允许创建不同的行为或算法。

要用软件开发术语来谈论模板方法模式,抽象类用于定义算法的步骤。这些步骤也被称为模板方法模式中的原始操作。这些步骤通过抽象方法定义,模板方法定义算法。ConcreteClass(继承自抽象类)实现了算法的子类特定步骤。

模板方法模式在以下情况下使用:

  • 当多个算法或类实现类似或相同的逻辑时。

  • 在子类中实现算法有助于减少代码重复。

  • 通过让子类通过覆盖实现行为,可以定义多个算法。

让我们用一个非常简单的日常例子来理解这个模式。想想你准备茶或咖啡时都做了些什么。在咖啡的情况下,你执行以下步骤来准备饮料:

  1. 煮水。

  2. 煮咖啡豆。

  3. 在咖啡杯中倒。

  4. 在杯子中加入糖和牛奶。

  5. 搅拌,咖啡就煮好了。

现在,如果你想泡一杯茶,你需要执行以下步骤:

  1. 煮水。

  2. 浸泡茶包。

  3. 在杯子中倒茶。

  4. 在茶中加柠檬。

  5. 搅拌,茶就煮好了。

如果你分析这两种准备方法,你会发现两种程序或多或少是相同的。在这种情况下,我们可以有效地使用模板方法模式。我们如何实现它?我们定义一个Beverage类,它具有准备茶和咖啡的通用抽象方法,例如boilWater()。我们还定义了preparation()模板方法,它将调用准备饮料(算法)的步骤序列。我们让具体的类PrepareCoffeePrepareTea定义定制步骤以实现准备咖啡和茶的目标。这就是模板方法模式如何避免代码重复。

另一个简单的例子是计算机语言使用的编译器。编译器本质上做两件事:收集源代码并将其编译为目标对象。现在,如果我们需要为 iOS 设备定义一个交叉编译器,我们可以借助模板方法模式来实现。我们将在本章后面详细讨论这个例子。

理解模板方法设计模式。

简而言之,模板方法模式的主要意图如下:

  • 定义算法的骨架,使用原始操作。

  • 重新定义子类的某些操作而不改变算法的结构。

  • 实现代码重用,避免重复劳动。

  • 利用公共接口或实现。

模板方法模式与以下术语一起使用—AbstractClassConcreteClass、模板方法和Client

  • AbstractClass:这声明了一个接口来定义算法的步骤。

  • ConcreteClass:这定义了子类特定的步骤定义。

  • template_method():这通过调用步骤方法来定义算法。

我们在章节中早些时候讨论了编译器的例子。假设我们想要为 iOS 设备开发自己的交叉编译器并运行程序。

我们首先开发一个抽象类(编译器),该类定义了编译器的算法。编译器执行的操作是收集程序语言中编写的代码源,并将其编译成目标代码(二进制格式)。我们定义这些步骤为collectSource()compileToObject()抽象方法,并定义了负责执行程序的run()方法。算法由compileAndRun()方法定义,该方法内部调用collectSource()compileToObject()run()方法来定义编译器的算法。iOSCompiler具体类现在实现了抽象方法,并在 iOS 设备上编译/运行 Swift 代码。

小贴士

Swift 编程语言用于在 iOS 平台上开发应用程序。

以下 Python 代码实现了模板方法设计模式:

from abc import  ABCMeta, abstractmethod

class Compiler(metaclass=ABCMeta):
    @abstractmethod
    def collectSource(self):
        pass

    @abstractmethod
    def compileToObject(self):
        pass

    @abstractmethod
    def run(self):
        pass

    def compileAndRun(self):
        self.collectSource()
        self.compileToObject()
        self.run()

class iOSCompiler(Compiler):
    def collectSource(self):
        print("Collecting Swift Source Code")

    def compileToObject(self):
        print("Compiling Swift code to LLVM bitcode")

    def run(self):
        print("Program runing on runtime environment")

iOS = iOSCompiler()
iOS.compileAndRun()

上述代码的输出应如下所示:

理解模板方法设计模式

模板方法模式的 UML 类图

让我们借助 UML 图来更深入地了解模板方法模式。

如前所述,模板方法模式有以下主要参与者:抽象类、具体类、模板方法和客户端。让我们将这些放入 UML 图中,看看类是如何看起来:

模板方法模式的 UML 类图

当我们查看 UML 图时,你会意识到这个模式中有四个主要参与者:

  • AbstractClass: 它使用抽象方法定义算法的操作或步骤。这些步骤被具体子类覆盖。

  • template_method(): 这定义了算法的骨架。模板方法中调用由抽象方法定义的多个步骤,以定义序列或算法本身。

  • ConcreteClass: 它实现了步骤(由抽象方法定义),以执行算法的子类特定步骤。

以下是一个代码示例,以了解所有参与者都参与的模板方法设计模式:

from abc import ABCMeta, abstractmethod

class AbstractClass(metaclass=ABCMeta):
    def __init__(self):
        pass

    @abstractmethod
    def operation1(self):
        pass

    @abstractmethod
    def operation2(self):
        pass

    def template_method(self):
        print("Defining the Algorithm. Operation1 follows Operation2")
        self.operation2()
        self.operation1()

class ConcreteClass(AbstractClass):

    def operation1(self):
        print("My Concrete Operation1")

    def operation2(self):
        print("Operation 2 remains same")

class Client:
    def main(self):
        self.concreate = ConcreteClass()
        self.concreate.template_method()

client = Client()
client.main()

上述代码的输出应如下所示:

模板方法模式的 UML 类图

模板方法模式在现实世界中的应用

让我们用一个非常容易理解的场景来实现模板方法模式。想象一下旅行社的情况,比如 Dev Travels。现在他们是怎样典型工作的呢?他们定义了去往不同地点的各种旅行,并为您提供假日套餐。套餐本质上是你作为客户所承担的旅行。旅行有诸如访问的地点、使用的交通方式以及其他定义旅行行程的因素。根据客户的需求,这个旅行可以有不同的定制。这不就是模板方法模式的需求吗?

设计考虑因素:

  • 对于前面的场景,根据 UML 图,我们应该创建一个定义旅行的AbstractClass接口

  • 旅行应包含多个抽象方法,用于定义所使用的交通方式、day1day2day3期间访问的地点,假设这是一个为期三天的周末之旅,并定义返程

  • itinerary()模板方法实际上定义了旅行的行程

  • 我们应该定义ConcreteClasses,这样我们就可以根据客户的需求定制不同的旅行

让我们在 Python 3.5 中开发一个应用程序并实现前面的用例。我们从一个抽象类Trip开始:

  • 抽象对象由Trip类表示。它是一个接口(Python 的抽象基类),定义了诸如使用的交通方式和不同日期要访问的地点等细节。

  • setTransport是一个抽象方法,应由ConcreteClass实现以设置交通方式。

  • day1()day2()day3()抽象方法定义了给定日期访问的地点。

  • itinerary()模板方法创建完整的行程(在这种情况下,即旅行)。旅行的顺序是首先定义交通方式,然后是每天要访问的地点,以及returnHome

以下代码实现了 Dev Travels 的场景:

from abc import abstractmethod, ABCMeta

class Trip(metaclass=ABCMeta):

    @abstractmethod
    def setTransport(self):
        pass

    @abstractmethod
    def day1(self):
        pass

    @abstractmethod
    def day2(self):
        pass

    @abstractmethod
    def day3(self):
        pass

    @abstractmethod
    def returnHome(self):
        pass

    def itinerary(self):
        self.setTransport()
        self.day1()
        self.day2()
        self.day3()
        self.returnHome()

我们还开发了一些代表具体类的类:

  • 在这种情况下,我们有两个主要的实体类—VeniceTripMaldivesTrip—它们实现了Trip接口

  • 具体类代表游客根据他们的选择和兴趣所采取的两种不同的旅行

  • VeniceTripMaldivesTrip都实现了setTransport()day1()day2()day3()returnHome()

让我们在 Python 代码中定义具体的类:

class VeniceTrip(Trip):
    def setTransport(self):
        print("Take a boat and find your way in the Grand Canal")

    def day1(self):
        print("Visit St Mark's Basilica in St Mark's Square")

    def day2(self):
        print("Appreciate Doge's Palace")

    def day3(self):
        print("Enjoy the food near the Rialto Bridge")

    def returnHome(self):
        print("Get souvenirs for friends and get back")

class MaldivesTrip(Trip):
    def setTransport(self):
        print("On foot, on any island, Wow!")

    def day1(self):
        print("Enjoy the marine life of Banana Reef")

    def day2(self):
        print("Go for the water sports and snorkelling")

    def day3(self):
        print("Relax on the beach and enjoy the sun")

    def returnHome(self):
        print("Dont feel like leaving the beach..")

现在,让我们谈谈旅行社和想要度过美好假期的游客:

  • TravelAgency类代表本例中的Client对象

  • 它定义了arrange_trip()方法,为顾客提供选择是否想要历史之旅或海滩之旅的选项

  • 根据游客的选择,实例化一个适当的类

  • 此对象随后调用itinerary()模板方法,并根据客户的选择为游客安排旅行

以下是对 Dev 旅行社的实现以及他们如何根据客户的选择安排旅行的示例:

class TravelAgency:
    def arrange_trip(self):
        choice = input("What kind of place you'd like to go historical or to a beach?")
        if choice == 'historical':
            self.trip = VeniceTrip()
            self.trip.itinerary()
        if choice == 'beach':
            self.trip = MaldivesTrip()
            self.trip.itinerary()

TravelAgency().arrange_trip()

上述代码的输出应如下所示:

现实世界中的模板方法模式

如果你决定进行一次历史之旅,这将产生以下代码输出:

现实世界中的模板方法模式

模板方法模式 – 钩子

钩子是在抽象类中声明的方法。它通常有一个默认实现。钩子的理念是给予子类在需要时将自身钩入算法的能力。子类使用钩子不是强制的,并且可以轻松忽略它。

例如,在饮料示例中,我们可以添加一个简单的钩子,根据客户的愿望查看是否需要提供茶或咖啡以及调料。

另一个钩子的例子可以在旅行社示例中。现在,如果我们有一些老年游客,他们可能不想在旅行的三天中都外出,因为他们可能容易感到疲劳。在这种情况下,我们可以开发一个钩子,确保day2的行程较轻松,这意味着他们可以去一些附近的地方,并在day3的计划中返回。

基本上,当我们需要子类提供实现时,我们使用抽象方法,而当子类实现它是可选的时候,我们使用钩子。

好莱坞原则和模板方法

好莱坞原则是总结为别给我们打电话,我们会给你打电话的设计原则。它源自好莱坞哲学,即制片厂在演员有角色时才会给演员打电话。

在面向对象的世界中,我们允许低级组件使用好莱坞原则将自己钩接到系统中。然而,高级组件决定低级系统何时以及需要什么。换句话说,高级组件将低级组件视为别给我们打电话,我们会给你打电话

这与模板方法模式相关,因为它是高级抽象类安排步骤以定义算法。根据算法的实际情况,低级类被调用以定义步骤的具体实现。

模板方法模式的优势和缺点

模板方法模式为你提供了以下优势:

  • 正如我们在本章前面看到的,没有代码重复。

  • 模板方法模式通过使用继承而不是组合来实现代码复用。只需要重写少数几个方法。

  • 灵活性允许子类决定如何实现算法中的步骤。

模板方法模式的缺点如下:

  • 在某些时候,调试和理解模板方法模式中的流程顺序可能会令人困惑。您可能会实现一个不应该实现的方法,或者根本不实现一个抽象方法。文档和严格的错误处理必须由程序员来完成。

  • 模板框架的维护可能是一个问题,因为任何级别的更改(低级或高级)都可能干扰实现。因此,使用模板方法模式进行维护可能会很痛苦。

常见问题

Q1. 是否应该禁止低级组件调用高级组件中的方法?

A: 不,低级组件肯定会通过继承调用高级组件。然而,程序员需要确保没有循环依赖,即低级组件和高级组件相互依赖。

Q2. 策略模式不类似于模板模式吗?

A: 策略模式和模板模式都封装了算法。模板模式依赖于继承,而策略模式使用组合。模板方法模式是通过子类化进行编译时算法选择,而策略模式是运行时选择。

摘要

我们从理解模板方法设计模式及其在软件架构中的有效应用开始本章。

我们还探讨了如何使用模板方法设计模式来封装算法,并通过覆盖子类中的方法提供实现不同行为的灵活性。

您通过 UML 图和 Python v3.5 的示例代码实现以及解释学习了该模式。

我们还涵盖了一个关于常见问题解答的部分,这将帮助您更好地了解该模式和它的可能优点/缺点。

我们将在下一章讨论组合模式——MVC 设计模式。

第九章。模型-视图-控制器 – 复合模式

在上一章中,我们从一个模板方法设计模式的介绍开始,其中子类重新定义了算法的具体步骤,从而实现了灵活性和代码重用。您学习了模板方法及其如何通过一系列步骤构建算法。我们讨论了 UML 图,其优缺点,在常见问题解答部分对其进行了更多了解,并在本章末尾总结了讨论。

在本章中,我们将讨论复合模式。我们将介绍模型-视图-控制器MVC)设计模式,并讨论它在软件开发中的应用。我们将使用一个示例用例,并在 Python v3.5 中实现它。

在本章中,我们将简要介绍以下内容:

  • 复合模式与模型-视图-控制器简介

  • MVC 模式和它的 UML 图

  • 带有 Python v3.5 代码实现的实际用例

  • MVC 模式——优点和缺点

  • 常见问题

在本章结束时,我们将总结整个讨论——请将此视为一个要点。

复合模式简介

在整本书中,我们探讨了各种设计模式。正如我们所见,设计模式被分为三个主要类别:结构型、创建型和行为型设计模式。您还通过示例学习了这些内容。

然而,在软件实现中,模式并不是孤立的。每个软件设计或解决方案并不是仅使用一个设计模式实现的。实际上,模式通常一起使用并组合起来,以实现特定的设计解决方案。正如 GoF 所定义的,“复合模式将两个或更多模式结合成一个解决方案,该解决方案解决重复或普遍问题。”复合模式不是一组一起工作的模式;它是对问题的通用解决方案。

现在,我们将探讨模型-视图-控制器复合模式。它是复合模式的最佳示例,并且在多年的设计解决方案中得到了应用。

模型-视图-控制器模式

MVC 是一种用于实现用户界面和易于修改和维护的架构的软件模式。本质上,MVC 模式讨论的是将应用程序分为三个基本部分:模型、视图和控制器。这三个部分相互关联,有助于将信息表示的方式与信息呈现的方式分开。

这就是 MVC 模式的工作方式:模型代表数据和业务逻辑(信息如何存储和查询),视图只是数据的表示(如何呈现),控制器是两者之间的粘合剂,它根据用户的需求指导模型和视图以某种方式行为。有趣的是,视图和控制器依赖于模型,但反之则不然。这主要是因为用户关心数据。模型可以独立工作,这是 MVC 模式的关键方面。

考虑网站的情况。这是描述 MVC 模式的经典例子之一。在网站上会发生什么?你点击一个按钮,发生一些操作,然后你看到你想要的内容。这是怎么发生的?

  • 你是用户,你与视图交互。视图是你看到的网页。你点击视图上的按钮,它告诉控制器需要做什么。

  • 控制器从视图中获取输入并将其发送到模型。模型根据用户的操作进行操作。

  • 控制器还可以根据从用户接收到的操作请求视图更改,例如更改按钮、呈现额外的 UI 元素等。

  • 模型通知视图状态的变化。这可能基于一些内部变化或外部触发器,如点击按钮。

  • 视图随后显示它直接从模型获得的状态。例如,如果用户登录到网站,他们可能会看到一个仪表板视图(登录后)。仪表板上需要填充的所有详细信息都由模型提供给视图。

MVC 设计模式与以下术语一起工作——模型、视图、控制器和客户端:

  • 模型:这声明了一个用于存储和操作数据的类

  • 视图:这声明了一个用于构建用户界面和数据显示的类

  • 控制器:这声明了一个连接模型和视图的类

  • 用户:这声明了一个基于某些操作请求特定结果的类

以下图像解释了 MVC 模式的流程:

模型-视图-控制器模式

要在软件开发术语中讨论 MVC 模式,让我们看看 MVC 模式中涉及的主要类:

  • 模型类用于定义在数据上发生的所有操作(如创建、修改和删除),并提供如何使用数据的方法。

  • 视图类是用户界面的表示。它将包含帮助我们根据应用程序的上下文和需求构建 Web 或 GUI 界面的方法。它不应包含任何自己的逻辑,只需显示它接收到的数据。

  • 控制器类用于接收请求数据并将其发送到系统的其他部分。它包含用于路由请求的方法。

MVC 模式在以下情况下使用:

  • 当需要更改展示方式而不更改业务逻辑时

  • 可以使用多个控制器与多个视图一起工作,以更改用户界面的表示。

  • 再次强调,模型可以改变,而视图不需要改变,因为它们可以独立工作。

简而言之,MVC 模式的主要意图如下:

  • 保持数据和数据展示的分离。

  • 类和实现的易于维护。

  • 修改数据存储和展示方式的灵活性。两者都是独立的,因此具有更改的灵活性。

让我们详细看看模型、视图和控制层,正如在学习 Python 设计模式Gennadiy ZlobinPackt Publishing中所述。

模型 – 应用程序的知识

模型是应用程序的基石,因为它独立于视图和控制层。反过来,视图和控制层依赖于模型。

模型还提供客户端请求的数据。通常,在应用程序中,模型由存储和返回信息的数据库表表示。模型具有改变状态的状态和方法,但不知道数据将如何被客户端看到。

确保模型在多次操作中保持一致性至关重要;否则,客户端可能会被损坏或显示过时的数据,这是完全不可接受的。

由于模型完全独立,因此在此部分工作的开发者可以专注于维护,而无需最新的视图更改。

视图 – 外观

视图是客户端在界面上看到的数据表示。视图可以独立开发,但不应该包含任何复杂逻辑。逻辑仍然应该位于控制器或模型中。

在当今世界,视图需要足够灵活,并且应该适应多个平台,如桌面、移动设备、平板电脑和多种屏幕尺寸。

视图应避免直接与数据库交互,并依赖模型获取所需数据。

控制器 – 粘合剂

如其名所示,控制器控制用户在界面上的交互。当用户点击界面上某些元素时,基于交互(按钮点击或触摸),控制器会调用模型,从而创建、更新或删除数据。

控制器也将数据传递给视图,以便用户可以在界面上查看信息。

控制器不应进行数据库调用或参与数据展示。控制器应作为模型和视图之间的粘合剂,尽可能薄。

现在我们开始动手,开发一个示例应用程序。下面的 Python 代码实现了 MVC 设计模式。假设我们想要开发一个应用程序,告诉用户云公司提供的营销服务,包括电子邮件、短信和语音设施。

我们首先开发model类(模型),该类定义了产品提供的服务,即电子邮件、短信和语音。每个服务都有指定的费率,例如,1000 封电子邮件将向客户收取 2 美元,1000 条消息的收费是 10 美元,1000 条语音消息的收费是 15 美元。因此,模型代表了产品服务和价格的数据。

然后,我们定义了view类(视图),它提供了一个方法将信息呈现回客户端。这些方法分别是list_services()list_pricing();正如其名所示,一个方法用于打印产品提供的服务,另一个用于列出服务的定价。

然后,我们定义了Controller类,该类定义了两个方法,get_services()get_pricing()。每个方法都查询模型并获取数据。然后,数据被传递到视图,从而呈现给客户端。

Client类实例化控制器。controller对象用于根据客户端的请求调用适当的方法:

class Model(object):
    services = {
                 'email': {'number': 1000, 'price': 2,},
                 'sms': {'number': 1000, 'price': 10,},
                 'voice': {'number': 1000, 'price': 15,},
    }

class View(object):
    def list_services(self, services):
        for svc in services:
            print(svc, ' ')

    def list_pricing(self, services):
        for svc in services:
            print("For" , Model.services[svc]['number'],
                               svc, "message you pay $",
                           Model.services[svc]['price'])

class Controller(object):
    def __init__(self):
        self.model = Model()
        self.view = View()

    def get_services(self):
        services = self.model.services.keys()
        return(self.view.list_services(services))

    def get_pricing(self):
        services = self.model.services.keys()
        return(self.view.list_pricing(services))

class Client(object):
    controller = Controller()
    print("Services Provided:")
    controller.get_services()
    print("Pricing for Services:")
    controller.get_pricing()

以下是为前面代码的输出:

控制器 – 粘合剂

MVC 设计模式的 UML 类图

让我们借助以下 UML 图来更深入地了解 MVC 模式。

正如我们在前面的章节中讨论的那样,MVC 模式有以下主要参与者:Model(模型)、View(视图)和Controller(控制器)类。

MVC 设计模式的 UML 类图

在 UML 图中,我们可以看到这个模式中的三个主要类:

  • Model类:这定义了与客户端某些任务相关的业务逻辑或操作。

  • View类:这定义了客户端看到的视图或表示。模型根据业务逻辑向视图呈现数据。

  • Controller类:这本质上是一个介于视图和模型之间的接口。当客户端采取某些操作时,控制器将视图的查询导向模型。

以下是一个包含所有参与者的代码示例,以了解该模式:

class Model(object):
    def logic(self):
        data = 'Got it!'
        print("Model: Crunching data as per business logic")
        return data

class View(object):
    def update(self, data):
        print("View: Updating the view with results: ", data)

class Controller(object):
    def __init__(self):
        self.model = Model()
        self.view = View()

    def interface(self):
        print("Controller: Relayed the Client asks")
        data = self.model.logic()
        self.view.update(data)

class Client(object):
    print("Client: asks for certain information")
    controller = Controller()
    controller.interface()

以下是为前面代码的输出:

MVC 设计模式的 UML 类图

现实世界中的 MVC 模式

我们那些古老的 Web 应用程序框架基于 MVC 的哲学。以 Django 或 Rails(Ruby)为例:它们以 Model-View-Controller 格式结构化他们的项目,除了它被表示为MTVModelTemplateView)之外,其中模型是数据库,模板是视图,控制器是视图/路由。

例如,让我们以 Tornado Web 应用程序框架(www.tornadoweb.org/en/stable/)为例,开发一个单页应用程序。这个应用程序用于管理用户的任务,并且用户有权限添加任务、更新任务和删除任务。

让我们看看设计考虑因素:

  • 让我们先从控制器开始。在 Tornado 中,控制器已被定义为视图/app 路由。我们需要定义多个视图,例如列出任务、创建新任务、关闭任务以及处理无法提供服务的请求的操作。

  • 我们还应该定义模型,即列出、创建或删除任务的数据库操作。

  • 最后,在 Tornado 中,视图由模板表示。根据我们的应用程序,我们需要一个模板来显示任务、创建或删除任务,以及如果 URL 未找到的模板。

模块

我们将使用以下模块来构建此应用程序:

  • Torando==4.3

  • SQLite3==2.6.0

让我们先在我们的应用程序中导入 Python 模块:

importtornado
import tornado.web
import tornado.ioloop
import tornado.httpserver
import sqlite3

以下代码表示数据库操作,本质上,是 MVC 中的模型。在 Tornado 中,数据库操作是在不同的处理器下执行的。处理器根据用户在 Web 应用中请求的路由在数据库上执行操作。在这里,我们讨论我们在这个示例中创建的四个处理器:

  • IndexHandler:这返回存储在数据库中的所有任务。它返回一个包含键 tasks 的字典。它执行 SELECT 数据库操作以获取这些任务。

  • NewHandler:正如其名所示,这有助于添加新任务。它检查是否有创建新任务的 POST 调用并在数据库中执行 INSERT 操作。

  • UpdateHandler:这在标记任务为完成或重新打开指定任务时很有用。在这种情况下,执行 UPDATE 数据库操作以设置状态为打开/关闭的任务。

  • DeleteHandler:这将从数据库中删除指定的任务。一旦删除,任务将不再在任务列表中可见。

我们还开发了一个 _execute() 方法,该方法接受一个 SQLite 查询作为输入并执行所需的数据库操作。_execute() 方法在 SQLite 数据库上执行以下操作:

  • 创建 SQLite 数据库连接

  • 获取游标对象

  • 使用游标对象进行事务

  • 提交查询

  • 关闭连接

让我们看看 Python 实现中的处理器:

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        query = "select * from task"
        todos = _execute(query)
        self.render('index.html', todos=todos)

class NewHandler(tornado.web.RequestHandler):
    def post(self):
        name = self.get_argument('name', None)
        query = "create table if not exists task (id INTEGER \
            PRIMARY KEY, name TEXT, status NUMERIC) "
        _execute(query)
        query = "insert into task (name, status) \
            values ('%s', %d) " %(name, 1)
        _execute(query)
        self.redirect('/')

    def get(self):
        self.render('new.html')

class UpdateHandler(tornado.web.RequestHandler):
    def get(self, id, status):
        query = "update task set status=%d where \
            id=%s" %(int(status), id)
        _execute(query)
        self.redirect('/')

class DeleteHandler(tornado.web.RequestHandler):
    def get(self, id):
        query = "delete from task where id=%s" % id
        _execute(query)
        self.redirect('/')

如果你查看这些方法,你会注意到一个叫做 self.render() 的东西。这本质上代表了 MVC 中的视图(在 Tornado 框架中的模板)。我们主要有三个模板:

  • index.html:这是一个列出所有任务的模板

  • new.html:这是一个创建新任务的视图

  • base.html:这是一个基础模板,其他模板都是从它继承的

考虑以下代码:

base.html
<html>
<!DOCTYPE>
<html>
<head>
        {% block header %}{% end %}
</head>
<body>
        {% block body %}{% end %}
</body>
</html>

index.html

{% extends 'base.html' %}
<title>ToDo</title>
{% block body %}
<h3>Your Tasks</h3>
<table border="1" >
<tralign="center">
<td>Id</td>
<td>Name</td>
<td>Status</td>
<td>Update</td>
<td>Delete</td>
</tr>
    {% for todo in todos %}
<tralign="center">
<td>{{todo[0]}}</td>
<td>{{todo[1]}}</td>
            {% if todo[2] %}
<td>Open</td>
            {% else %}
<td>Closed</td>
            {% end %}
            {% if todo[2] %}
<td><a href="/todo/update/{{todo[0]}}/0">Close Task</a></td>
            {% else %}
<td><a href="/todo/update/{{todo[0]}}/1">Open Task</a></td>
            {% end %}
<td><a href="/todo/delete/{{todo[0]}}">X</a></td>
</tr>
    {% end %}
</table>

<div>
<h3><a href="/todo/new">Add Task</a></h3>
</div>
{% end %}

new.html

{% extends 'base.html' %}
<title>ToDo</title>
{% block body %}
<div>
<h3>Add Task to your List</h3>
<form action="/todo/new" method="post" id="new">
<p><input type="text" name="name" placeholder="Enter task"/>
<input type="submit" class="submit" value="add" /></p>
</form>
</div>
{% end %}

在 Tornado 中,我们也有 MVC 中的控制器,即应用路由。在这个示例中,我们有四个应用路由:

  • /:这是列出所有任务的路由

  • /todo/new:这是创建新任务的路由

  • /todo/update:这是将任务状态更新为打开/关闭的路由

  • /todo/delete:这是删除已完成任务的路由

代码示例如下:

class RunApp(tornado.web.Application):
    def __init__(self):
        Handlers = [
            (r'/', IndexHandler),
            (r'/todo/new', NewHandler),
            (r'/todo/update/(\d+)/status/(\d+)', UpdateHandler),
            (r'/todo/delete/(\d+)', DeleteHandler),
        ]
        settings = dict(
            debug=True,
            template_path='templates',
            static_path="static",
        )
        tornado.web.Application.__init__(self, Handlers, \
            **settings)

我们还有应用程序设置,并且可以启动 HTTP Web 服务器来运行应用程序:

if__name__ == '__main__':
    http_server = tornado.httpserver.HTTPServer(RunApp())
    http_server.listen(5000)
    tornado.ioloop.IOLoop.instance().start()

当我们运行 Python 程序时:

  1. 服务器已启动并运行在端口5000上。适当的视图、模板和控制器已经配置好。

  2. 在浏览http://localhost:5000/时,我们可以看到任务列表。以下屏幕截图显示了浏览器中的输出:模块

  3. 我们还可以添加一个新任务。一旦你点击添加,就会添加一个新的任务。在下面的屏幕截图中,添加了一个新的任务编写新章节,并在任务列表中列出:模块

    当我们输入新任务并点击 ADD 按钮时,任务会被添加到现有任务列表中:

    模块

  4. 我们还可以从 UI 关闭任务。例如,我们更新了做饭任务,列表也会更新。如果我们选择的话,可以重新打开任务:模块

  5. 我们还可以删除一个任务。在这种情况下,我们删除了第一个任务新任务,任务列表将更新以删除该任务:模块

MVC 模式的优点

以下是一些 MVC 模式的优点:

  • 使用 MVC,开发者可以将软件应用分成三个主要部分:模型、视图和控制器。这有助于实现易于维护、强制松散耦合和降低复杂性。

  • MVC 允许在前端进行独立更改,而无需在后台逻辑上进行任何或很少的更改,因此开发工作仍然可以独立进行。

  • 类似地,模型或业务逻辑可以更改,而不会在视图中进行任何更改。

  • 此外,控制器可以更改,而不会对视图或模型产生影响。

  • MVC 还有助于招聘具有特定能力的人员,如平台工程师和 UI 工程师,他们可以在自己的专业领域独立工作。

常见问题

Q1. MVC 不是一个模式吗?为什么它被称为复合模式?

A: 复合模式本质上是由多个模式组合在一起来解决软件应用开发中的大型设计问题。MVC 模式是最受欢迎和最广泛使用的复合模式。由于其广泛的使用和可靠性,它被视为一个模式本身。

Q2. MVC 只用于网站吗?

A: 不,网站是描述 MVC 的最佳例子。然而,MVC 可以用于多个领域,例如 GUI 应用程序或任何需要以独立方式松散耦合和分割组件的地方。MVC 的典型例子包括博客、电影数据库应用程序和视频流网页应用。虽然 MVC 在许多地方都很实用,但如果用于着陆页、营销内容或快速的单页应用,则可能有些过度。

Q3. 多个视图可以与多个模型一起工作吗?

A: 是的,你经常会遇到需要从多个模型中收集数据并在一个视图中展示的情况。在当今的 Web 应用世界中,一对一映射很少见。

摘要

我们通过理解复合模式开始本章,并探讨了模型-视图-控制器(MVC)模式及其在软件架构中的有效应用。然后我们研究了 MVC 模式是如何被用来确保松散耦合并维护一个用于独立任务开发的分层框架的。

您还学习了与 UML 图和 Python 3.5 示例代码实现一起的解释,该模式。我们还涵盖了一个关于常见问题解答(FAQs)的部分,这将帮助您获得更多关于该模式和其可能的优势/劣势的想法。

在下一章中,我们将讨论反模式。那里见!

第十章。状态设计模式

在本章中,我们将介绍状态设计模式。像命令或模板设计模式一样,状态模式属于行为模式。您将了解状态设计模式,我们将讨论它在软件开发中的应用。我们将使用一个示例用例、一个现实世界场景,并在 Python v3.5 中实现它。

在本章中,我们将简要介绍以下主题:

  • 状态设计模式简介

  • 状态设计模式及其 UML 图

  • 一个带有 Python v3.5 代码实现的现实世界用例

  • 状态模式:优点和缺点

在本章结束时,您将欣赏到状态设计模式的应用和上下文。

定义状态设计模式

行为模式关注对象所具有的职责。它们处理对象之间的交互以实现更大的功能。状态设计模式是一种行为设计模式,有时也被称为对象状态模式。在这个模式中,一个对象可以根据其内部状态封装多个行为。状态模式也被认为是一种对象在运行时改变其行为的方式。

小贴士

在运行时更改行为是 Python 擅长的!

例如,考虑一个简单收音机的案例。收音机有 AM/FM(一个切换开关)频道和一个扫描按钮来扫描多个 FM/AM 频道。当用户打开收音机时,收音机的初始状态已经设置(比如说,设置为 FM)。点击扫描按钮时,收音机会被调谐到多个有效的 FM 频率或频道。当基本状态现在变为 AM 时,扫描按钮帮助用户调谐到多个 AM 频道。因此,基于收音机的初始状态(AM/FM),当调谐到 AM 或 FM 频道时,扫描按钮的行为会动态改变。

因此,状态模式允许对象在内部状态改变时改变其行为。这会给人一种对象本身已经改变了其类的感觉。状态设计模式用于开发有限状态机,并有助于适应状态事务操作。

理解状态设计模式

状态设计模式通过与三个主要参与者合作工作:

  • 状态:这被认为是一个封装对象行为的接口。这种行为与对象的状态相关联。

  • 具体状态:这是一个实现状态接口的子类。具体状态实现了与对象特定状态相关的实际行为。

  • 上下文:这定义了客户端感兴趣的接口。上下文还维护一个具体状态子类的实例,该实例内部定义了对象特定状态的实施。

让我们看看这三个参与者(StateConcreteStateAConcreteStateB)的结构代码实现。在这个代码实现中,我们定义了一个 State 接口,它有一个 Handle() 抽象方法。ConcreteState 类,ConcreteStateAConcreteStateB,实现了 State 接口,因此定义了针对 ConcreteState 类的特定 Handle() 方法。因此,当 Context 类设置一个状态时,该状态的 ConcreteClassHandle() 方法会被调用。在以下示例中,由于 Context 设置为 stateA,因此会调用 ConcreteStateA.Handle() 方法并打印 ConcreteStateA

from abc import abstractmethod, ABCMeta

class State(metaclass=ABCMeta):

    @abstractmethod
    def Handle(self):
        pass

class ConcreteStateB(State):
    def Handle(self):
        print("ConcreteStateB")

class ConcreteStateA(State):
    def Handle(self):
        print("ConcreteStateA")

class Context(State):

    def __init__(self):
        self.state = None

    def getState(self):
        return self.state

    def setState(self, state):
        self.state = state

    def Handle(self):
        self.state.Handle()

context = Context()
stateA = ConcreteStateA()
stateB = ConcreteStateB()

context.setState(stateA)
context.Handle()

我们将看到以下输出:

理解状态设计模式

使用 UML 图解理解状态设计模式

如前节所述,UML 图中有三个主要参与者:StateConcreteStateContext。在本节中,我们将尝试在一个 UML 类图中体现它们。

使用 UML 图解理解状态设计模式

让我们详细理解 UML 图的元素:

  • State:这是一个定义 Handle() 抽象方法的接口。Handle() 方法需要由 ConcreteState 实现。

  • ConcreteState:在这个 UML 图中,我们定义了两个 ConcreteClassesConcreteStateAConcreteStateB。这些实现了 Handle() 方法,并定义了基于 State 变化的实际操作。

  • Context:这是一个接受客户端请求的类。它还维护对对象当前状态的引用。根据请求,调用具体行为。

状态设计模式的一个简单示例

让我们通过一个简单示例来理解所有三个参与者。比如说,我们想要实现一个带有简单按钮的电视遥控器来执行开关操作。如果电视开启,遥控器按钮将关闭电视,反之亦然。在这种情况下,State 接口将定义一个方法(比如 doThis())来执行开关电视等操作。我们还需要为不同的状态定义 ConcreteClass。在这个例子中,有两个主要状态,StartStateStopState,分别表示电视开启和关闭的状态。

对于这个场景,TVContext 类将实现 State 接口并保持对当前状态的引用。根据请求,TVContext 将请求转发给 ConcreteState,它实现了实际行为(对于给定状态)并执行必要的操作。因此,在这种情况下,基本状态是 StartState(如前所述),而 TVContext 类接收到的请求是关闭电视。TVContext 类理解了需求,并相应地将请求转发给 StopState 具体类,该类进而调用 doThis() 方法来实际关闭电视:

from abc import abstractmethod, ABCMeta

class State(metaclass=ABCMeta):

    @abstractmethod
    def doThis(self):
        pass

class StartState (State):
    def doThis(self):
        print("TV Switching ON..")

class StopState (State):
    def doThis(self):
        print("TV Switching OFF..")

class TVContext(State):

    def __init__(self):
        self.state = None

    def getState(self):
        return self.state

    def setState(self, state):
        self.state = state

    def doThis(self):
        self.state.doThis()

context = TVContext()
context.getState()
start = StartState()
stop = StopState()

context.setState(stop)
context.doThis()

以下是前面代码的输出:

状态设计模式的一个简单示例

带有 v3.5 实现的 State 设计模式

现在,让我们看看状态设计模式在现实世界中的一个实际应用案例。想象一个计算机系统(台式机/笔记本电脑)。它可以有多个状态,例如OnOffSuspendHibernate。现在,如果我们想借助状态设计模式来体现这些状态,我们将如何操作?

假设,我们从ComputerState接口开始:

  • 状态应该定义两个属性,即nameallowedname属性代表对象的状态,而allowed是一个列表,定义了状态可以进入的对象状态。

  • 状态必须定义一个switch()方法,这个方法实际上会改变对象的状态(在这个例子中,是电脑)。

让我们看看ComputerState接口的代码实现:

class ComputerState(object):
    name = "state"
    allowed = []

    def switch(self, state):
        if state.name in self.allowed:
            print('Current:',self,' => switched to new state',state.name)
            self.__class__ = state
        else:
            print('Current:',self,' => switching to',state.name,'not possible.')

    def __str__(self):
        return self.name

现在,让我们看看实现State接口的ConcreteState。我们将定义四个状态:

  • On:这个操作将电脑开启。这里允许的状态是OffSuspendHibernate

  • Off:这个操作将电脑关闭。这里允许的状态仅仅是On

  • Hibernate:这个状态将电脑置于休眠模式。电脑只有在处于这个状态下才能被开启。

  • Suspend:这个状态将电脑挂起,一旦电脑被挂起,就可以将其开启。

现在,让我们看看代码:

class Off(ComputerState):
    name = "off"
    allowed = ['on']

class On(ComputerState):
    name = "on"
    allowed = ['off','suspend','hibernate']

class Suspend(ComputerState):
    name = "suspend"
    allowed = ['on']

class Hibernate(ComputerState):
    name = "hibernate"
    allowed = ['on']

现在,我们探索上下文类(Computer)。上下文执行两个主要操作:

  • __init__(): 这个方法定义了电脑的基态

  • change():这个方法将改变对象的状态,实际的行为改变由ConcreteState类(onoffsuspendhibernate)实现

下面是前面方法的实现:

class Computer(object):
    def __init__(self, model='HP'):
        self.model = model
        self.state = Off()

    def change(self, state):
        self.state.switch(state)

下面是客户端的代码。我们创建Computer类(上下文)的对象,并将一个状态传递给它。状态可以是以下之一:OnOffSuspendHibernate。根据新的状态,上下文调用其change(state)方法,最终切换电脑的实际状态:

if __name__ == "__main__":
    comp = Computer()
    # Switch on
    comp.change(On)
    # Switch off
    comp.change(Off)

    # Switch on again
    comp.change(On)
    # Suspend
    comp.change(Suspend)
    # Try to hibernate - cannot!
    comp.change(Hibernate)
    # switch on back
    comp.change(On)
    # Finally off
    comp.change(Off)

现在,我们可以观察到以下输出:

带有 v3.5 实现的 State 设计模式

__class__是每个类的内置属性。它是对类的引用。例如,self.__class__.__name__代表类的名称。在这个例子中,我们使用 Python 的__class__属性来改变State。因此,当我们向change()方法传递状态时,对象的类在运行时会被动态地改变。comp.change(On)代码将对象状态更改为On,随后更改为不同的状态,如SuspendHibernateOff

状态模式的优缺点

这里是状态设计模式的优点:

  • 在状态设计模式中,对象的行为是其状态的函数结果,并且行为会根据状态在运行时改变。这消除了对 if/else 或 switch/case 条件逻辑的依赖。例如,在电视遥控器场景中,我们也可以通过简单地编写一个类和方法来实现行为,该方法将请求一个参数并执行一个动作(打开/关闭电视),使用if/else块。

  • 使用状态模式,实现多态行为的优势显而易见,并且添加状态以支持额外行为也更加容易。

  • 状态设计模式还通过将特定状态的行为聚合到ConcreteState类中,这些类被放置在代码的一个位置,从而提高了内聚性

  • 使用状态设计模式,通过添加一个额外的ConcreteState类,就可以很容易地添加一个行为。因此,状态模式提高了扩展应用行为的能力,并总体上提高了代码的可维护性。

我们已经看到了状态模式的优点。然而,它们也有一些陷阱:

  • 类爆炸:由于每个状态都需要通过ConcreteState来定义,所以我们可能会写出很多功能较小的类。考虑有限状态机的例子——如果有很多状态,但每个状态与其他状态没有太大区别,我们仍然需要将它们作为单独的ConcreteState类来编写。这增加了我们需要编写的代码量,并且使得审查状态机的结构变得困难。

  • 每引入一个新的行为(尽管添加行为只是添加一个额外的ConcreteState),Context类都需要更新以处理每个行为。这使得每次添加新行为时Context的行为都变得更加脆弱。

摘要

总结到目前为止我们所学的,在状态设计模式中,对象的行为是基于其状态决定的。对象的状态可以在运行时改变。Python 在运行时改变行为的能力使得应用和实现状态设计模式变得非常容易。状态模式还让我们能够控制对象可以采取的状态,例如我们在本章前面看到的计算机示例中的状态。Context类提供了一个更简单的接口,而ConcreteState确保了向对象添加行为变得容易。因此,状态模式提高了内聚性、扩展灵活性,并消除了冗余代码块。我们以 UML 图的形式学术性地研究了该模式,并借助 Python 3.5 代码实现学习了状态模式的实现方面。我们还审视了在使用状态模式时可能会遇到的几个陷阱,以及当添加更多状态或行为时代码可能会显著增加的情况。我希望你在阅读这一章时过得愉快!

第十一章。反模式

在上一章中,我们首先介绍了复合模式。你学习了设计模式如何协同工作来解决现实世界的实际问题。我们进一步探讨了模型-视图-控制器设计模式——复合模式之王。我们了解到,当需要组件之间的松散耦合以及数据存储方式与数据展示方式的分离时,会使用 MVC 模式。我们还研究了 MVC 模式的 UML 图,并了解了各个组件(模型、视图和控制器)之间是如何相互工作的。我们还看到了它是如何通过 Python 实现应用于现实世界的。我们讨论了 MVC 模式的好处,在 FAQ 部分对其进行了更多了解,并在章节末尾总结了讨论。

在本章中,我们将讨论反模式。这与本书中的其他所有章节都不同;在这里,我们将讨论作为架构师或软件工程师我们不应该做什么。我们将通过理论和实践示例来理解反模式是什么以及它们如何在软件设计或开发方面体现出来。

简而言之,在本章中我们将涵盖以下主题:

  • 反模式的介绍

  • 带有示例的反模式

  • 开发过程中的常见陷阱

本章结束时,我们将总结整个讨论——请将此视为一个要点。

反模式的介绍

软件设计原则代表了一套规则或指南,帮助软件开发者在设计层面做出决策。根据罗伯特·马丁的观点,糟糕的设计有四个方面:

  • 不可移动性:应用程序以这种方式开发,使其变得非常难以重用

  • 僵化性:应用程序以这种方式开发,任何小的变化都可能反过来导致软件的许多部分移动

  • 脆弱性:当前应用程序的任何变化都可能导致现有系统相对容易地崩溃

  • 粘性:开发者通过在代码或环境中本身进行更改来避免困难的架构级别更改

如果应用上述糟糕设计的方面,会导致在软件架构或开发中不应实施的解决方案。

反模式是针对重复性问题解决方案的结果,其结果是无效的,并变得适得其反。这意味着什么?假设你遇到一个软件设计问题。你着手解决这个问题。然而,如果解决方案对设计有负面影响或导致应用程序中任何性能问题,又会怎样呢?因此,反模式是软件应用程序中常见的缺陷过程和实现。

反模式可能是以下情况的结果:

  • 开发者不了解软件开发实践

  • 开发者没有在正确上下文中应用设计模式

反模式可以证明是有益的,因为它们提供了以下机会:

  • 认识到软件行业中的重复性问题,并为其中大多数问题提供详细的解决方案

  • 开发工具以识别这些问题并确定潜在原因

  • 描述可以在多个级别上采取的改进应用程序和架构的措施

反模式可以分为两大类:

  1. 软件开发反模式

  2. 软件架构反模式

软件开发反模式

当你开始为应用程序或项目进行软件开发时,你会考虑代码结构。这个结构与产品架构、设计、客户用例以及许多其他开发考虑因素保持一致。

通常,在软件开发过程中,由于以下原因,软件可能会偏离原始代码结构:

  • 开发者的思维过程随着开发而发展

  • 用例往往会根据客户反馈而变化

  • 初始设计的数据结构可能会随着功能或可扩展性的考虑而发生变化

由于上述原因,软件通常需要进行重构。重构在很多情况下被赋予了负面含义,但事实上,重构是软件开发旅程中的关键部分之一,它为开发者提供了重新审视数据结构和思考可扩展性和不断变化的客户需求的机会。

以下示例为您提供了在软件开发和架构中观察到的不同反模式的概述。我们将仅涵盖其中的一些,包括原因、症状和后果。

意大利面代码

这是软件开发中最常见且最常听到的反模式。你知道意大利面是什么样的吗?如此复杂,不是吗?如果结构是临时开发的,软件的控制流也会变得混乱。意大利面代码难以维护和优化。

意大利面的典型原因包括以下:

  • 对面向对象编程和分析的无知

  • 未考虑的产品架构或设计

  • 快速修复的心态

当以下几点成立时,你知道你陷入了意大利面困境:

  • 结构的最小重用是可能的

  • 维护工作过于繁重

  • 扩展性和灵活性降低

黄金锤

在软件行业,你可能会看到许多例子,其中某个解决方案(技术、设计或模块)被用于许多地方,因为该解决方案在多个项目中都产生了效益。正如我们在本书中的例子所看到的那样,一个解决方案最适合在特定环境中使用,并应用于某些类型的问题。然而,团队或软件开发者往往会选择一个经过验证的解决方案,而不考虑它是否适合需求。这就是为什么它被称为“黄金锤”,一把适用于所有可能的钉子(解决所有问题的方案)。

黄金锤的典型原因包括以下:

  • 这是一种来自高层(架构师或技术领导者)的建议,他们并不接近当前的问题

  • 一个解决方案在过去带来了很多好处,但在具有不同上下文和需求的项目中

  • 一家公司因为投入了资金培训员工或员工对这种技术感到舒适而陷入这种技术

金刚钻效应的后果如下:

  • 一种解决方案被强迫应用于所有软件项目

  • 产品不是通过特性来描述,而是通过开发中使用的科技来描述

  • 在公司的走廊里,你会听到开发者们谈论,“那可能比使用这个更好。”

  • 需求未完成且与用户期望不一致

洪流效应

这种反模式与死代码相关,或者是一段无法使用的代码,由于担心修改后可能会破坏其他部分,所以保留在软件应用中。随着时间的推移,这段代码继续留在软件中,并像熔岩变成硬岩一样固化其位置。这种情况可能发生在你开始开发软件以支持某个用例,但随着时间的推移,该用例本身发生变化的情况下。

洪流效应的原因包括以下几方面:

  • 生产环境中大量的试错代码

  • 单独编写的代码未经审查,且在没有培训的情况下转交给其他开发团队

  • 软件架构或设计的初步想法已经体现在代码库中,但现在已经没有人理解它了

洪流效应的症状如下:

  • 低代码覆盖率(如果有的话)对于已开发的测试

  • 许多没有原因的注释代码

  • 已废弃的接口,或者开发者试图绕过现有代码

复制粘贴或剪切粘贴编程

如你所知,这是最常见的反模式之一。经验丰富的开发者将他们的代码片段发布到网上(GitHub 或 Stack Overflow),这些代码片段是解决一些常见问题的解决方案。开发者经常直接复制这些片段并用于他们的应用程序,以推动应用程序的开发。在这种情况下,没有验证这是最优化代码,甚至没有验证代码实际上适合上下文。这导致软件应用缺乏灵活性,难以维护。

复制粘贴或剪切粘贴的原因如下:

  • 新手开发者不习惯编写代码或不知道如何开发

  • 快速修复错误或继续开发

  • 由于需要代码结构或模块间的标准化,而导致的代码重复

  • 缺乏长期思考或前瞻性

剪贴板或复制粘贴的后果包括以下几方面:

  • 软件应用中类似类型的问题

  • 维护成本增加和错误生命周期延长

  • 代码库模块化程度低,相同的代码行数众多

  • 继承了最初存在的问题

软件架构反模式

软件架构是整体系统架构的重要组成部分。虽然系统架构侧重于设计、工具和硬件等方面,但软件架构关注的是建模软件,这种软件被开发团队、测试团队、产品经理和其他利益相关者所理解。这种架构在确定实施的成功以及产品如何为顾客工作方面发挥着关键作用。

我们将讨论我们在现实世界中观察到的某些架构级反模式,这些反模式与软件开发和实施架构有关。

重新发明轮子

我们经常听到技术领导者谈论“不要重新发明轮子”。这本质上意味着什么?对一些人来说,这可能意味着代码或库的重用。实际上,它指的是架构的重用。例如,你已经解决了一个问题,并提出了一个架构级的解决方案。如果你在其他任何应用程序中遇到类似的问题,之前开发的思想过程(架构或设计)应该被重用。重新审视相同的问题并制定解决方案是没有意义的,这本质上就是重新发明轮子。

导致重新发明轮子的原因如下:

  • 缺乏关于架构级问题和解决方案的中央文档或存储库

  • 社区或公司内部技术领导者之间的沟通不足

  • 从零开始构建是组织遵循的过程;基本上,不成熟的过程和松散的过程实施及遵守

这种反模式的后果包括以下内容:

  • 解决一个标准问题有太多解决方案,其中许多没有经过深思熟虑

  • 工程团队花费更多的时间和资源,导致超预算和更长的上市时间

  • 封闭的系统架构(仅适用于单一产品的架构)、重复劳动和风险管理不佳

供应商锁定

如反模式名称所示,产品公司往往依赖于供应商提供的一些技术。这些技术与其系统紧密相连,以至于很难摆脱。

以下是供应商锁定的原因:

  • 与供应商公司中的权威人士熟悉,以及可能在技术采购中享受折扣

  • 根据营销和销售方案而不是技术评估来选择技术

  • 即使当前项目不适合项目需求或要求,也在当前项目中使用经过验证的技术(经过验证表示在以前的经验中,使用这项技术的投资回报率非常高)

  • 技术人员/开发者已经接受了使用这项技术的培训

供应商锁定带来的后果如下:

  • 公司产品发布的发布周期和维护周期直接依赖于供应商的发布时间表

  • 产品是围绕技术而非客户需求开发的

  • 产品的上市时间不可靠,不符合客户的期望

委员会设计

有时,根据组织中的流程,一群人坐在一起设计特定的系统。由此产生的软件架构通常很复杂或不符合标准,因为它涉及太多的思维过程,而且可能没有正确技能集或设计产品经验的技术人员提出了想法。

委员会设计的原因如下:

  • 组织中的流程涉及让架构或设计得到许多利益相关者的批准

  • 没有单一的联系点或负责设计的架构师

  • 由市场营销人员或技术人员而非客户反馈设定的设计优先级

观察到与此反模式相关的症状包括以下:

  • 即使设计已经确定,开发人员和架构师之间也存在冲突的观点

  • 过于复杂的设计,很难进行文档记录

  • 任何对规格或设计的变更都需要经过多人的审查,从而导致实施延迟

摘要

总结本章内容,你学习了反模式(AntiPatterns)的定义、分类,以及它们与软件开发或软件架构的关系。我们探讨了常见反模式及其成因、症状和后果。我相信你会从中学到东西,并在你的项目中避免类似情况的发生。

就这样,朋友们,这是本书的最后一章。希望你喜欢它,并且这本书能帮助你提高技能。祝你们所有人好运!

posted @ 2025-09-20 21:35  绝不原创的飞龙  阅读(33)  评论(0)    收藏  举报