课程介绍

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、无数工程师实践的代码设计经验的总结,它是面向对象思想的高度提炼和模板化。使用设计模式将会让代码具有更高的可重用性、更好的灵活性和可拓展性、更容易阅读和理解。

程序不应只是冷冰冰的代码,更应赋予它生活的乐趣和特殊的意义。本课程内容将会从生活的角度,在生活的每一个细节和故事中解读一个个设计模式。力求用更通俗的语言阐述难懂的概念;用更简单的语法实现复杂的逻辑;用更短小的代码写出强悍的程序! 希望能带给读者一种全新的阅读体验和思考方式。

半年前,发布了《如何从生活中领悟设计模式》一系列的课程,收效还不错!这一课程共计 11 篇,讲了常用的 10 种设计模式。应读者要求,经半年精心准备后终于可以把剩余的那些设计模式补充完整,并对上一期的内容做了部分修正和升级,现合集在一起作为升级版,原有课程不再售卖(读者若已购买,不影响阅读,同时若购买新的课程会有很大的优惠。)。

此升级版的系列课程分三部分内容:

  • 基础篇(第01~21课),19 种常用设计模式单独章节讲解 + 剩余 4 种模式合集(会有1~2篇的篇幅);
  • 进阶篇(第22~24课),是基础设计模式的衍生,也是各大编程语言中非常重要而常见的种编程机制;
  • 经验篇(第25~27课),将会分享我对设计原则、设计模式、项目重构的经验和看法。

本系列课程相关源码,请单击这个链接获取,https://github.com/luoweifu/PyDesignPattern。

作者介绍

罗伟富(Spencer.Luo),CSDN 博客专家,某知名在线教育公司技术专家。3 年 SDK 开发,2 年客户端开发,现从事后端基础技术研究,从底层到应用层,从前端到后端积累了丰富的开发经验,熟悉 C++、Java、Python、PHP 等多种不同开发语言。热爱生活、喜欢技术、善于用生活的场景解读难懂的技术!微信公众号:SunLogging,个人微信:SmilingSunrise。

课程内容

导读:生活中的设计模式——启程之前,请不要错过我
  • 为什么叫设计模式

    • 什么是设计模式
    • 设计模式与生活有什么联系
  • 为什么要学设计模式
  • 如何进行学习
  • 为什么选择 Python
    • 弥补市场空缺
    • 大势所趋,Python 已然成风
  • 简单的 Python 基础
    • Python 的特点
    • 基本语法
    • 常用容器
      • List(列表)
      • Tuple(元组)
      • Dictionary(字典)
    • 类的定义
    • Demo 让你顿悟
  • 重要说明

两年前 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 类图表示不同的实体(人、事物和数据)如何彼此相关;换句话说,它显示了系统的静态结构。想进一步了解类图中的各种关系,可参考以下文章:

  • UML 类图关系大全
  • UML 类图关系(泛化 、继承、实现、依赖、关联、聚合、组合)

阅读本系列文章

通过阅读本系列文章,以轻松愉快的方式学习设计模式和编程思想。本系列文章没有阅读的先后顺序,每一章都是单独成文,可从任意一篇文章开始。

为什么选择 Python

虽然说设计模式与编程语言没有关系,它是对面向对象思想的灵活应用和高度概括,可以用任何一种语言来实现它,但总归是需要用一种语言进行举例的。本系列文章的所有示例代码均使用 Python 语言编写,为什么选择 Python,主要是基于以下两个原因。

弥补市场空缺

设计模式于 1995 被 GoF 提出,被广泛应用于热门的面对象语言。目前用 Java、C++ 描述的设计模式的书籍和资料已经非常多了,但用 Python 来描述的真是太少了;我在当当上搜索了一下“Python 设计模式”关键字,发现只有那零星的几本书。而作为已经挤进 Top4 的 Python 语言,这明示是不够的。Python 已经越来越成熟,也越来越多地被使用,作为一个有技术追求的 IT 儿有必要了解一下基于 Python 代码设计。

大势所趋,Python 已然成风

  • C 语言诞生于 1972 年,确随着 Unix 的诞生才深深植根于各大操作系统;
  • C++ 语言诞生于 1983 年,确因微软的可视化桌面操作系统才得以广泛传播;
  • Java 语言诞生于 1995 年,确因互联网的迅速崛起才变得家喻户晓;
  • Python 语言诞生于 1991 年,而下一场技术革命已然开始,AI 时代已然成风。在 AI 领域中已经被广泛使用的 Python 语言必将成为下一个时代的第一开发语言!

最热门的 AI 开源框架 PyTorch 和 TensorFlow 都已经采用了 Python 作为接口和开发语言。除此之外,还有一堆的 AI 相关的框架库,也都纷纷采用,如 AIMA、pyDatalog、SimpleAI、PyBrain、PyML 等。

作为这么一门有前途的语言,必然是要去学习和使用的。

简单的 Python 基础

如果已经熟悉 Python 语言,这一部分的内容可直接跳过!

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 有五个标准的数据类型:

  • Numbers(数字)
  • String(字符串)
  • List(列表)
  • Tuple(元组)
  • Dictionary(字典)

其中 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(列表)

List(列表)是 Python 中使用最频繁的数据类型,用 [ ] 标识。

列表可以完成大多数集合类的数据结构实现。类似于 Java 中的 ArrayList,C++ 中的 Vector。此外,一个 List 中还可以同时包含不同类型的数据,支持字符、数字、字符串,甚至可以包含列表(即嵌套)。

  • 列表中值的切割也可以用到变量 [头下标:尾下标] ,就可以截取相应的列表,从左到右索引默认 0 开始,从右到左索引默认 -1 开始,下标可以为空表示取到头或尾。
  • 加号(+)是列表连接运算符,星号(*)是重复操作。

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(元组)

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(字典)

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__() 方法)不会被自动调用,它需要在其派生类的构造中亲自专门调用。
  • 在调用基类的方法时,需要使用 super() 前缀。
  • Python 总是首先查找对应类型的方法,如果它不能在派生类中找到对应的方法,它才开始到基类中逐个查找(先在本类中查找调用的方法,找不到才去基类中找)。

如果在继承元组中列了一个以上的类,那么它就被称作“多重继承”。

基础重载方法

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)

Demo 让你顿悟

我们将一段 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

重要说明

  • 为了降低程序复杂度,本系列文章中用到的所有示例代码均不考虑多线程安全,望借鉴 Demo 的读者注意。
  • 本系列所有 Demo 均是在 Python 3.6.3 下编写的,Python 3.0 以上应该都可以正常运行。

点击了解更多《白话设计模式 28 讲》

第01课:生活中的监听模式——一坑爹的热水器
  • 用程序来模拟生活
  • 从剧情中思考监听模式
    • 监听模式
  • 监听模式的模型抽象
    • 代码框架
    • 类图
    • 基于框架的实现
    • 模型说明
      • 设计要点
      • 推模型和拉模型
  • 应用场景

【故事剧情】

刚刚大学毕业的 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("水已烧开!可以用来饮用了。")

测试代码不用变。自己跑一下,会发现输出结果和之前的是一样的。

模型说明

设计要点

在设计观察者模式的程序时要注意以下几点:

  1. 要明确谁是观察者谁是被观察者,只要明白谁是关注对象,问题也就明白了。一般观察者与被观察者之间是多对一的关系,一个被观察对象可以有多个监听对象(观察者)。如一个编辑框,有鼠标点击的监听者,也有键盘的监听者,还有内容改变的监听者。
  2. Observable 在发送广播通知的时候,无须指定具体的 Observer,Observer 可以自己决定是否要订阅 Subject 的通知。
  3. 被观察者至少需要有三个方法:添加监听者、移除监听者、通知 Observer 的方法;观察者至少要有一个方法:更新方法,更新当前的内容,作出相应的处理。
  4. 添加监听者、移除监听者在不同的模型称谓中可能会有不同命名,如观察者模型中一般,addObserver,removeObserver;在源-监听器(Source/Listener)模型中一般是 attach/detach,应用在桌面编程的窗口中,还可能是 attachWindow/detachWindow,或 Register/UnRegister。不要被名称迷糊了,不管他们是什么名称,其实功能都是一样的,就是添加/删除观察者。
推模型和拉模型

观察者模式根据其侧重的功能还可以分为推模型和拉模型。

推模型:被观察者对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。一般这种模型的实现中,会把被观察者对象中的全部或部分信息通过 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 引用去取数据。

应用场景

  1. 对一个对象状态或数据的更新需要其他对象同步更新,或者一个对象的更新需要依赖另一个对象的更新;
  2. 对象仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节,如消息推送。

学习设计模式,更应该领悟其设计思想,不应该应该局限于代码的层面。 观察者模式还可以用于网络中的客户端和服务器,比如手机中的各种 App 的消息推送,服务端是被观察者,各个手机 App 是观察者,一旦服务器上的数据(如 App 升级信息)有更新,就会被推送到手机客户端。在这个应用中你会发现服务器代码和 App 客户端代码其实是两套完全不一样的的代码,它们是通过网络接口进行通迅的,所以如果你只是停留在代码层面是无法理解的!

点击了解更多《白话设计模式 28 讲》

第02课:生活中的适配模式——身高不够鞋来凑
  • 用程序来模拟生活
  • 从剧情中思考适配器模式
    • 适配器模式
    • 设计思想
  • 适配器模式的模型抽象
    • 类图
  • 模型说明
    • 设计要点
    • 优缺点
      • 适配器模式的优点
      • 适配器模式的缺点
  • 实战应用
  • 应用场景

【故事剧情】

晚上九点半,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 接口。

模型说明

设计要点

适配器模式中主要三个角色,在设计适配器模式时要找到并区分这些角色:

  • 目标(Target): 即你期望的目标接口,要转换成的接口。
  • 源对象(Adaptee): 即要被转换的角色,要把谁转换成目标角色。
  • 适配器(Adapter): 适配器模式的核心角色,负责把源对象转换和包装成目标对象。

优缺点

适配器模式的优点
  • 可以让两个没有关联的类一起运行,起着中间转换的作用。
  • 提高了类的复用。
  • 灵活性好,不会破坏原有的系统。
适配器模式的缺点
  • 如果原有系统没有设计好(如 Target 不是抽象类或接口,而一个实体类),适配器模式将很难实现。
  • 过多地使用适配器,容易使代码结构混乱,如明明看到调用的是 A 接口,内部调用的却是 B 接口的实现。

实战应用

有一个电子书阅读器的项目(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 文件

应用场景

  • 系统需要使用现有的类,而这些类的接口不符合现有系统的要求。
  • 对已有的系统拓展新功能时,尤其适用于在设计良好的系统框架下增加接入第三方的接口或第三方的 SDK 时。

点击了解更多《白话设计模式 28 讲》

第03课:生活中的状态模式——人与水的三态
第04课:生活中的单例模式——你是我生命的唯一
第05课:生活中的职责模式——我的假条去哪了
第06课:生活中的中介模式——找房子问中介
第07课:生活中的代理模式——帮我拿一下快递
第08课:生活中的装饰模式——你想怎么穿就怎么穿
第09课:生活中的工厂模式——你要拿铁还是摩卡
第10课:生活中的迭代模式——下一个就是你了
第11课:生活中的组合模式——自己组装电脑
第12课:生活中的构建模式——想要车还是庄园
第13课:生活中的克隆模式——给你一个分身术
第14课:生活中的策略模式——怎么来不重要,人到就行
第15课:生活中的命令模式——大闸蟹,走起
第16课:生活中的备忘模式——好记性不如烂笔头
第17课:生活中的享元模式——颜料很贵必须充分利用
第18课:生活中的外观模式——学妹别慌,学长帮你
第19课:生活中的访问模式——一千个读者一千个哈姆雷特
第20课:生活中的设计模式——与经典设计模式的不解渊源
第21课:生活中的设计模式——那些未完待续的设计模式
第22课:深入解读过滤器模式——制作一杯鲜纯细腻的豆浆
第23课:深入解读对象池技术——共享让生活更便捷
第24课:深入解读回调机制——把你技能亮出来
第25课:谈谈我对设计模式的理解
第26课:谈谈我对设计原则的思考
第27课:谈谈我对项目重构的看法
附录:Python 中 Metaclass 的原理

阅读全文: http://gitbook.cn/gitchat/column/5b26040ac81ac568fcf64ea3

从生活中领悟设计模式(Python)相关推荐

  1. 如何从生活中领悟设计模式

    文章推荐 Selenium 自动化测试从零实战[阅读原文] 原来这样做,才能向架构师靠近[阅读原文] Cordova App 打包全揭秘[阅读原文] TensorFlow on Android:物体识 ...

  2. 生活中的设计模式——启程之前,请不要错过我

    两年前CSDN出一个产品叫ink,旨在提供一个高质量写作环境.那时就有写这一系列的想法了,而且也确实写了,就在ink里写了三篇文章,后来不知道因为什么原因这个产品下架了,我的三篇文章也没了,这事也就一 ...

  3. 【转】第00课导读:生活中的设计模式——启程之前,请不要错过我

    为什么叫设计模式 什么是设计模式 设计模式与生活有什么联系 为什么要学设计模式 如何进行学习 为什么选择 Python 弥补市场空缺 大势所趋,Python 已然成风 简单的 Python 基础 Py ...

  4. 导读:生活中的设计模式——启程之前,请不要错过我

    为什么叫设计模式 什么是设计模式 设计模式与生活有什么联系 为什么要学设计模式 如何进行学习 为什么选择 Python 弥补市场空缺 大势所趋,Python 已然成风 简单的 Python 基础 Py ...

  5. python能在生活中做什么-Python可以解决哪些生活中的小问题

    Python作为人工智能类的热门编程语言,近些年越来越受到关注,尤其是随着机器人.语音识别等人工智能技术的发展,Python成为继Java.C.C++之后排名第四的编程语言.但大多数人对于Java.C ...

  6. python能在生活中做什么-Python能在生活中做什么

    很多人学习Python就是为了在人工智能.大数据等领域谋求一份高薪工作,Python其实很接地气,我们如果学习了Python而不把它作为一种谋生手段也可以在生活中解决很多问题,那么Python能在生活 ...

  7. python能在生活中做什么-python能做哪些生活有趣的事情

    躺着赚钱 一位匿名知乎网友爆料用Python写了自动化交易程序,2年躺着赚了200万!相当于普通程序员10年的工资,此刻的心情...你懂的! 不过,这位大侠的真实身份也被网友找出了,真是人红了想低调都 ...

  8. 生活中,会Python爬虫也能赚钱

    在下写了5,.6年Python,期间写了各种奇葩爬虫,挣各种奇葩的钱,写这篇文章总结下几种爬虫挣钱的方式. 1.最典型的就是找爬虫外包活儿. 这是网络爬虫最通常的的挣钱方式,通过外包网站,熟人关系接一 ...

  9. python算法基础设计模式,python常见的设计模式

    Python有设计模式么 Python设计模式主要分为三大类:创建型模式.结构型模式.行为型模式;三 大类中又被细分为23种设计模式,以下这几种是最常见的. 单例模式:是一种常用的软件设计模式,该模式 ...

  10. 生活中的适配器模式——身高不够鞋来凑

    [故事剧情] 晚上九点半,Tony上了地铁,准备回家,正巧还有一个空位,赶紧走向前坐下.工作一天后,疲惫不堪的他正准备坐着打个盹小睡一会.这时进来两个小姑娘,一个小巧可爱,一个身姿曼妙:嬉笑地聊着天走 ...

最新文章

  1. mysql 插入指定值_mysql实现随机把字段值插入指定表
  2. python统计单元测试代码覆盖率
  3. QT中关于头文件一个很奇怪的问题
  4. 运维工程师 | 交换机堆叠
  5. python 计时_Python计时相关操作详解【time,datetime】
  6. 使用Java 8防止日志过宽
  7. PHP发送数据到指定方法,php通过header发送自定义数据方法_php技巧
  8. 【Elasticsearch】请在64位平台上使用Lucene的MMapDirectory
  9. windows 设置ssh登录
  10. 阿里AI获NLP顶会比赛冠军,达摩院计划推出首个通才型AI医生
  11. Futura字体和Logo设计实践
  12. pyplot输出的绘图界面出现中文乱码的解决方案
  13. Socket+华为云 实现广域网五子棋在线对战
  14. 关于微信小程序,你不知道的那些事
  15. 详析:谷歌为何紧急封杀阿里云OS?
  16. C语言必会100题(2)。用*号输出字母C的图案/输出特殊图案/输出9*9口诀/输出国际象棋棋盘/打印楼梯,同时在楼梯上方打印两个笑脸
  17. Spire.XLS:一款Excel处理神器(2)
  18. Jexl表达式引擎(2)
  19. 显示商品信息(java web)
  20. 【Windows11系统更新后蓝牙没了】

热门文章

  1. 通过 SiteServer CMS 推进政府网站集约化、集群化建设
  2. windows update 离线包下载
  3. 什么是SoC?什么是IP核?它们有什么关系?
  4. 新华linux桌面操作系统3.0
  5. 安徽省滁州市谷歌卫星地图下载
  6. java zoom,进口javazoom不能得到解决
  7. 制造企业发展遇瓶颈?低代码平台助其逆风翻盘!
  8. imb服务器怎么拆硬盘,IBM P750更换本地硬盘
  9. CSDN新手机号绑定不成功,提示已存在账号,CSDN换绑手机号的相关问题
  10. 大华linux密码,大华ME-S-S系列双SD卡车载DVR