课件
前言
编程实际就是一项实践活动,学习编程最好的方法就是学中做和做中学,具体来说,可以用看、问、练这三个字来概念学习编程的关键环节:
- 看:经常阅读优秀的代码,学习别人的代码,这是提高开发人员修行的一种捷径,从这些代码中吸取牛人们的技术精髓。
- 问:https://qithub.com/seais/seais/issues/545 ,善于发现问题,并以专业的方式提出问题,通过对问题求解的交流来扩展自己的编程学习,初学编程者可能经常遇到如何正确提问这样的困惑,这里我们给出一个参考的网页,它说的是如何向开源社区提问题,以及如何用更专业的方式更好地提出自己的问题。
- 练:亲自动手编写代码。不动手的学习是徒劳无用的,提升编程能力最有效的方法就是实践、实践、再实践。
软件开发的工程思维
程活动都是有相通性的,先来看一下高楼大厦是如何建成的:
在建楼之前首先认真地分析问题,要明确盖什么样的楼以及在哪里盖等等,然后会做一个初步的总体设计,还有可能建造一个实体的模型进行评估和测试,在完成整个设计方案之后,我们先用钢筋搭建一个整体框架,然后再一层一层、一块一块地去建造,最后测试和验收。
软件工程也是一项解决问题的工程活动,首先要对问题有一个分析的过程,由于很多问题非常复杂,就需要把这些复杂的问题分解成更容易处理的若干小问题,然后针对这些不同的问题设计相应的解决方案,再把所设计的小的构件组合形成一个完整的系统,这个过程也是很有挑战性的,需要应用抽象的方法进行分解和合成。
在程序设计的过程中实现这种分解和合成有一些通用的原则,应用这些原则可以增加代码的可修改性和可测试性,便于程序的扩展和维护,这一讲将介绍模块化设计、面向抽象编程以及错误和异常处理这三种编程实践,并结合一个实例说明如何更好地设计Python程序。
模块化设计
模块化的设计思想在工程领域中应用非常广泛,它的目的就是降低系统构造的复杂性(设计中组件的嵌套也是同样的道理),增加灵活性。
我们都知道活字印刷术,它可以称得上是古代产品领域的一种模块化设计,单个的汉字作为一个文字的“模块”,它本身是有自己的特定含义,不同的汉字通过语法这个“接口”进行连接可以进一步构成一个更复杂的文章“产品”。
在现代建筑领域中,模块化设计也用于建造各种住宅和办公建筑,这里显示的是迪拜集团在伦敦西部建立的第一家模块化酒店,它把建筑体分成若干个空间模块,精装修甚至清洁都是在工厂里完成,然后运到工地像搭积木一样来搭建房屋,这样做的好处在于建造的房屋更加高效灵活,维护的时候可以单独地替换一个单元而不会影响到其它单元的使用。
模块化程序设计
同样的道理,程序的模块化设计就是把一个很大的程序,按照功能分拆成一系列小的模块,其中每一个模块相对独立、功能单一、结构清晰、接口简单,各个模块之间通过接口进行调用,这样做可以有效地降低程序设计的复杂性,提高模块的可靠性和可复用性,有助于实现快速地开发,方便维护和功能扩展。
模块化设计需要对程序进行分解,针对不同的系统可能有不同的分解策略,对于一个WEB应用系统来说,我们可以按照系统业务类型进行水平划分,将其分成用户管理、权限管理以及其他不同的业务功能模块,也可以按照系统的层次进行垂直的划分,把它分成应用服务、业务逻辑、数据存储等典型的多层结构。
我们还可以通过区别容易改变和不容易改变的部分来划分模块,将稳定、不常变化的代码抽象到一个模块里,比如系统中比较通用的基础的功能,将经常变化的代码放在另外的一个或多个模块里,比如业务的逻辑,因为易变的代码经常修改会很不稳定,分开之后,这些代码在修改的时候就不会影响到那些不变的代码。
模块化设计也可以基于单一职责进行划分,就是把代码按照职责抽象出来形成不同的模块,再把同一职责的代码放在一个模块里。这是一个瑞士军刀,它大概有十几种刀,还有mp3的功能,显然它不是基于单一职责的,虽然在野外瑞士军刀方便携带但并不代表它很好用。类和函数是程序的基本构件,通过把一个大的程序分解成若干相对独立的类或函数,可以使程序的结构清晰容易理解,对于类或者函数的设计,可以基于单一职责进行分解,就是应该只做一件事并且做好这件事(犹如工厂上的流水线,每人专职做自己的事),但是在这里单一职责并不等同于单一功能,而是指引起变化的原因,也就是说从变化的角度去考虑,一个类或者函数应该有且只有一个引起它变化的原因,按照这个原则,应该避免编写万能的类和庞大的函数。
Python的模块化设计
Python程序的模块元素主要包括函数、类、模块、包四种类型,每个Py文件就是一个模块,其中还可以导入其它的模块,一些模块还可以作为启动程序独立运行。
康威生命游戏
下面我们将结合一个生命游戏的例子来讲解一下如何对其进行模块化设计。
生命游戏是英国数学家康威在1970年发明的细胞自动机,它是假设有这么一个二维矩形的世界,这个世界里面每一个方格都居住着一个细胞,细胞本身是有生和死两种状态,随着时间的变化,每个方格里面的细胞它的生死也会发生变化,一个细胞在下一个时刻的生死,是由它周围8个方格里面的细胞状态来决定的:
- 如果一个细胞的周围至少有四个活的细胞,那么下一个时刻它就会因为拥挤而死亡;
- 如果这个细胞的周围正好有三个活的细胞,那么下一个时刻它也会成为一个活的细胞;
- 如果正好是有两个活的细胞,那么它的状态是不会发生变化的;
- 如果只有一个活的,或者甚至没有活的细胞,那么它就会因为孤独而死亡;
康威生命游戏维基百科链接:https://zh.wikipedia.org/wiki/%E5%BA%B7%E5%A8%81%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F
这个游戏是在一个类似于围棋棋盘一样的、可以无限延伸的二维方格网中进行,在游戏的开始,每个细胞都可以随机地或者被规定的设为一个生或者死的状态,之后细胞就按照前面给出的规则进行生息行繁,这里显示的是维基百科上面的两个动态图,现在我们来分析一下生命游戏这个例子:
首先细胞是在二维方格网中生长变化,所以像地图这样一些数据是一个要素,其次细胞的变化是有一定规则的,按照这样的规则来控制细胞的变化,也就是游戏的规则,因此游戏需要有一个控制逻辑,最后整个细胞的变化是和时间有关系的,那么就需要一个计时器。
根据前面的分析我们可以把生命游戏的三个要素设计成三个不同的模块,每个模块都有各自单一的职责,再通过一定的方式相互地连接和调用,这样生命游戏就被划分成地图模块、逻辑模块和计时模块三个部分:
- 地图模块:管理和地图相关的一切数据的初始化、获取和更新;
- 逻辑模块:控制完整的游戏逻辑,按照游戏的规则对地图数据进行改变;
- 计时模块:负责和时间相关的功能,在适当的时机触发,逻辑模块的更新;
这种划分是把生命游戏这样一个大问题分解成一些更容易处理的小问题,显然每个模块实现起来更加的简单、清楚,而且易于测试和扩展,比如可以修改游戏规则、改变更新频率、设置不同的地图数据等。
上面给出的只是一种方案,实际上你还可以设计其他不同的方案,只要符合设计质量原则就可以,例如你可以增加一个UI模块,专门负责游戏的图形化显示。
面向抽象编程
完成模块的设计之后,我们就要设计这些模块之间的连接部分。当然连接的设计要抽象的合适,以便降低在未来开发中发生变化的可能。面向抽象编程就是把模块的行为抽象出来形成相对固定的接口,这样不论对接口的实现是如何变化,只要接口不变,就不会影响使用接口的模块。这就好像木工里边的榫,它并不关心不同部件的实现方式,而是通过连接部分把不同的部件严密地结合在一起,形成一个完整的产品。
首先我们来看地图模块,游戏逻辑对地图的操作包括初始化、重置地图再随机设定活细胞、获取某一个方格周围的活细胞数量、更新或者读取方格的细胞状态等,因此需要在地图模块定义这些关键的方法。
对于逻辑模块,首先是要在主程序中进行实例化,在计时器触发的时候进行游戏的循环,即根据当前状态更新地图的下一时刻的状态,还要输出当前的地图,所以这里我们定义了逻辑模块的相应方法。
最后是计时模块,它也要在主程序中进行实例化,关键是设定触发的间隔秒数,另外还有一个触发函数,最后还需要启动计时器。计时器在启动之后就会按照设定的间隔时间持续地触发,这里我们给出了相关方法的定义。
完成模块化设计以及关键方法的定义之后,开发人员就可以分别实现各个模块,同时模块测试的设计工作也可以开始进行,需要强调的是各个模块对外提供的服务,也就是那些关键方法,不应该发生变化,否则会连带产生其它调用模块的修改,给整个程序的构造带来不稳定。
错误与异常处理
程序可能会出现错误,例如Python的解析错误或者未捕获的异常这种运行时的错误等,如果程序能够对运行时出现的可预见的错误进行处理。就会避免让用户感到困惑的问题,常见的异常有很多比如被零除、断言错误、操作系统错误、输入输出错误等。
程序的异常处理是用于管理程序运行期间错误的一种常用方法,我们已经见过很多产品的报错处理,对错误和异常处理并不陌生:
Python程序异常处理
Python程序异常处理是用try except来捕获并处理的,编写代码的时候我们要注意细化具体的异常类型,有针对性地进行处理,同时还应该控制try和except的代码块的大小,代码块越大就容易出现预期之外的异常。此外,在代码实现的关键部分还要养成检查变量合法性的习惯,避免错误的数据在未来积累成了更大的错误,那样可能会大大增加错误定位的难度,很难进行调试。
生命游戏这个程序一旦开始运行就会无休止地运行下去,我们需要有一种方式让它安全地停止。目前我们是用命令行的方式来运行,用控制台来打印输出,所以可以通过常见的Ctrl+C的方式来终止程序,但是这样会在Python里边引发异常,这是一种可预见的异常,我们需要捕获和处理它。
要避免Ctrl+C报错,我们可以在启动部分编写异常处理代码,另外也应该在关键的部分进行变量合法性检查,比如地图的初始化就应该检查输入参数是不是合法,这样可以避免程序在某个奇怪的地方出错。
到这里我们已经完成了生命游戏的问题分析,模块和抽象设计,以及异常处理的部分实现,剩下的部分就由同学们按照我们给出的编程规范高质量地完成这个游戏的编程实现,最后再进行有效的测试。