学习Python设计模式

 

本书主要参阅的书籍是《精通Python设计模式》

本书分为创建型模式、结构型模式、行为型模式三大类,同时又细分为16种模式。
具体到每个模式,则通过简单介绍、现实生活中例子、软件应用实例、应用场景、具体代码实现、小结几部分,多个角度加深对某个设计模式的理解。
案例贴近生活,代码简单易懂,描述清晰明白,翻译水平上佳,确实算是我认为的好书,同时翻译还将代码上传到GitHub上方便读者下载学习,这里真应该点个赞了!

我们在学习设计模式的时候不应当仅仅立足于软件开发这一角度,同时应该立足于实际,或者以更加抽象的角度来看待这些设计模式背后的思想。比如针对某个设计模式,我们要明白想通过它实现怎么样的功能,这样设计的好处在哪里,我要提供什么输入,我将得到什么输出,即通过函数或黑盒子的视角从外面看待这个设计模式。同时也要将视角放到该设计模式的内部,可以把具体的某个方法视为车间、某个对象视为工人,将之与现实世界某个应用场景映射起来,分析通过怎么样的一个调度实现了业务的功能,其中的结构优势何在,节省了空间还是时间上的资源等。

笔记摘抄

  1. 设计模式的本质是在已有的方案之上发现更好的方案(而不是全新发明)。
  2. 设计模式并非是某种高大上或者神秘的东西,而是一些常见的软件工程设计问题的最佳实践方案。
  3. 个人认为软件开发技术的学习都应该以实践为前提,只有理解实践过程中遇到的种种问题,才能明白那些技术的本质和目的是什么,因为每种新技术都是因某个/某些问题而出现的。
  4. 设计模式一般是描述如何组织代码和使用最佳实践来解决常见的设计问题。

书籍结构

创建型模式

创建型设计模式处理对象创建相关的问题,目标是当直接创建对象不太方便时,提供更好的方式。

工厂模式

工厂背后的思想是简化对象的创建

通过将创建对象的代码和使用对象的代码解耦,工场能够降低应用维护的复杂度。

工厂通常有两种形式:一种是工厂方法,它是一个方法(或以地道的Python数据来说,是一个函数),对不同的输入参数返回不同的对象;第二种是抽象工厂,它是一组用于创建一系列相关事物对象的工厂方法。

工厂方法可以在必要时创建新的对象,从而提高性能和内存使用率。

一个抽象工厂是(逻辑上的)一组工厂方法,其中的每个工厂方法负责产生不同种类的对象。

抽象工厂模式是工厂方法模式的一种泛化。

通常一开始时使用工厂方法,因为它更简单。

工厂方法和抽象工厂设计模式可适用于以下场景:

  • 想要追踪对象莱恩创建时
  • 想要将对象的创建与使用解耦时
  • 想要优化应用的性能和资源占用时

建造者模式

建造者模式将一个复杂对象的构造过程与其表现分离,这样,同一个构造过程可用于创建多个不同的表现。

如果我们知道一个对象必须经过多个步骤来创建,并且要求同一个构造过程可以产生不同的表现,就可以使用建造者模式。

建造者模式可适用于以下场景:

  • 想要创建一个复杂对象(对象由多个部分构成,且对象的创建要经过多个不同的步骤,这些步骤也许还需遵从特定的顺序)
  • 要求一个对象能有不同的表现,并希望将对象的构造与表现解耦
  • 想要在某个时间点创建对象,但在稍后的时间点再访问

原型模式

原型设计模式用于创建对象的完全副本。

一般创建副本的两种方式:

  • 当创建一个浅副本时,副本依赖引用
  • 当创建一个深副本时,副本复制所有东西

第一种情况下,我们希望提升应用性能和优化内存使用,在对象之间引入数据共享,但需要小心地修改数据,因为所有变更对所有副本都是可见的。

第二种情况下,我们希望能够对一个副本进行更改而不会影响其他对象。

结构型模式

结构型设计模式处理一个系统中不同实体(比如,类和对象)之间的关系,关注的是提供一种简单的对象组合方式来创建新功能。

适配器模式

适配器模式帮助我们实现两个不兼容接口之间的兼容

开放/封闭原则(open/close principle)是面向对象设计的基本原则之一,声明一个软件实体应该对拓展是开放的,对修改则是封闭的。本质上这意味着我们应该无需修改一个软件实体的源码就能拓展其行为。适配器模式遵从开放/封闭原则。

适配器让一件产品在制造出来之后需要应对新需求之时还能工作。

修饰器模式

修饰器模式通常用于拓展一个对象的功能。

一般来说,应用中有些部件是通用的,可应用于其他部件,这样的部件被看作横切关注点。

修饰器模式是实现横切关注点的绝佳方案,因为横切关注点通用但不太适合使用面向对象编程范式来实现。

Python进一步拓展了修饰器的概念,允许我们无需使用继承或组合就能拓展任意可调用对象(函数、方法或类)的行为。

外观模式

外观模式有助于隐藏系统所的内部复杂性,并通过一个简化的接口向客户端暴露必要的部分。本质上,外观(Facade)是在已有复杂系统之上实现的一个抽象层。

使用外观模式的最常见理由是为一个复杂系统提供单个简单的入口点。

不把系统的内部功能暴露给客户端代码有一个额外的好处:我们可以改变系统内部代码,但客户端代码不用关心这个改变,也不会受到这个改变的影响。客户端代码不需要进行任何改变。

如果你的系统包含多层,外观模式也能派上用场。你可以为每一层引入一个外观入口点,并让所有层级通过它们的外观相关通信。这提高了层级之间的松耦合性,尽可能保持层级独立。

外观模式是一种隐藏系统复杂性的优雅方式,因为多数情况下客户端代码并不应该关心系统的这些细节。

享元模式

作为软件工程师,我们应该编写更好的软件来解决软件问题,而不是要求客户购买更多更好的硬件。

享元模式通过为相似对象引入数据共享来最小化内存使用,提升性能。

一个享元(Flyweight)就是一个包含状态独立的不可变(又称固有的)数据的共享对象。

若想要享元模式有效,需要满足GoF的《设计模式》一书罗列的以下几个条件。

  • 应用需要使用大量的对象。
  • 对象太多,存储/渲染它们的代价太大。
  • 对象ID对于应用不重要。

一般来说,在应用需要创建大量的计算代价大但共享许多属性的对象时,可以使用享元。

模型-视图-控制器模式

关注点分离(Separation of Concerns,SoC)原则是软件工程相关的设计原则之一。

SoC原则背后的思想是将一个应用切分成不同的部分,每个部分解决一个单独的关注点。

模型-视图-控制器(Model-View-Controller,MVC)模式是应用到面向对象编程的SOC原则。

其中,模型是核心的部分,代表着应用的信息本源,包含和管理(业务)逻辑、数据、状态以及应用的规则。视图是模型的可视化表现。视图只是展示数据,并不处理数据。控制器是模型与视图之间的链接/粘附。模型与视图之间的所有通信都通过控制器进行。

为了实现模型与其表现之间的解耦,每个视图通常都需要属于它的控制器。(我认为作者想表达的是没有控制器也可以,但是由于视图并不具备处理数据的功能,并且通常每个页面所要处理的业务都会不同,因此这样的模型是高度定制的模型,适用性很差)

在从头开始实现MVC时,请确保创建的模型很智能,控制器很瘦,视图很傻瓜。

可以将具有以下功能的模型视为智能模型:

  • 包含所有的校验/业务规则/逻辑
  • 处理应用的状态
  • 访问应用数据(数据库、云或其他)
  • 不依赖UI
    可以将符合以下条件的控制器视为瘦控制器:
  • 在用户与视图交互时,更新模型
  • 在模型改变时,更新视图
  • 如果需要,在数据传递给模型/视图之前进行处理
  • 不展示数据
  • 不直接访问应用数据
  • 不包含校验/业务规则/逻辑
    可以将符合以下条件的视图视为傻瓜视图:
  • 展示数据
  • 允许用户与其交互
  • 仅做最小的数据处理,通常由一种模板语言提供处理能力(例如,使用简单的变量和循环控制)
  • 不存储任何数据
  • 不直接访问应用数据
  • 不包含校验/业务规则/逻辑

使用MVC时,请确保创建智能的模型(核心功能)、瘦控制器(实现视图与模型之间通信的能力)以及傻瓜式的视图(外在表现,最小化逻辑处理)

代理模式

四种不同的知名代理类型:

  • 远程代理:实际存在于不同地址空间(例如,某个网络服务器)的对象在本地的代理者。
  • 虚拟代理:用于懒初始化,将一个大计算量对象的创建延迟到真正需要的时候进行。
  • 保护/防护代理:控制对敏感对象的访问。
  • 智能(引用)代理:在对象被访问时执行额外的动作。此类代理的例子包括引用计数和线程安全检查。

现实中,,永远不要执行以下操作:

  • 在源代码中存储密码
  • 以明文形式存储密码
  • 使用一种弱(例如,MD5)或自定义加密形式

行为型模式

行为型模式处理对象互联和算法的问题

责任链模式

责任链(Chain of Responsibility)模式用于让多个对象来处理单个请求时,或者用于预先不知道应该由哪个对象(来自某个对象链)来处理某个特定请求时。其原则如下所示:

  1. 存在一个对象链(链表、树或任何其他便捷的数据结构)。
  2. 我们一开始将请求发送给链中的第一个对象。
  3. 对象决定其是否要处理该请求。
  4. 对象将请求转发给下一个对象。
  5. 重复该过程,直到到达链尾。

通过使用责任链模式,我们能让许多对象来处理一个特定请求。在我们预先不知道应该由哪个对象来处理某个请求时,这是有用的。另一个责任链可以派上用场的场景是,在我们知道可能会有多个对象都需要对同一个请求进行处理之时。

这一模式的价值在于解耦。

客户端与所有处理程序(一个处理程序与所有其他处理程序之间也是如此)之间不再是多对多关系,客户端仅需要知道如何与链的起始节点(标头)进行通信。

在无法预先知道处理程序的数量和类型时,该模式有助于对请求/处理事件进行建模。适合使用责任链模式的系统例子包括基于事件的系统、采购系统和运输系统。

命令模式

命令设计模式帮助我们将一个操作(撤销、重做、复制、粘贴)封装成一个对象。简而言之,这意味着创建一个类,包含实现该操作所需要的所有逻辑和方法。

这样做的优势如下所述参考:

  • 我们并不需要直接执行一个命令。命令可以按照希望执行。
  • 调用命令的对象与知道如何执行命令的对象解耦。调用者无需知道命令的任何实现细节。
  • 如果有意义,可以把多个命令组织起来,这样调用者能够按顺序执行它们。例如,在实现一个多层撤销命令时,这是很有用的。

解释器模式

解释器(Interpreter)模式仅能引起应用的高级用户的兴趣。这是因为解释器模式背后的贮存思想是让非初级用户和领域专家使用一门简单的语言来表达想法。

领域特定语言(Domain Specific Language,DSL)是一种针对一个特定领域的有限表达能力的计算机语言。DSL分为内部DSL和外部DSL。

内部DSL构建在一个宿主编程语言之上。

  • 优势:我们不必担心创建、编译及解析语法,因为这些已经被宿主语言解决掉了。
  • 劣势:会受限于宿主语言的特性。如果宿主语言不具备这些特性,构建一种表达能力强、简洁而且优美的内部DSL是富有挑战性的。

外部DSL不依赖某种宿主语言。DSL的创建者可以决定语言的方方面面(语法、句法等),但也要负责为其创建一个解析器和编译器。为一种新语言创建解析器和编译器是一个非常复杂、长期而又痛苦的过程。

解释器模式仅与内部DSL相关。

观察者模式

观察者模式描述单个对象(发布者,又称为主持者或可观察者)与一个或多个对象(订阅者,又称为观察者)之间的发布-订阅关系。

观察者模式背后的思想等同于MVC和关注点分离原则背后的思想,即降低发布者与订阅者之间的耦合度,从而易于在运行时添加/删除订阅者。此外,发布者不关心它的订阅者是谁。它只是将通知发送给所有订阅者。

当我们希望在一个对象(主持者/发布者/可观察者)发生变化时通知/更新另一个或多个对象的时候,通常会使用观察者模式。观察者的数量以及谁是观察者可能会有所不同,也可以(在运行时)动态地改变。

状态模式

在很多问题中,有限状态机(通常名为有限状态机)是一个非常方便的状态转换建模(并在必要时以数学方式形式化)工具。

状态机是一个抽象机器,有两个关键部分,状态和转换。状态是指系统的当前(激活)状况。一个状态机在任意时间点只会有一个激活状态。转换是指从一个状态切换到另一个状态,因某个事件或条件的触发而开始。在一个转换发生之前或之后通常会执行一个或多个动作。

状态机带一个不错的特性是可以用图来表现(称为状态图),其中每个状态都是一个节点,每个转换都是两个节点之间的边。

状态模式就是应用到一个特定软件工程问题的状态机。

使用状态模式本质上相当于实现一个状态机来解决特定领域的一个软件问题。

状态设计模式解决的是一定上下文中无限数量状态的完全封装,从而实现更好的可维护性和灵活性。

策略模式

策略模式通常用在我们希望对同一个问题透明地使用多种方案时。如果并不存在针对所有输入数据和所有情况的完美算法,那么我们可以使用策略模式,动态地决定在每种情况下应使用哪种算法。

模板模式

编写优秀代码的一个要素是避免冗余。

模板模式关注的是消除代码冗余,其思想是我们应该无需改变算法结构就能重新定义一个算法的某个部分。

设计模式是被发现,而不是被发明出来的。