设计模式(Design Pattern)是一套被反复使用、多数人知晓的、无数工程师实践的代码设计经验的总结,它是面向对象思想的高度提炼和模板化。使用设计模式将会让代码具有更高的可重用性、更好的灵活性和可拓展性、更容易阅读和理解。
程序不应只是冷冰冰的代码,更应赋予它生活的乐趣和特殊的意义。本课程内容将会从生活的角度,在生活的每一个细节和故事中解读一个个设计模式。力求用更通俗的语言阐述难懂的概念;用更简单的语法实现复杂的逻辑;用更短小的代码写出强悍的程序! 希望能带给读者一种全新的阅读体验和思考方式。
半年前,发布了《如何从生活中领悟设计模式》一系列的课程,收效还不错!这一课程共计 11 篇,讲了常用的 10 种设计模式。应读者要求,经半年精心准备后终于可以把剩余的那些设计模式补充完整,并对上一期的内容做了部分修正和升级,现合集在一起作为升级版,原有课程不再售卖(读者若已购买,不影响阅读,同时若购买新的课程会有很大的优惠。)。
此升级版的系列课程分三部分内容:
本系列课程相关源码,请单击这个链接获取,https://github.com/luoweifu/PyDesignPattern。
罗伟富(Spencer.Luo),CSDN 博客专家,某知名在线教育公司技术专家。3 年 SDK 开发,2 年客户端开发,现从事后端基础技术研究,从底层到应用层,从前端到后端积累了丰富的开发经验,熟悉 C++、Java、Python、PHP 等多种不同开发语言。热爱生活、喜欢技术、善于用生活的场景解读难懂的技术!微信公众号:SunLogging,个人微信:SmilingSunrise。
两年前 CSDN 出了一个产品叫 ink,旨在提供一个高质量的写作环境,那时就有写设计模式这一系列的想法了,而且也确实写了,在 ink 里写了三篇文章,后来不知道什么原因这个产品下架了,写的三篇文章也没了,这事也就一直被搁置了;直到今天,我想重新开始,以全新的方式和思路重新写这一系列内容!
设计模式最初是被 GoF 于 1995 年提出的,GoF(Gang of Four,四人帮)即 Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides。他们四人于 1995 年出版了一本书《Design Patterns:Elements of Reusable Object-Oriented Software》(翻译成中文是《设计模式 可复用面向对象软件的基础》),第一次将设计模式提升到理论高度,并将之规范化,该书提出了 23 种经典的设计模式。
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、无数工程师实践的代码设计经验的总结,它是面向对象思想的高度提炼和模板化,使用设计模式是为了让代码具有更高的可重用性,更好的灵活性和可拓展性,更易被人阅读和理解。GoF 提到的模式有四个基本要素:
我一直坚信:程序源于生活,又高于生活!程序的灵魂在于思维的方式,而思维的灵感来源于生活的精彩。互联网是一个虚拟的世界,而程序本身就是对生活场景的虚拟和抽象,每一个模式我都能在生活中找到他的影子。比如,说到状态模式我能想到水有冰、水、气三种状态,而人也有少、壮、老三个不同的阶段;提起中介模式我能立马想到房产中介;看到单例模式,脑海中会即刻浮现心目中的那个她……
设计模式是面向对象的高度抽象和总结,而越抽象的东西越难以理解。本系列文章的目地就是为了降低设计模式的阅读门槛,以生活中的小故事开始,用风趣的方式,由浅入深地讲述每一个模式。让你再次看到设计模式时不只是一个模式,还是生活中的一个个小确幸!程序不是冷冰冰的代码,它还有生活的乐趣和特殊意义。
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案,这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。所以不管你是新手还是老手,学习设计模式将对你都有莫大的帮助。
学习设计模式的理由有很多,这里只列出几个最实现的:
熟悉一门面向对象语言
首先,至少要熟悉一门面向对象的计算机语言。如果没有,请根据自己的学习爱好,或希望从事的工作,先选择一门面向对象语言(C++、Java、Python、Go 等都可以)进行学习和实战,对抽象、继承、多态、封装有一定的基础之后,再来看本系列的文章内容。
了解 Python 的基本语法
对 Python 的基本语法有一个简单了解。Python 语法非常简单,只要有一定的编程语言基础,通过下文的介绍很快就能理解的。
学会阅读 UML 图
UML(Unified Modeling Language)称为统一建模语言或标准建模语言,是面向对象软件的标准化建模语言。UML 规范用来描述建模的概念有:类(对象的)、对象、关联、职责、行为、接口、用例、包、顺序、协作以及状态。
UML 类图表示不同的实体(人、事物和数据)如何彼此相关;换句话说,它显示了系统的静态结构。想进一步了解类图中的各种关系,可参考以下文章:
阅读本系列文章
通过阅读本系列文章,以轻松愉快的方式学习设计模式和编程思想。本系列文章没有阅读的先后顺序,每一章都是单独成文,可从任意一篇文章开始。
虽然说设计模式与编程语言没有关系,它是对面向对象思想的灵活应用和高度概括,可以用任何一种语言来实现它,但总归是需要用一种语言进行举例的。本系列文章的所有示例代码均使用 Python 语言编写,为什么选择 Python,主要是基于以下两个原因。
设计模式于 1995 被 GoF 提出,被广泛应用于热门的面对象语言。目前用 Java、C++ 描述的设计模式的书籍和资料已经非常多了,但用 Python 来描述的真是太少了;我在当当上搜索了一下“Python 设计模式”关键字,发现只有那零星的几本书。而作为已经挤进 Top4 的 Python 语言,这明示是不够的。Python 已经越来越成熟,也越来越多地被使用,作为一个有技术追求的 IT 儿有必要了解一下基于 Python 代码设计。
最热门的 AI 开源框架 PyTorch 和 TensorFlow 都已经采用了 Python 作为接口和开发语言。除此之外,还有一堆的 AI 相关的框架库,也都纷纷采用,如 AIMA、pyDatalog、SimpleAI、PyBrain、PyML 等。
作为这么一门有前途的语言,必然是要去学习和使用的。
如果已经熟悉 Python 语言,这一部分的内容可直接跳过!
Python 崇尚优美、清晰、简单,是一个优秀并广泛使用的语言。
与 Java 和 C++ 这些语言相比,Python 最大的两个特点是:
刚转过来的时候可能会有点不适,用一段时间就好了!
个人觉得,在所有的高级计算机语言中,Python 是最接近人类自然语言的。Python 的语法、风格都与英文的书写习惯非常接近,Python 的这种风格被称为 Pythonic,如条件表达式,在 Java 和 C++ 中是这样的:
int min = x < y ? x : y
而 Python 是这样的:
min = x if x < y else y
有没有觉得第二种方式更接近人类的自然思维?
数据类型
Python 是一种弱类型的语言,变量的定义不需要在前面加类型说明,而且不同类型之间可以方便地相互转换。Python 有五个标准的数据类型:
其中 List、Tuple、Dictionary 为容器,将在下一部分介绍。Python 支持四种不同的数字类型:int(有符号整型)、long(长整型)、float(浮点型)、complex(复数)。
每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。
Demo:
age = 18 # intweight = 62.51 # floatname = "Tony" # stringprint("age:", age)print("weight:", weight)print("name:", name)# 变量的类型可以直接改变age = nameprint("age:", age)a = b = c = 5# a,b,c三个变量指向相同的内存空间,具有相同的值print("a:", a, "b:", b, "c:", c)print("id(a):", id(a), "id(b):", id(b), "id(c):", id(c))
结果:
age: 18weight: 62.51name: Tonyage: Tonya: 5 b: 5 c: 5id(a): 1457772400 id(b): 1457772400 id(c): 1457772400
List(列表)是 Python 中使用最频繁的数据类型,用 [ ] 标识。
列表可以完成大多数集合类的数据结构实现。类似于 Java 中的 ArrayList,C++ 中的 Vector。此外,一个 List 中还可以同时包含不同类型的数据,支持字符、数字、字符串,甚至可以包含列表(即嵌套)。
Demo:
list = ['Thomson', 78, 12.58, 'Sunny', 180.2]tinylist = [123, 'Tony']print(list) # 输出完整列表print(list[0]) # 输出列表的第一个元素print(list[1:3]) # 输出第二个至第三个元素print(list[2:]) # 输出从第三个开始至列表末尾的所有元素print(tinylist * 2) # 输出列表两次print(list + tinylist) # 打印组合的列表list[1] = 100 # 修改第二个元素的值print(list) # 输出完整列表list.append("added data")print(list) # 输出增加后的列表
结果:
['Thomson', 78, 12.58, 'Sunny', 180.2]Thomson[78, 12.58][12.58, 'Sunny', 180.2][123, 'Tony', 123, 'Tony']['Thomson', 78, 12.58, 'Sunny', 180.2, 123, 'Tony']['Thomson', 100, 12.58, 'Sunny', 180.2]['Thomson', 100, 12.58, 'Sunny', 180.2, 'added data']
Tuple(元组)是另一个数据类型,元组用“()”标识,内部元素用逗号隔开。元组不能二次赋值,相当于只读列表,用法与 List 类似。Tuple 相当于 Java 中的 final 数组,C++ 中的 const 数组。
Demo:
tuple = ('Thomson', 78, 12.58, 'Sunny', 180.2)tinytuple = (123, 'Tony')print(tuple) # 输出完整元组print(tuple[0]) # 输出元组的第一个元素print(tuple[1:3]) # 输出第二个至第三个的元素print(tuple[2:]) # 输出从第三个开始至列表末尾的所有元素print(tinytuple * 2) # 输出元组两次print(tuple + tinytuple)# 打印组合的元组# tuple[1] = 100 # 不能修改元组内的元素
结果:
('Thomson', 78, 12.58, 'Sunny', 180.2)Thomson(78, 12.58)(12.58, 'Sunny', 180.2)(123, 'Tony', 123, 'Tony')('Thomson', 78, 12.58, 'Sunny', 180.2, 123, 'Tony')
Dictionary(字典)是 Python 中除列表以外最灵活的内置数据结构类型。字典用“{ }”标识,字典由索引(key)和它对应的值 value 组成,相当于 Java 和 C++ 中的 Map。
列表是有序的对象集合,字典是无序的对象集合。两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存取。
Demo:
dict = {}dict['one'] = "This is one"dict[2] = "This is two"tinydict = {'name': 'Tony', 'age': 24, 'height': 177}print(dict['one']) # 输出键为'one' 的值print(dict[2]) # 输出键为 2 的值print(tinydict) # 输出完整的字典print(tinydict.keys()) # 输出所有键print(tinydict.values())# 输出所有值
结果:
This is oneThis is two{'name': 'Tony', 'age': 24, 'height': 177}dict_keys(['name', 'age', 'height'])dict_values(['Tony', 24, 177])
使用 class 语句来创建一个新类,class 之后为类的名称并以冒号结尾,如下实例:
class ClassName: '类的帮助信息' #类文档字符串 class_suite #类体
类的帮助信息可以通过 ClassName.__doc__
查看,class_suite 由类成员,方法,数据属性组成。如:
class Test: "这是一个测试类" def __init__(self): self.__ivalue = 5 def getvalue(self): return self.__ivalue
其中,__init__
为初始化函数,相当于构造函数。
访问权限:
__foo__
:定义的是特殊方法,一般是系统定义名字,类似 __init__
() 之类的。_foo
:以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *。__foo
:双下划线的表示的是私有类型(private)的变量,只能是允许这个类本身进行访问了。类的继承:
继承的语法结构如下:
class 派生类名(基类名): 类体
Python 中继承中的一些特点:
__init__()
方法)不会被自动调用,它需要在其派生类的构造中亲自专门调用。如果在继承元组中列了一个以上的类,那么它就被称作“多重继承”。
基础重载方法
Python 的类中有很多内置的方法,我们可以通过重写这些方法来实现一些特殊的功能,这些方法有:
序号 | 方法 | 描述 | 简单的调用 |
---|---|---|---|
1 | __init__ (self [,args...] ) |
构造函数 | obj = className(args) |
2 | __del__ (self) |
析构方法, 删除一个对象 | del obj |
3 | __repr__ (self) |
转化为供解释器读取的形式 | repr(obj) |
4 | __str__ (self) |
用于将值转化为适于人阅读的形式 | str(obj) |
5 | __cmp__ (self, x) |
对象比较 | cmp(obj, x) |
我们将一段 Java 的代码对应到 Python 中来实现,进行对比阅读,相信很快就能明白其中的用法。Java 代码如下:
class Person { public static int visited; Person(String name, int age, float height) { this.name = name; this.age = age; this.height = height; } public String getName() { return name; } public int getAge() { return age; } public void showInfo() { System.out.println("name:" + name); System.out.println("age:" + age); System.out.println("height:" + height); System.out.println("visited:" + visited); Person.visited ++; } private String name; protected int age; public float height;}class Teacher extends Person { Teacher(String name, int age, float height) { super(name, age, height); } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public void showInfo() { System.out.println("title:" + title); super.showInfo(); } private String title;}public class Test { public static void main(String args[]) { Person tony = new Person("Tony", 25, 1.77f); tony.showInfo(); System.out.println(); Teacher jenny = new Teacher("Jenny", 34, 1.68f); jenny.setTitle("教授"); jenny.showInfo(); }}
对应的 Python 代码:
class Person: "人" visited = 0 def __init__(self, name, age, height): self.__name = name self._age = age self.height = height def getName(self): return self.__name def getAge(self): return self._age def showInfo(self): print("name:", self.__name) print("age:", self._age) print("height:", self.height) print("visited:", self.visited) Person.visited = Person.visited +1class Teacher(Person): "老师" def __init__(self, name, age, height): super().__init__(name, age, height) self.__title = None def getTitle(self): return self.__title def setTitle(self, title): self.__title = title def showInfo(self): print("title:", self.__title) super().showInfo()def testPerson(): "测试方法" tony = Person("Tony", 25, 1.77) tony.showInfo() print(); jenny = Teacher("Jenny", 34, 1.68); jenny.setTitle("教授"); jenny.showInfo();testPerson()
自己测试一下,会发现结果是一样的:
name: Tonyage: 25height: 1.77visited: 0title: 教授name: Jennyage: 34height: 1.68visited: 1
点击了解更多《白话设计模式 28 讲》
【故事剧情】
刚刚大学毕业的 Tony 只身来到北京这个硕大的城市,开始了北漂的生活。但刚刚毕业的他身无绝技、包无分文,为了生活只能住在沙河镇一个偏僻的村子里,每天坐着程序员专线(13号线)来回穿梭于昌平区与西城区……
在一个寒冷的冬天,下班之后要坐2个小时的地铁+公交才能回到住处,Tony 拖着疲惫的身体回到家。准备洗一个热水澡暖暖身体,耐何简陋的房子中用的还是90年代的热水器。因为热水器没有警报更没有自动切换模式的功能,所以烧热水必须得守着;不然时间长了成杀猪烫,时间短了又冷成狗。无奈的 Tony 背靠着墙,头望着天花板,深夜中做起了白日梦:一定要努力工作,过两个月我就可以自己买一个智能热水器了:水烧好了就发一个警报,我就可以直接去洗操。还要能自己设定模式,既可以烧开了用来喝,可以烧暖了用来洗澡……
Tony 陷入白日梦中……他的梦虽然在现实世界里不能立即实现,但在程序世界里可以。程序来源于生活,下面我们就用代码来模拟 Tony 的白日梦。
源码示例:
class WaterHeater: "热水器:战胜寒冬的有利武器" def __init__(self): self.__observers = [] self.__temperature = 25 def getTemperature(self): return self.__temperature def setTemperature(self, temperature): self.__temperature = temperature print("current temperature is:", self.__temperature) self.notifies() def addObserver(self, observer): self.__observers.append(observer) def notifies(self): for o in self.__observers: o.update(self)class Observer: "洗澡模式和饮用模式的父类" def update(self, waterHeater): passclass WashingMode(Observer): "该模式用于洗澡用" def update(self, waterHeater): if waterHeater.getTemperature() >= 50 and waterHeater.getTemperature() < 70: print("水已烧好,温度正好!可以用来洗澡了。")class DrinkingMode(Observer): "该模式用于饮用" def update(self, waterHeater): if waterHeater.getTemperature() >= 100: print("水已烧开!可以用来饮用了。")
测试代码:
def testWaterHeater(): heater = WaterHeater() washingObser = WashingMode() drinkingObser = DrinkingMode() heater.addObserver(washingObser) heater.addObserver(drinkingObser) heater.setTemperature(40) heater.setTemperature(60) heater.setTemperature(100)
输出结果:
current temperature is: 40current temperature is: 60水已烧好,温度正好!可以用来洗澡了。current temperature is: 100水已烧开!可以用来饮用了。
这个代码非常简单,水烧到50-70度时,会发出警告:可以用来洗澡了!烧到100度也会发出警告:可以用来喝了!在这里洗澡模式和饮用模式扮演了监听的角色,而热水器则是被监听的对象。一旦热水器中的水温度发生变化,监听者都能及时知道并做出相应的判断和动作。其实这就是程序设计中监听模式的生动展现。
监听模式又名观察者模式,顾名思意就是观察与被观察的关系,比如你在烧开水得时时看着它开没开,你就是观察者,水就是被观察者;再比如说你在带小孩,你关注她是不是饿了,是不是喝了,是不是撒尿了,你就是观察者,小孩就是被观察者。
观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。当你看这些模式的时候,不要觉得陌生,它们就是观察者模式。
观察者模式一般是一种一对多的关系,可以有任意个(一个或多个)观察者对象同时监听某一个对象。监听的对象叫观察者(后面提到监听者,其实就指观察者,两者是等价的),被监听的对象叫被观察者(Observable,也叫主题 Subject)。被观察者对象在状态或内容发生变化时,会通知所有观察者对象,使它们能够做出相应的变化(如自动更新自己的信息)。
上面的示例代码还是相对比较粗糙,我们可以对它进行进一步的重构和优化,抽象出监听模式的框架模型。
class Observer: "观察者的基类" def update(self, observer, object): passclass Observable: "被观察者的基类" def __init__(self): self.__observers = [] def addObserver(self, observer): self.__observers.append(observer) def removeObserver(self, observer): self.__observers.remove(observer) def notifyObservers(self, object = 0): for o in self.__observers: o.update(self, object)
上面的代码框架可用类图表示如下:
addObserver,removeObserver 分别用于添加和删除观察者,notifyObservers 用于内容或状态变化时通知所有的观察者。因为 Observable 的 notifyObservers 会调用 Observer 的 update 方法,所有观察者不需要关心被观察的对象什么时候会发生变化,只要有变化就是自动调用 update,只需要关注 update 实现就可以了。
有了上面的代码框架之后,我们要实现示例代码的功能就会更简单了。最开始的示例代码我们假设它为 version 1.0,那么再看看基于框架的 version 2.0 吧。
class WaterHeater(Observable): "热水器:战胜寒冬的有利武器" def __init__(self): super().__init__() self.__temperature = 25 def getTemperature(self): return self.__temperature def setTemperature(self, temperature): self.__temperature = temperature print("current temperature is:", self.__temperature) self.notifyObservers()class WashingMode(Observer): "该模式用于洗澡用" def update(self, observable, object): if isinstance(observable, WaterHeater) and observable.getTemperature() >= 50 and observable.getTemperature() < 70: print("水已烧好,温度正好!可以用来洗澡了。")class DrinkingMode(Observer): "该模式用于饮用" def update(self, observable, object): if isinstance(observable, WaterHeater) and observable.getTemperature() >= 100: print("水已烧开!可以用来饮用了。")
测试代码不用变。自己跑一下,会发现输出结果和之前的是一样的。
在设计观察者模式的程序时要注意以下几点:
观察者模式根据其侧重的功能还可以分为推模型和拉模型。
推模型:被观察者对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。一般这种模型的实现中,会把被观察者对象中的全部或部分信息通过 update 的参数传递给观察者 [update(Object obj) ,通过 obj 参数传递]。
如某应用 App 的服务要在凌晨1:00开始进行维护,1:00-2:00期间所有服务将会暂停,这里你就需要向所有的 App 客户端推送完整的通知消息:“本服务将在凌晨1:00开始进行维护,1:00-2:00期间所有服务将会暂停,感谢您的理解和支持!” 不管用户想不想知道,也不管用户会不会在这段期间去访问,消息都需要被准确无误地通知到。这就是典型的推模型的应用。
拉模型:被观察者在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到被观察者对象中获取,相当于是观察者从被观察者对象中拉数据。一般这种模型的实现中,会把被观察者对象自身通过 update 方法传递给观察者 [update(Observable observable ),通过 observable 参数传递 ],这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
如某应用 App 有新的版本推出,则需要发送一个版本升级的通知消息,而这个通知消息只会简单地列出版本号和下载地址,如果你需要升级你的 App 还需要调用下载接口去下载安装包完成升级。这其实也可以理解成是拉模型。
推模型和拉模型其实更多的是语义和逻辑上的区别。我们上面的代码框架,从接口 [update(self, observer, object)] 上你应该知道是可以同时支持推模型和拉模型的。推模型时,observer 可以传空,推送的信息全部通常 object 传递;拉模型时,observer 和 object 都传递数据,或只传递 observer,需要更具体的信息时通过 observer 引用去取数据。
学习设计模式,更应该领悟其设计思想,不应该应该局限于代码的层面。 观察者模式还可以用于网络中的客户端和服务器,比如手机中的各种 App 的消息推送,服务端是被观察者,各个手机 App 是观察者,一旦服务器上的数据(如 App 升级信息)有更新,就会被推送到手机客户端。在这个应用中你会发现服务器代码和 App 客户端代码其实是两套完全不一样的的代码,它们是通过网络接口进行通迅的,所以如果你只是停留在代码层面是无法理解的!
点击了解更多《白话设计模式 28 讲》
【故事剧情】
晚上九点半,Tony 上了地铁,准备回家,正巧还有一个空位,赶紧走向前坐下。工作一天后,疲惫不堪的他正准备坐着打个盹小睡一会儿。这时进来两个小姑娘,一个小巧可爱,一个身姿曼妙;嬉笑地聊着天走到了 Tony 的前面,Tony 犹豫了片刻后还是绅士般地给小女孩让了个座……
两个小姑娘道了声谢谢,便挤在一块坐下了,继续有说有笑地谈论着……
Amy:周末在商场里看到你和一个帅哥在一起。好你个 Nina,脱单了也不告诉姐姐我,太不够意思了!怎么……想金屋藏“娇”啊!
Nina:不是啦,也是最近刚有事,还没来得及告诉你呢。
Amy:那快说说呗!那小哥看着很高啊!
Nina:嗯,他1米85。
Amy:厉害了,你155 他185,这就是传说中的最萌身高组合啊!
Nina:嗯,走在大街上,别人都回头看我们,弄的我挺不好了意思的~
Amy:你这是身在福中不知福啊!别人就是因为想求也求不到呢!
Nina:也有很气的时候啦,有时生气想打他,结果粉拳一出去就被他的大手包了饺子。
Amy:哈哈哈哈,还有呢!
Nina:还有一件很囧的事,我一抬头总是看到他的鼻毛,他一低头总是看到我的头发屑!
Amy:哈哈哈!笑的我肚子痛了……所以你们在一起,你一定要天天洗头,他一定要天天修鼻毛咯~
Nina:是啊!可麻烦了~
Amy:看来还是我这 160 的身高最棒了!衣服可以随便挑,更重要的是我男友 175,穿上高跟鞋,我就可以挽着他的手肩并肩地走~
Nina:这就是所谓的身高不够鞋来凑吗?
Amy:不然怎么叫万能的高跟鞋呢……
Nina:好羡慕啊!在我这,高跟鞋也无能~
Amy:... ...
正听的兴起时,地铁门开了。Tony 才反应过来,到站了,该下车了。Tony 赶忙往车门方向走,一不小心额头碰到了把手上,只好一手护着头往外跑,两个小姑娘相视一笑~
身材苗条、长像出众是每个人梦寐以求的,尤其是女孩子!但很多人却因为先天的原因并不能如意,这时就需要通过服装、化妆去弥补。所谓美女,三分靠长相七分靠打扮!比如身高不够,就可以通过穿高跟鞋来弥补;如果本身就比较高,那穿不穿高跟鞋就没那么重要了。这里的高跟鞋就起着一个适配的作用,能让你的形象增高四、五厘米,下面我们就用代码来模拟一下高跟鞋在生活中的场景吧!
源码示例:
class IHightPerson: "接口类,提供空实现的方法,由子类去实现" def getName(self): "获取姓名" pass def getHeight(self): "获取身高" passclass HighPerson(IHightPerson): "个高的人" def __init__(self, name): self.__name = name def getName(self): return self.__name def getHeight(self): return 170class ShortPerson: "个矮的人" def __init__(self, name): self.__name = name def getName(self): return self.__name def getRealHeight(self): return 160 def getShoesHeight(self): return 6class DecoratePerson(ShortPerson, IHightPerson): "有高跟鞋搭配的人" def getHeight(self): return super().getRealHeight() + super().getShoesHeight()
测试代码:
def canPlayReceptionist(person): """ 是否可以成为(高级酒店)接待员 :param person: IHightPerson的对象 :return: 是否符合做接待员的条件 """ return person.getHeight() >= 165;def testPerson(): lira = HighPerson("Lira") print(lira.getName() + "身高" + str(lira.getHeight()) + ",完美如你,天生的美女!" ) print("是否适合做接待员:", "符合" if canPlayReceptionist(lira) else "不符合") print() demi = DecoratePerson("Demi"); print(demi.getName() + "身高" + str(demi.getHeight()) + "在高跟鞋的适配下,你身高不输高圆圆,气质不输范冰冰!") print("是否适合做接待员:", "符合" if canPlayReceptionist(lira) else "不符合")
输出结果:
Lira身高170,完美如你,天生的美女!是否适合做接待员: 符合Demi身高166在高跟鞋的适配下,你身高不输高圆圆,气质不输范冰冰!是否适合做接待员: 符合
在上面的例子中,高跟鞋起着一个适配的作用,让其形象增高 5~7 厘米完全不在话下,而且效果立竿见影!使得一些女孩原本不符合接待员的真实身高,在鞋子的帮助下也能符合条件。如高跟鞋一样,使原本不匹配某种功能的对象变得匹配这种功能,这在程序中叫做适配器模式。
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而无法一起工作的两个类能够在一起工作。
适配器模式的作用:
适配器模式又叫变压器模式,也叫包装模式(Wrapper),它的核心思想是将一个对象经过包装或转换后使它符合指定的接口,使得调用方可以像使用这接口的一般对象一样使用它。这一思想,在我们生活中可谓是处处可见,比如变压器插座,能让你像使用国内电器一样使用美标(110V)电器;还有就是各种转接头,如 MiniDP 转 HDMI 转接头、HDMI 转 VGA 线转换器、Micro USB 转 Type-C 转接头等。
你们知道吗?“设计模式”一词最初是来源于建筑领域,而中国古建筑是世界建筑史的一大奇迹(如最具代表性的紫禁城),中国古建筑的灵魂是一种叫榫卯结构的建造理念。
榫卯(sǔn mǎo)是两个木构件上所采用的一种凹凸结合的连接方式。凸出部分叫榫(或榫头);凹进部分叫卯(或榫眼、榫槽)。它是古代中国建筑、家具及其他木制器械的主要结构方式。
榫卯结构的经典模型如下图:
榫卯是藏在木头里的灵魂!而随着时代的变化,其结构也发生着一些变化,现在很多建材生产商也在发明和生产新型的具有榫卯结构的木板。假设木板生产商有下面两块木板,木板 A 是榫,木板 B 是卯,A、B 两块木板就完全吻合。他们之间的榫卯接口是一种 T 字形的接口。
后来,随着业务的拓展,木板厂商增加了一种新木板 C。但 C 是 L 形的接口,不能与木板 A 对接。为了让木板 C 能与木板 A 进行对接,就需要增加一个衔接板 D 进行适配,而这个 D 就相当于适配器,如下图:
适配器模式通常用于对已有的系统拓展新功能时,尤其适用于在设计良好的系统框架下接入第三方的接口或第三方的 SDK 时。在系统的最初设计阶段,最好不要把适配器模式考虑进去,除非一些特殊的场景(如系统本身就是要去对接和适配多种类型的硬件接口)。
适配器模式的类图如下:
Target 是一个接口类,是提供给用户调用的接口抽象,如上面示例中的 IHightPerson。Adaptee 是你要进行适配的对象类,如上面的 ShortPerson。Adapter 是一个适配器,是对 Adaptee 的适配,它将 Adaptee 的对象转换(或说包装)成符合 Target 接口的对象;如上面的 DecoratePerson,将 ShortPerson 的 getRealHeight 和 getShoesHeight 方法包装成 IHightPerson 的 getHeight 接口。
适配器模式中主要三个角色,在设计适配器模式时要找到并区分这些角色:
有一个电子书阅读器的项目(Reader),研发之初,产品经理经过各方讨论,最后告诉我们只支持 TXT 和 Epub 格式的电子书。然后经过仔细思考、精心设计,采用了如图1的代码架构。在这个类图中,有一个阅读器的核心类 Reader,一个 TXT 文档的关键类 TxtBook(负责 TXT 格式文件的解析),和一个 Epub 文档的关键类 EpubBook(负责 Epub 格式文件的解析)。
图1:阅读器类图
产品上线半年后,市场响应良好,业务部门反映:有很多办公人员也在用我们的阅读器,他们希望这个阅读器能同时支持 PDF 格式,这样就不用在多个阅读器神之间来回切换了,此时程序就需要增加对 PDF 格式的支持,而 PDF 并不是核心业务,我们不会单独为其开发一套 PDF 解析内核,而会使用一些开源的 PDF 库(我们称它为第三方库),如 MuPDF、TCPDF 等。而开源库的接口和我们的接口并不相同(如图2),返回的内容也不是我们直接需要的,需要经过一些转换才能符合我们的要求。
图2:第三方 PDF 解析库的类图
这时,我们就需要对 PDF 的解析库 MuPDF 进行适配。经过上面的学习,你一定知道这时该用适配器模式了,于是有了如下图3的类图结构。
图3:兼容 PDF 的类图结构
代码实现如下:
class Page: "电子书一页的内容" def __init__(self, pageNum): self.__pageNum = pageNum def getContent(self): return "第 " + str(self.__pageNum) + " 页的内容..."class Catalogue: "目录结构" def __init__(self, title): self.__title = title self.__chapters = [] self.setChapter("第一章") self.setChapter("第二章") def setChapter(self, title): self.__chapters.append(title) def showInfo(self): print("标题:" + self.__title) for chapter in self.__chapters: print(chapter)class IBook: "电子书文档的接口类" def parseFile(self, filePath): pass def getCatalogue(self): pass def getPageCount(self): pass def getPage(self, pageNum): passclass TxtBook(IBook): "TXT解析类" def parseFile(self, filePath): # 模拟文档的解析 print(filePath + " 文件解析成功") self.__pageCount = 500 return True def getCatalogue(self): return Catalogue("TXT电子书") def getPageCount(self): return self.__pageCount def getPage(self, pageNum): return Page(pageNum)class EpubBook(IBook): "TXT解析类" def parseFile(self, filePath): # 模拟文档的解析 print(filePath + " 文件解析成功") self.__pageCount = 800 return True def getCatalogue(self): return Catalogue("Epub电子书") def getPageCount(self): return self.__pageCount def getPage(self, pageNum): return Page(pageNum)class Outline: "第三方PDF解析库的目录类" passclass PdfPage: "PDF页" def __init__(self, pageNum): self.__pageNum = pageNum def getPageNum(self): return self.__pageNumclass ThirdPdf: "第三方PDF解析库" def __init__(self): self.__pageSize = 0 def open(self, filePath): print("第三方解析PDF文件:" + filePath) self.__pageSize = 1000 return True def getOutline(self): return Outline() def pageSize(self): return self.__pageSize def page(self, index): return PdfPage(index)class PdfAdapterBook(ThirdPdf, IBook): "TXT解析类" def parseFile(self, filePath): # 模拟文档的解析 rtn = super().open(filePath) if(rtn): print(filePath + "文件解析成功") return rtn def getCatalogue(self): outline = super().getOutline() print("将Outline结构的目录转换成Catalogue结构的目录") return Catalogue("PDF电子书") def getPageCount(self): return super().pageSize() def getPage(self, pageNum): page = self.page(pageNum) print("将PdfPage的面对象转换成Page的对象") return Page(page.getPageNum())# 导入os库import osclass Reader: "阅读器" def __init__(self, name): self.__name = name self.__filePath = "" self.__curBook = None self.__curPageNum = -1 def __initBook(self, filePath): self.__filePath = filePath extName = os.path.splitext(filePath)[1] if(extName.lower() == ".epub"): self.__curBook = EpubBook() elif(extName.lower() == ".txt"): self.__curBook = TxtBook() elif(extName.lower() == ".pdf"): self.__curBook = PdfAdapterBook() else: self.__curBook = None def openFile(self, filePath): self.__initBook(filePath) if(self.__curBook is not None): rtn = self.__curBook.parseFile(filePath) if(rtn): self.__curPageNum = 1 return rtn return False def closeFile(self): print("关闭 " + self.__filePath + " 文件") return True def showCatalogue(self): catalogue = self.__curBook.getCatalogue() catalogue.showInfo() def prePage(self): return self.gotoPage(self.__curPageNum - 1) def nextPage(self): return self.gotoPage(self.__curPageNum + 1) def gotoPage(self, pageNum): if(pageNum < 1 or pageNum > self.__curBook.getPageCount()): return None self.__curPageNum = pageNum print("显示第" + str(self.__curPageNum) + "页") page = self.__curBook.getPage(self.__curPageNum) page.getContent() return page
测试代码:
def testReader(): reader = Reader("阅读器") if(not reader.openFile("平凡的世界.txt")): return reader.showCatalogue() reader.gotoPage(1) reader.nextPage() reader.closeFile() print() if (not reader.openFile("平凡的世界.epub")): return reader.showCatalogue() reader.gotoPage(5) reader.nextPage() reader.closeFile() print() if (not reader.openFile("平凡的世界.pdf")): return reader.showCatalogue() reader.gotoPage(10) reader.nextPage() reader.closeFile()
输出结果:
平凡的世界.txt 文件解析成功标题:TXT电子书第一章第二章显示第1页显示第2页关闭 平凡的世界.txt 文件平凡的世界.epub 文件解析成功标题:Epub电子书第一章第二章显示第5页显示第6页关闭 平凡的世界.epub 文件第三方解析PDF文件:平凡的世界.pdf平凡的世界.pdf文件解析成功将Outline结构的目录转换成Catalogue结构的目录标题:PDF电子书第一章第二章显示第10页将PdfPage的面对象转换成Page的对象显示第11页将PdfPage的面对象转换成Page的对象关闭 平凡的世界.pdf 文件
点击了解更多《白话设计模式 28 讲》
阅读全文: http://gitbook.cn/gitchat/column/5b26040ac81ac568fcf64ea3