当软件应用开发项目开始时,它本质上被认为是一个需要解决的问题。当我们开始开发应用时,我们开始开发特定于给定问题的解决方案。最终,此解决方案可能会在类似问题中重新使用,并成为解决此类问题的标准解决方案。随着时间的推移,我们看到许多问题显示出相同的模式。一旦我们修改了我们的标准解决方案来处理这个观察到的模式,我们就会提出一个设计模式。设计模式不是玩笑;他们花了数年的时间来生产,经过尝试和测试,以解决大量类似模式的问题。
设计模式不仅定义了我们构建软件应用的方式,还提供了关于什么有效什么无效的知识,同时尝试解决特定类型的问题。有时,没有特定的设计模式可以满足特定应用的需求,开发人员别无选择,只能想出一些独特的东西。
是否有一些现有的标准设计模式可用于特定类型的问题?我们如何决定使用哪种设计模式解决我们的问题?我们能否偏离特定的设计模式,并在开发解决方案时使用它们?在本章中,我们将尝试回答这些问题。
在本章结束时,您将了解以下内容:
- 设计模式及其分类
- Python 的面向对象特性,以及我们如何使用它来实现一些常见的设计模式
- 可能使用特定模式的用例
可以通过运行以下命令克隆代码示例:
git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python
运行代码示例的说明可以在章节目录中的README.md
文件下找到。
设计模式定义了一种我们可以组织解决给定问题的方法。它没有定义可用于解决问题的算法,而是提供了一个抽象概念,例如,应该如何组织代码,需要定义哪些类,它们的粒度是什么,以及如何创建不同的对象。
设计模式已经获得了很大的吸引力,1994 年出版的《设计模式:可重用面向对象软件的元素》一书在试图理解设计模式时仍然是事实上的参考。
设计模式通常由以下元素组成:
- 问题陈述:问题陈述描述了我们想要解决的问题,因此也定义了我们可以使用的设计模式。问题陈述将告诉我们计划进行的设计范围、可能需要注意的约束,以及不同组件在应用中如何相互通信。
- 解决方案:解决方案描述了弥补问题的设计。它详细介绍了类层次结构应该如何形成,对象将如何形成,对象之间的关系,以及不同组件之间如何进行通信。解决方案将是一个抽象设计,而不是指定实现的细节。这使得解决方案具有通用性,可以应用于一类问题,而不需要考虑应该使用什么算法来解决特定问题。
- 后果:在软件开发的世界里,没有什么是免费的。一切都有代价,我们用一件东西换另一件。重要的是权衡是否合理。同样的道理也适用于设计模式的选择,这些模式都有其自身的后果。大多数情况下,这些后果是在空间和时间权衡方面产生的,如果特定的设计选择不能证明权衡的成本是合理的,则这些后果将成为评估备选方案的重要组成部分。有时,其结果还可能定义语言的实现障碍,并且常常会影响应用的可重用性和灵活性。
设计模式的选择并不是每一组问题的共同点。使用什么模式来解决问题将取决于几个因素,例如开发人员对问题的解释、对需要使用的编程语言的任何限制、与项目相关的截止日期等等。
在设计模式:可重用面向对象软件的元素一书中,设计模式被分为三大类:
- 创建模式:这些模式定义了如何创建对象,从而使您的代码独立于存在的对象,从而将其与新对象引入代码库时可能产生的影响分离。这需要将对象创建逻辑与代码库隔离。这些模式,例如 Singleton 和 Factory,属于创造性模式的范畴。
- 结构模式:与处理对象如何创建的创作模式不同,结构模式通常用于描述。。。
在选择设计模式时,我们可能需要设计模式应该满足的一组特定特征。让我们来看看如果我们使用 Python 来实现我们的设计模式,这些特性可能包括什么:
- 最小惊奇原则:Python 的禅宗说应该遵循最小惊奇原则。这意味着所使用的设计模式不应该在预期显示的行为方面让用户感到惊讶。
- 减少耦合:耦合定义为软件内部不同组件相互依赖的程度。具有高度耦合的软件可能很难维护,因为对一个组件的更改可能需要对许多其他组件进行更改。耦合作为一种影响不能从软件中完全消除,但应选择设计模式,以便在开发过程中将耦合程度降至最低。
- 注重简单:开始开发一个设计原则过于笼统的软件可能弊大于利。它可能会在代码库中引入许多不需要的功能,这些功能很少使用或根本没有使用。设计模式的选择应该更多地关注为所述问题提供简单的解决方案,而不是关注特定设计模式可以解决多少常见类型的问题。
- 避免重复:良好的设计模式选择将有助于开发人员避免重复代码逻辑,并将其保存在一个地方,系统的不同组件可以从那里访问它。减少逻辑的重复不仅可以节省开发时间,还可以简化维护过程,其中逻辑的更改只需要在单个点进行,而不需要在代码库的多个部分进行。
面向对象编程(OOP)是指代码的组织形式,我们不关心方法的组织,而是关心对象、它们的属性和行为。
对象可以表示任何逻辑实体,例如动物、车辆和家具,并将包含描述它们的属性和行为。
基于 OOP 的语言的基本构造块是类,该类通常将逻辑相关的实体组合到一个单元中。当我们需要使用这个单元时,我们创建这个单元的一个新实例,称为 class 对象,并使用对象公开的公共接口来操作这个对象。
Python 中的面向对象编程。。。
一种语言不能仅仅因为支持类和对象而被视为面向对象的语言。该语言还需要支持一组不同的功能,如封装、多态性、组合和继承,才能被视为面向对象的语言。Python 在这方面支持许多基于 OOP 的概念,但由于其松散类型的特性,它的支持方式有所不同。让我们看看 Python 中这些特性是如何不同的。
封装是一个术语,用于指类仅通过对象公开的公共接口限制对其成员的访问的能力。封装的概念有助于我们处理关于我们想对对象做什么的细节,而不是关于对象如何处理内部更改的细节。
在 Python 中,封装不是严格强制的,因为我们不支持访问修饰符,例如 private、public 和 protected,这些修饰符可用于严格控制对类内特定成员的访问。
然而,Python 确实支持通过名称修改来封装,可以使用名称修改来限制对特定属性的直接访问。。。
合成是用于表示不同对象之间关系的属性。这种关系在合成中的表达方式是将一个对象作为另一个对象的属性。
Python 支持组合的概念,它允许程序员构建对象,然后这些对象可以成为其他对象的一部分。例如,让我们看看下面的代码片段:
class MessageHandler:
__message_type = ['Error', 'Information', 'Warning', 'Debug']
def __init__(self, date_format):
self.date_format = date_format
def new_message(message, message_code, message_type='Information'):
if message_type not in self.__message_type:
raise Exception("Unable to handle the message type")
msg = "[{}] {}: {}".format(message_type, message_code, message)
return msg
class WatchDog:
def __init__(self, message_handler, debug=False):
self.message_handler = message_handler
self.debug = debug
def new_message(message, message_code, message_type):
try:
msg = self.message_handler.new_message(message, message_code, message_type)
except Exception:
print("Unable to handle the message type")
return msg
message_handler = MessageHandler('%Y-%m-%d')
watchdog = WatchDog(message_handler)
正如我们从示例中看到的,我们将message_handler
对象作为watchdog
对象的属性。这标志着我们可以用 Python 实现合成的方法之一。
继承是我们在对象中创建层次结构的一种方式,从最一般到最具体。通常构成另一个类的基类也称为基类,而从基类继承的类称为子类。例如,如果一个类B
派生自类A
,那么我们会说类B
是类A
的子类。
与 C++一样,Python 支持多继承和多级继承的概念,但不支持在 C++支持的类继承时使用访问修饰符。
让我们来看看 Python 如何通过在我们的 BugZOT 应用中建模一个新的请求将如何实现继承。下面的代码片段给出了。。。
让我们看一个如何在 Python 中实现多重继承的抽象示例,如下面的代码片段中所见:
class A:
def __init__(self):
print("Class A")
class B:
def __init__(self):
print("Class B")
class C(A,B):
def __init__(self):
print("Class C")
该示例展示了如何在 Python 中实现多重继承。这里有一件有趣的事情,就是理解当我们使用多重继承时,Python 中的方法解析顺序是如何工作的。那么,让我们来看一看。
那么,根据前面的例子,如果我们创建一个C
类的对象,会发生什么呢?
>>> Cobj = C()Class C
正如我们所看到的,这里只调用了派生类构造函数。那么,如果我们也想调用父类构造函数呢?为此,我们需要类C
构造函数中的super()
调用的帮助。为了让它发挥作用,让我们稍微修改一下C
的实现:
>>> class C(A,B):... def __init__(self):... print("C")... super().__init__()>>> Cobj = C()CA
一旦我们创建了派生类的对象,我们就可以看到派生类构造函数首先被调用,然后是第一个继承类的构造函数。super()
自动呼叫。。。
mixin 是一个存在于每种面向对象语言中的概念,可用于实现可在代码的不同位置反复重用的对象类。像 Django web framework 这样的项目提供了大量预构建的混入,可用于在我们为应用实现的自定义类中实现特定的功能集(例如,对象操作、表单呈现等)。
那么,混音是语言的一些特殊特征吗?答案是否定的,它们不是一些特殊的特性,而是一些小类,它们不是为了变成独立的对象而构建的。相反,它们的构建是为了通过支持多重继承为类提供一些特定的额外功能。
回到我们的示例应用 BugZot,我们需要一种以 JSON 格式从多个对象返回数据的方法。现在,我们有两个选择;我们可以在单个方法级别构建返回 JSON 数据的功能,也可以构建可以在多个类中反复重用的 mixin:
Import json
class JSONMixin:
def return_json(self, data):
try:
json_data = json.dumps(data)
except TypeError:
print("Unable to parse the data into JSON")
return json_data
现在,让我们想象一下,如果我们想要我们在试图理解继承的同时在示例中实现的 bug 类。我们需要做的只是继承Bug
类中的JSONMixin
:
class Bug(Request, JSONMixin):
…
而且,通过简单地继承类,我们获得了所需的功能。
在 OOP 中,抽象基类是那些只包含方法声明而不包含其实现的类。这些类不应该有独立的对象,而是作为基类构建的。从抽象基类派生的类需要为抽象类中声明的方法提供实现。
在 Python 中,虽然您可以通过不为声明的方法提供实现来构建抽象类,但语言本身并不强制执行派生类的概念来为方法提供实现。因此,如果在 Python 中执行,以下示例将运行得非常好:
class AbstractUser: def return_data(self): passclass ...
Python 提供了很多特性,其中一些特性对我们来说是直接可见的,比如列表理解、动态类型求值等等,而其中一些特性则不是那么直接。在 Python 中,很多事情都可以被认为是魔术,发生在幕后。其中之一是元类的概念。
在 Python 中,一切都是对象,无论是方法还是类。即使在 Python 内部,类也被认为是第一类对象,可以传递给方法,分配给变量,等等。
但是,正如 OOP 的概念所述,每个对象都表示一个类的实例。所以,如果我们的类是对象,那么它们也应该是某个类的实例。那么,那是哪一节课?这个问题的答案是type
课程。Python 中的每个类都是type
类的实例。
这很容易验证,如以下代码段所示:
class A:
def __init__(self):
print("Hello there from class A")
>>>isinstance(A, type)
True
这些对象为类的类称为元类。
在 Python 中,我们通常不直接使用元类,因为大多数时候,我们试图借助元类解决的问题通常可以通过使用其他一些简单的解决方案来解决。但是元类确实为我们提供了很多创建类的能力。让我们首先看看如何通过设计一个LoggerMeta
类来创建我们自己的元类,该类将强制实例类为前缀为HANDLER_
的不同日志方法提供有效的处理程序方法:
class LoggerMeta(type):
def __init__(cls, name, base, dct):
for k in dct.keys():
if k.startswith('HANDLER_'):
if not callable(dct[k]):
raise AttributeError("{} is not callable".format(k))
super().__init__(name, base, dct)
def error_handler():
print("error")
def warning_handler():
print("warning")
class Log(metaclass=LoggerMeta):
HANDLER_ERROR = error_handler
HANDLER_WARN = warning_handler
HANDLER_INFO = 'info_handler'
def __init__(self):
print(“Logger class”)
在本例中,我们通过继承类型类定义了一个名为LoggerMeta
的metaclass
。(为了定义任何元类,我们需要从类型类或任何其他元类继承。继承的概念甚至在metaclass
创建期间也适用。)一旦我们声明了我们的metaclass
,我们将在metaclass
中为__init__
魔术方法提供定义。元类的__init__
魔术方法接收类对象、要创建的新类的名称、新类将从中派生的基类列表以及包含用于初始化新类的新类属性的字典。
在__init__
方法内部,我们提供了一个实现,用于验证名称以HANDLER_
开头的类属性是否分配了有效的处理程序。如果分配给属性的处理程序不可调用,我们将引发一个AttributeError
并阻止类的创建。在__init__
方法的末尾,我们返回基类__init__
方法的调用结果。
在下一个示例中,我们创建了两个简单的方法,作为处理错误类型消息和警告类型消息的处理程序。
在本例中,我们定义了一个元类为LoggerMeta
的类日志。该类包含一些属性,例如HANDLER_ERROR
、HANDLER_WARN
、HANDLER_INFO
和魔法方法__init__
。
现在,让我们看看如果我们尝试执行提供的示例会发生什么:
python3 metaclass_example.py
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __init__
AttributeError: HANDLER_INFO is not callable
从输出中可以看到,解释器解析类日志的定义创建类后,调用元类__init__
方法,验证类的属性并引发AttributeError
。
Python 中的元类为我们提供了很多可供使用的功能,并使我们能够神奇地完成很多事情,例如,根据方法的名称生成类属性,并跟踪一个类的多少实例已经初始化。
我们已经学习了 Python 中的 OOP 和元类,现在让我们继续使用它们来实现 Python 中的一些设计模式,并学习如何选择要使用的设计模式。
Singleton 模式是Gang of Four在书中找到自己位置的模式之一,它可以有多种用途,我们只希望一个类在整个应用中有一个实例。
Singleton 模式强制一个类只有一个实例,该实例将由应用中的任何组件/模块使用。当我们希望只使用一个对象来控制对资源的访问时,这种强制执行非常有用。这些类型的资源可以是日志文件、数据库、崩溃处理机制等等。
在大多数基于 OOP 的语言中,要实现单例模式,第一步是使类构造函数私有,然后在类内使用静态方法。。。
__call__
magic 方法在 Python 元类上下文中是特殊的。与从元类创建新类时调用的__init__
方法不同,创建初始化类的对象时调用__call__
方法。为了更好地理解这一点,让我们尝试运行以下示例:
class ExampleMeta(type):
def __init__(cls, name, bases, dct):
print("__init__ called")
return super().__init__(name, bases, dct)
def __call__(cls, *args, **kwargs):
print("__call__ called")
return super().__call__(*args, **kwargs)
class Example(metaclass=Example):
def __init__(self):
print("Example class")
__init__ called
>>> obj = Example()
__call__ called
从这个例子可以清楚地看出,一旦解释器完成了基于metaclass
的类的初始化,就会调用__init__
方法,而在创建类的对象时就会调用__call__
方法。
现在,有了这个理解,让我们构建我们的数据库连接类,它将提供数据库操作。在本例中,我们将只关注类的初始化部分,同时在后面的章节中提供完整的类实现细节。
现在,在bugzot
目录下,我们创建一个名为database.py
的文件,它将保存我们的数据库类:
from bugzot.meta import Singleton
class Database(metaclass=Singleton):
def __init__(self, hostname, port, username, password, dbname, **kwargs):
"""Initialize the databases
Initializes the database class, establishing a connection with the database and providing
the functionality to call the database.
:params hostname: The hostname on which the database server runs
:parms port: The port on which database is listening
:params username: The username to connect to database
:params password: The password to connect to the database
:params dbname: The name of the database to connect to
"""
self.uri = build_uri(hostname, port, username, password, dbname)
#self.db = connect_db()
self.db_opts = kwargs
#self.set_db_opts()
def connect_db(self):
"""Establish a connection with the database."""
pass
def set_db_opts(self):
"""Setup the database connection options."""
pass
在本例中,我们定义了数据库类,它将帮助我们建立到数据库的连接。这个类的不同之处在于,每当我们尝试创建这个类的新实例时,它总是返回相同的对象。例如,让我们试着看看如果我们创建同一类的两个不同对象会发生什么:
dbobj1 = Database("example.com", 5432, "joe", "changeme", "testdb")
dbobj2 = Database("example.com", 5432, "joe", "changeme", "testdb")
>>> dbobj1
<__main__.Database object at 0x7fb6d754a7b8>
>>> dbobj2
<__main__.Database object at 0x7fb6d754a7b8>
在本例中,我们可以看到,当我们试图实例化该类的新对象时,返回了数据库对象的同一个实例。
现在,让我们来看看另一个有趣的模式,称为 Po.T0.工厂 AutoT1 模式。
在开发大型应用的过程中,在某些情况下,我们可能需要根据用户输入或其他动态因素动态初始化类。为了实现这一点,我们可以在类实例化期间初始化所有可能的对象,并根据来自环境的输入返回所需的对象,或者我们可以完全推迟类对象的创建,直到收到输入为止。
工厂模式是后一种情况的解决方案,在这种情况下,我们在类中开发了一个特殊的方法,该方法将负责根据环境的输入动态初始化对象。
现在,让我们看看如何在 Python 中实现工厂模式。。。
让我们从一个图表开始讨论 MVC 模式:
该图显示了使用 MVC 模式的应用中的请求流。当用户发出新的请求时,应用截取请求,然后将请求转发给相应的控制器处理该请求。一旦控制器接收到请求后,它就会与模型交互,模型会根据它接收到的请求执行一些业务逻辑。这可能涉及数据库更新或获取一些数据。一旦模型执行了业务逻辑,控制器使用需要传递给视图的任何数据执行视图,然后显示请求的响应。
虽然我们稍后将在书中实现 MVC 模式,但是当我们开发了 BugZOT 应用时,让我们看看 MVC 模式中的不同组件,以及它们扮演的角色。
控制器充当模型和视图之间的中介。当首次向应用发出请求时,控制器会截获该请求,并根据该请求决定需要调用哪个模型和视图。一旦决定了这一点,控制器就会执行模型来运行业务逻辑,从模型中检索数据。检索数据并完成模型执行后,控制器将使用从模型收集的数据执行视图。视图执行完成后,用户将看到来自视图的响应。
简而言之,控制器负责执行以下操作:
- 正在拦截对应用的请求,并执行所需的。。。
模型是应用的业务逻辑所在的位置。很多时候,开发人员会将模型与数据库混淆,这对于某些 web 应用可能是正确的,但如果一般考虑的话,则不是这样。
模型的作用是处理数据,提供对数据的访问,并允许根据请求进行修改。这包括从数据库或文件系统检索数据、向其中添加新数据以及在需要更新时修改现有数据。
模型不关心存储的数据应如何呈现给用户或应用的另一个组件,因此将呈现逻辑与业务逻辑分离。该模型也不会频繁更改其模式,并且在整个应用生命周期中或多或少保持一致。
因此,简言之,模型负责执行以下角色:
- 提供访问存储在应用中的数据的方法
- 将表示逻辑与业务逻辑分离
- 为存储在应用中的数据提供持久性
- 提供一致的接口来处理数据
视图负责向用户显示数据,或向用户显示一个界面,用户可以通过该界面操作存储在模型中的数据。MVC 中的视图通常是动态的,并且根据模型中发生的更改而频繁更改。视图也可以被认为只包含应用的表示逻辑,而不考虑应用将如何存储数据以及如何检索数据。通常,视图可用于缓存表示状态以加速数据的显示。
因此,简而言之,以下是视图执行的功能:
- 为应用提供表示逻辑,以显示存储在应用中的数据
- 向用户提供。。。
在本章中,我们介绍了设计模式的概念,以及它们如何帮助我们解决设计应用时经常遇到的一些问题。然后我们讨论了如何决定使用哪种设计模式,以及是否必须选择已经定义的模式之一。在本章中,我们进一步探讨了 Python 作为一种语言的一些面向对象功能,还探讨了在 Python 中实现抽象类和元类的一些示例,以及如何使用它们来构建其他类和修改它们的行为。
在掌握了面向对象 Python 的知识之后,我们继续实现一些常见的设计模式,例如 Python 中的 Singleton 和 Factory 模式,并探索了 MVC 模式,了解了它们试图解决的问题。
现在,随着我们对设计模式的了解,我们应该了解如何使处理应用内部数据的过程高效。下一章将带领我们完成探索不同技术的旅程,这些技术将帮助我们有效地处理将在应用中发生的数据库操作。
- 我们如何在 Python 中实现责任链模式,在哪些可能的用例中可以使用它?
__new__
方法和__init__
方法有什么区别?- 如何使用 ABCMeta 类作为抽象类的元类来实现抽象类?