面向对象程序设计 - oceanbei333/leetcode GitHub Wiki

面向对象程序设计

面向对象程序设计(Object Oriented Programming)作为一种新方法,其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。模型是用来反映现实世界中事物特征的。任何一个模型都不可能反映客观事物的一切具体特征,只能对事物特征和变化规律的一种抽象,且在它所涉及的范围内更普遍、更集中、更深刻地描述客体的特征。通过建立模型而达到的抽象是人们对客体认识的深化。

面向对象的好处

根据面向对象的特征,我们可以总结如下:

1)对象易于理解和抽象,面向对象很容易把现实世界反映到计算机领域,从而方便设计。

2)更加容易重用代码:只要使用继承就可以,使用父类的方法,只要使用多态,就可以使用相同的代码处理不同类型的对象

3)具有可扩充性和开放性:

4)代码易于阅读

5)代码容易维护

面向过程与面向对象

1. 面向过程

面向过程是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。

img

举个例子,下五子棋,面向过程的设计思路是首先分析解决这个问题的步骤:

(1)开始游戏(2)黑子先走(3)绘制画面(4)判断输赢(5)轮到白子(6)绘制画面(7)判断输赢(8)返回步骤(2) (9)输出最后结果。

面向过程始终关注的是怎么一步一步地判断棋局输赢的,通过控制代码,从而实现函数的顺序执行。

2. 面向对象

在日常生活或编程中,简单的问题可以用面向过程的思路来解决,直接有效,但是当问题的规模变得更大时,用面向过程的思想是远远不够的。所以慢慢就出现了面向对象的编程思想。世界上有很多人和事物,每一个都可以看做一个对象,而每个对象都有自己的属性和行为,对象与对象之间通过方法来交互。面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个对象在整个解决问题的步骤中的属性和行为。

img

在下五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:

(1)黑白双方,这两方的行为是一样的。

(2)棋盘系统,负责绘制画面

(3)规则系统,负责判定犯规、输赢等。

然后赋予每个对象一些属性和行为:

(4)第一类对象(黑白双方)负责接受用户输入,并告知第二类对象(棋盘系统)棋子布局的变化,棋盘系统接收到了棋子的变化,并负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

可以看出,面向对象是以功能来划分问题,而不是以步骤解决。比如绘制画面这个行为,在面向过程中是分散在了多个步骤中的,可能会出现不同的绘制版本,所以要考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘系统这个对象中出现,从而保证了绘图的统一。

3. 优缺点比较

面向过程

优点:

流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析。

效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。

缺点:

需要深入的思考,耗费精力,代码重用性低,扩展能力差,后期维护难度比较大。

面向对象

优点:

结构清晰,程序是模块化和结构化,更加符合人类的思维方式;

易扩展,代码重用率高,可继承,可覆盖,可以设计出低耦合的系统;

易维护,系统低耦合的特点有利于减少程序的后期维护工作量。

缺点:

开销大,当要修改对象内部时,对象的属性不允许外部直接存取,所以要增加许多没有其他意义、只负责读或写的行为。这会为编程工作增加负担,增加运行开销,并且使程序显得臃肿。

性能低,由于面向更高的逻辑抽象层,使得面向对象在实现的时候,不得不做出性能上面的牺牲,计算时间和空间存储大小都开销很大。

面向对象的特征

封装

封装性很重要,它可以避免外部对象修改对象内部的状态,从而引起对象本身的稳定性,因此在代码编写过程中,要尽量考虑到对象的每个属性,不需要暴露的,尽量不要暴露。

我们可以封装“类”的属性(比如身家、年龄等),也可以封装“类”的方法(比如说如何赚钱、如何消磨时间)

而面向对象的类封装了属性后,对属性的修改只能通过类的方法进行,一来不会暴露内部的具体属性;二来对属性的操作都是统一的,不会出现乱改的情况。 封装方法的主要原因是“隔离复杂度”。每个类只需要关注自己负责的功能如何完成即可,如果需要其他类配合,只需要调用类的方法即可,而不需要了解其他类功能的具体实现。

继承

继承性,使不同的类,有相同的方法,这样可以最大程度地重用代码。

多态

多态屏蔽了子类对象的差异,使得调用者可以写出通用性的代码,而无须针对每个子类来写不同的代码。

UML

UML,Unified Modeling Language,中文翻译为“统一建模语言”。

UML是一种是面向对象软件的标准化建模语言

用例图

用例图是通过用例来展现系统能力的

用例图作用

● 设计师对系统要完成的职责有了整体的理解,为后续的分析和设计打下基础; ● 测试人员可以基于用例图和用例来设计验收的测试用例; ● 开发人员、运维人员、资料人员都能够通过用例图对系统有一个初步的了解; ● 客户可以基于用例图整体评估其需求是否被正确地理解,是否有遗漏。

用例图的构成

用例图包含三个主要的元素:角色、系统、用例。

![截屏2020-08-24 上午11.17.40](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 上午11.17.40.png)

● 角色:图中的小人即角色; ● 系统:图中的边框即系统;

● 用例:图中的椭圆即用例。

类图

类图,顾名思义,就是表示类的图。类图主要包含两部分:类定义和类关系。 ● 类定义:用来表示某个类; ● 类关系:用来表示类之间的关系。

类定义图

类定义图包含三部分:类名称、属性区、方法区,正好与代码层面的类结构一一对应。

![截屏2020-08-24 上午11.21.02](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 上午11.21.02.png)

类关系图

继承

在UML图中,通过一条带空心三角形箭头的连接线来表示继承,三角形指向的一端是父类,另一端是子类。

![截屏2020-08-24 下午12.29.50](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午12.29.50.png)

关联

关联,顾名思义,就是指两个类之间有关系和联系。

在UML中类的关系有如下几种:继承/实现关系、依赖关系、组合关系、聚合关系,关联关系。

关联关系

“关联关系”是最弱的一种关系。

你和公司签订了劳动合同,那么合同类、公司类、员工类就有关联关系了

![截屏2020-08-24 下午12.33.27](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午12.33.27.png)

依赖

依赖是比关联更强的一种关系,一个类“依赖”了另外一个类,就意味着一旦被依赖的类发生改变,则依赖类也必须跟着改变。

![截屏2020-08-24 下午12.34.38](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午12.34.38.png)

常见的依赖关系

  1. 方法调用

    这种依赖关系是Client调用了Supplier的方法,也就是使用了Supplier的服务。

  2. 数据依赖

    这种依赖关系是Client使用了Supplier的属性,也就是使用了Supplier的数据。

  3. 对象依赖

    对象依赖是指Client需要创造Supplier对象,将其返回给其他类。

组合&聚合

聚合和组合关系的关注点在于:它们是一种“整体-部分”的关系。

聚合——是一种“has a”的关系,即:某个类包含另外一个类,但并不负责另外类的维护,且两个类的对象生命周期是不一样的,“整体”销毁后,“部分”还能继续存在。 组合——是一种“owns a”的关系,即:某个类包含另外一个类,且还要负责另外类的维护,且两个类的对象生命周期是一样的,“整体”销毁后,“部分”同样被销毁了。

在 UML 中,聚合是通过一条带空心菱形的连接线来表示的,菱形所在的一端是“整体”,另一端是“部分”。组合的表达方式和聚合基本一致,只是菱形由空心改为实心的。

![截屏2020-08-24 下午12.39.33](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午12.39.33.png)

动态图

在 UML 中使用动态图来描述一个系统的动态行为,动态图包括“状态图”、“活动图”、“序列图”、“协作图” 4种。

序列图

序列图主要用于描述对象按照时间顺序组织的消息交互过程,其关键特征是强调按照“时间顺序”来组织对象的交互,所以序列图有时又称为“时序图”或者“顺序图”。

序列图的元素如下。

【对象】 对象通过一个带纵向时间线的矩形来表示,矩形里面显示类的名称(见图10-33)。

【控制焦点】 控制焦点是时间线上表示“时间段”的符号,在这个时间段内对象将执行相应的操作,即:对象处于激活状态。 控制焦点用小矩形表示,如图10-34中的“1:文档准备”。

【消息】 消息是两个对象之间交互的具体内容,包括自关联消息、请求消息、返回消息。 “自关联消息”代表对象本身的一个处理,用一条指向自身的带箭头的实线连接线表示,

“请求消息”代表请求方发送给接收方的消息,使用一条带箭头的实线连接线表示,箭头一端代表接收方,另一端代表发送方,

“返回消息”和“请求消息”正好相反,代表了接收方返回给请求方的响应,使用一条带箭头的虚线连接线表示,箭头的一端代表请求方,另一端代表接收方,

【对象终止】 对象终止指对象被销毁,使用一个打叉的符号来表示,

![截屏2020-08-24 下午12.43.31](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午12.43.31.png)

面向对象分析和设计全流程概述

面向对象的技术流程可以概括如下: 需求模型→领域模型→设计模型→实现模型 (1)需求模型 通过和客户沟通,结合行业经验和知识,明确客户的需求。 (2)领域模型 基于需求模型,提炼出领域相关的概念,为后面的面向对象设计打下基础。 (3)设计模型 以领域模型为基础,综合面向对象的各种设计技巧,完成类的设计。 (4)实现模型 以设计模型为基础,将设计模型翻译为具体的语言实现,完成编码。

需求模型

需求即功能?

我们来看一个简单的例子:ATM(自动取款机)。 有的人说,ATM的功能是取款、存款、查询余额,所以针对ATM的需求应该是:取款、存款、查询余额。 有的人说,ATM的功能有很多,如识别卡、密码认证、点钞、验钞、查询余额、跨行取款等,所以针对ATM的需求应该是:识别卡、密码认证、点钞、验钞、查询余额、跨行取款。 如果你是ATM购买商,你认为哪种才是你的需求? 如果你是ATM制造者,你认为哪种才是你的需求? 如果你是ATM使用者,你认为哪种才是你的需求?

需求:对客户来说有价值的事情。 功能:系统为了实现客户价值而提供的能力。

需求分析的方法

“需求分析518方法”,具体来说,就是5W1H8C,

5W,即When、Where、Who、What、Why。

1H,即How。 8C,即8个Constraint,包括性能(Performance)、成本(Cost)、时间(Time)、可靠性(Reliability)、安全性(Security)、合规性(Compliance)、技术性(Technology)、兼容性(Compatibility)。

5W

1)When 这是和时间相关的环境信息,常见的时间信息有 ● 季节信息:春、夏、秋、冬等; ● 日期信息:节日、假日等; ● 作息时间:白天、晚上、凌晨、早晨、上午、下午、晚上、深夜等。 (2)Where 这是和地点相关的环境信息,常见的地点信息有 ● 国家、地区:不同的国家和地区有不同的文化、风俗、制度等; ● 室内、室外、街道; ● 建筑物。 (3)Who 这是和参与者相关的信息。注意,这里是用“参与者”来描述的,而不是说“人”,为什么呢? 因为很多外部参与者不一定是“人”,例如外系统、动物等都可以算作Who里面的。

(4)What 这个就是客户最终想要的输出。例如一个文档、一份报告、一张图片、一个系统等。 一般情况下,这也是我们看到的最原始的需求。 (5)Why 这个就是客户遇到的问题、困难、阻碍等,也是客户提出需求的驱动力。 只要是客户觉得不爽的地方都属于Why的范围。

一个长颈鹿的实例:

有一个建筑公司的需求分析人员收到了一个客户需求“给我建一栋很大的房子”,于是建筑公司就建了房子,房子是欧式风格,又大又宽敞,全套宜家家居,全木地板,进口电器……简直是应有尽有,结果客户来收房子的时候说了一句话,让建筑公司吐血,你知道是什么话吗? 客户说:“先生们,我是要一栋房子给长颈鹿住!” 因此,即使是简单的一句话需求,我们也需要详细分析。例如: Who,这栋房子的购买者是动物园,管理者是动物园的饲养员,使用者是长颈鹿,评估者可能是动物管理协会、卫生局等政府部门。 When,这个可能要求一年四季了,如果长颈鹿只是运来展览一下,那么就是展览的这几个月。 Where,这栋房子要建在动物园里,而不是其他居民小区,那么动物园肯定有一些相关的规定。 What,要求一栋房子,但不是简单意义上的房子,而是长颈鹿住的房子,这就需要考虑高度、围栏等。 Why,这个就有可能是动物园要临时展览,也有可能是要引进长颈鹿,还有可能是原来的长颈鹿房子破旧了。 虽然我们前面讲的时候,对5W都是一视同仁的,但实际上有一个W是非常关键的,如果这个W错了,那么即使其他4W全部正确,也可能导致最后没有满足客户需求,客户不满意。 这个最关键的W就是Why,因为只有真正了解了客户的需求驱动力,才能解决客户的问题;而只有解决了客户的问题,客户才会真正的满意。这也是我们前面提到的需求分析的终极目标——“挖掘客户的问题,实现客户价值”!

1H

很多人常犯的一个错误是在需求分析阶段分析了需求如何实现,这样做是不正确的。需求分析阶段的How不是指如何实现需求,而是指需求本身的流程

举个例子:取款是一个需求,但取款本身包含多次交互,要插卡、输入密码、输入金额、打印账单、取钱这些步骤,How就是用来描述这整个流程是如何运行的。 说起来How很简单,但实际上这是需求分析工作量最大的一部分,How分析的结果是需求分析的主要输出,而且How分析的质量直接影响最后需求实现的质量。

可以使用用例方法确定需求的流程

8C

8C指的是8个约束和限制(Constraint)

(1)性能Performance 性能是指系统提供相应服务的效率。一般而言,性能主要包括响应时间、吞吐量。 性能是很多系统架构设计的关键约束之一。例如,同样一个Web网站,虽然都是提供信息给用户游览,但一个日访问量1万的网站,和日访问量10亿的网站,两者的设计是完全不一样的。 (2)成本Cost 成本是指为了实现系统而需要付出的代价。 成本也是很多系统架构设计的关键约束之一。例如,客户只愿意出100万来买这个系统,但我们却设计了一个耗费1000万的系统,要么客户不愿意买,要么我们自己亏损。无论哪种情况,最后都是我们赔本了。 (3)时间Time 时间是指客户要求什么时候交付。 (4)可靠性Reliability 可靠性是指系统长时间正确运行的能力,例如银行、证券、电信等公司,对宕机时间要求是很严格的。 (5)安全性Security 安全性是指对信息安全的保护能力,例如涉及钱、身份证、社会保险号等的需求对这个要求很高。 (6)合规性Compliance 合规性是指满足各种行业标准、法律法规、规范等,例如3C、SOX、3GPP、ITUT等。 (7)技术性Technology 有的客户可能要求我们采用某种技术,例如客户现在使用的都是Windows机器,那么就可能要求我们基于Windows平台开发。 (8)兼容性C

兼容性是指我们的产品或系统与客户已有的产品或系统的兼容能力。要知道现在很少有产品是孤立运行的,特别是在大企业、大公司中,多个系统都是相互交互、相互配合的。新的系统必须能够和已有的系统配合,否则将无法运行。

用例方法

用例是用来描述需求的流程

用例方法三段法(NEA方法)如下所述。 (1)正常处理(Normal) 通过和客户沟通,分析需求的正常流程。 (2)异常处理(Exception) 在正常处理流程的步骤上,分析每一步的各种异常情况和对应的处理。 (3)替代处理(Alternative) 在正常处理流程的步骤上,分析每一步是否有其他替代方法,以及替代方法如何做。

用例的具体写法

一个完整的用例应该包含如下几个部分。

【用例名称】 一般情况下,用例的名称即需求的名称。 【场景】 场景即用例发生的环境,正好对应5W中的3个W:Who、Where、When。 【用例描述】 描述详细的用例内容,对应5W中的What和How,即用户应该怎样做,以及每个步骤中的输出。但并不要求每个步骤都一定有输出,可以有也可以没有,也可以有多个。 【用例价值】 描述用例对应的客户价值,对应5W中的Why。 【约束和限制】 即整个需求流程中相关的约束和限制条件,对应518方法中的8C。

我们来看一个简单的样例:POS机。

第一步:正常处理

【用例名称】 买单 【场景】 Who:顾客、收银员; Where:商店的收银台; When:营业时间。 【用例描述】 1.顾客携带选择好的商品到收银台; 2.收银员逐一扫描商品条形码,系统根据条形码查询商品信息; 3.扫描完毕,系统显示商品总额,收银员告诉顾客商品总额; 4.顾客将钱交给收银员; 5.收银员清点钱数,输入收到的款额,系统给出找零的数目; 6.收银员将找零的钱还给顾客,并打印小票; 7.买单完成,顾客携带商品和小票离开。 【用例价值】 顾客买完单以后,就可以携带商品离开了,而商店也将得到收入。 【约束和限制】

1.POS机必须符合国标XXX; 2.键盘使用中文,因为收银员都是中国人; 3.一次买单数额不能超过99999RMB; 4.POS机要非常稳定,至少一天内不要出现故障。

第二步:异常处理

【用例名称】 买单 【场景】 【用例描述】 2.收银员逐一扫描商品条形码,系统根据条形码查询商品信息;2.1 扫描仪坏了,必须支持手工输入条形码;2.2 商品的条形码无法扫描,必须支持手工输入条形码;2.3 条形码能够扫描,但查询不到信息,需要收银员和顾客沟通,放弃购买此商品。 4.顾客将钱交给收银员;4.1 顾客的钱不够,顾客和收银员沟通,删除某商品;4.2 顾客的钱不够,顾客和收银员沟通,删除某类商品中的一个或几个(例如买了5包烟,去掉两包);4.3 顾客觉得某个商品价格太高,要求删除某商品。 【用例价值】 【约束和限制】

例如系统坏了应该怎么处理。 但实际上我们没有必要这么写,因为用例分析的目的是为了详细分析系统如何做才能够实施用户的价值,如果系统本身都坏了,这个就不是用例关注的内容了。 用例分析中的“异常”是指流程的异常情况,而不包含系统本身的异常。

第三步:替代处理

在第二步的基础上,我们增加替代处理,即有的步骤可以换一种方式来实现。例如,如下用例中的付款方式,可以有信用卡支付、会员卡支付、购物卡支付等。 【用例名称】 买单 【场景】 Who:顾客、收银员; Where:商店的收银台; When:营业时间。 【用例描述】 4.顾客将钱交给收银员; 4.1 顾客的钱不够,顾客和收银员沟通,删除某商品; 4.2 顾客的钱不够,顾客和收银员沟通,删除某类商品中的一个或几个(例如买了5包烟,去掉两包);

4.3 顾客觉得某个商品价格太高,要求删除某商品。 4-A:顾客使用信用卡支付 4-A.1 信用卡支付流程(请读者自行思考完善,可以写在这里,如果内容太多,也可以另外写一个子用例) 4-B:顾客使用购物卡支付 4-B.1 购物卡支付流程 4-C:顾客使用会员卡积分支付 4-C.1 会员卡积分支付流程

最终的用例

【用例名称】 买单 【场景】 Who:顾客、收银员; Where:商店的收银台; When:营业时间。 【用例描述】 1.顾客携带选择好的商品到收银台; (这一步没有异常) 2.收银员逐一扫描商品条形码,系统根据条形码查询商品信息; 2.1 扫描仪坏了,必须支持手工输入条形码; 2.2 商品的条形码无法扫描,必须支持手工输入条形码; 2.3 条形码能够扫描,但查询不到信息,需要收银员和顾客沟通,放弃购买此商品。 3.扫描完毕,系统显示商品总额,收银员告诉顾客商品总额; (这一步没有异常) 4.顾客将钱交给收银员; 4.1 顾客的钱不够,顾客和收银员沟通,删除某商品; 4.2 顾客的钱不够,顾客和收银员沟通,删除某类商品中的一个或几个(例如买了5包烟,去掉两包);

4.3 顾客觉得某个商品价格太高,要求删除某商品。 4-A:顾客使用信用卡支付 4-A.1 信用卡支付流程(请读者自行思考完善,可以写在这里,如果内容太多,也可以另外写一个子用例) 4-B:顾客使用购物卡支付 4-B.1 购物卡支付流程 4-C:顾客使用会员卡积分支付 4-C.1 会员卡积分支付流程 5.收银员清点钱数,输入收到的款额,系统给出找零的数目; (这一步没有异常) 6.收银员将找零的钱还给顾客,并打印小票; 7.买单完成,顾客携带商品和小票离开。 【用例价值】 顾客买完单以后,就可以携带商品离开了,而商店也将得到收入。 【约束和限制】 1.POS机必须符合国标XXX; 2.键盘使用中文,因为收银员都是中国人; 3.一次买单数额不能超过99999RMB; 4.POS机要非常稳定,至少一天内不要出现故障。

功能

有了用例之后,提取功能可以说是一件水到渠成的事情,基本上只是一项文字工作,我们只需要将用例中那些需要系统完成的事情——更简单地说,是动词——提取出来,就成为了系统的功能

![截屏2020-08-24 下午1.30.04](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午1.30.04.png)

用例中同一个功能在不同的步骤中可能出现了多次(例如“手工输入条形码”、“删除某商品”),最后提取的时候要合并。

系统顺序图

SSD(System Sequence Diagram,注意不要与SSD硬盘混淆),中文翻译为“系统顺序图”,主要用于描述在某个用例的某个分支场景下,外部参与者与系统的交互过程。简单来说,SSD就是用例的可视化描述。

![截屏2020-08-24 下午1.33.26](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午1.33.26.png)

领域模型

在需求分析阶段是不区分面向对象还是面向过程的,从领域模型开始,我们就开始了面向对象的分析和设计过程,可以说,领域模型是完成从需求分析到面向对象设计的一座桥梁。

领域模型是对领域内的概念类或现实世界中对象的可视化表示,又称概念模型、领域对象模型、分析对象模型。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。 从这个定义我们可以看出,领域模型有两个主要的作用。 ● 发掘重要的业务领域概念; ● 建立业务领域概念之间的关系。

领域建模的三字经方法:找名词、加属性、连关系。

找名词

POS机领域类,详细如下: 顾客、收银员、商品、扫描仪、钱、信用卡、会员卡、小票、买单、键盘、屏幕

加属性

找出领域模型的名词后,接下来的一个重要工作就是将这些名词相关的属性找出来,使其更加准确,如表5-1所示。 但加属性和找名词有一点点差别:有的属性并没有在用例中明确给出,需要分析人员和设计人员额外添加,此时也是分析师的行业和领域经验起决定作用。

![截屏2020-08-24 下午1.38.07](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午1.38.07.png)

连关系

![截屏2020-08-24 下午1.38.58](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午1.38.58.png)

设计模型

完成领域类到软件类的转换,这就是面向对象领域设计阶段的主要任务。

第一步:领域映射

领域模型中的“领域类”,是设计模型中“软件类”最好的来源。

类筛选

“软件类”是软件系统内部的一个概念,而领域类是业务领域的概念,并不是每个领域类最终都会体现在软件系统中。

以 POS 机的领域类为例。领域类“顾客”不需要转换为软件类,因为顾客是 POS 机业务领域的一个重要参与者,但并不是 POS 机内部需要实现的一个实体,

经过筛选后,剩下的领域类就需要都转换为软件类,具体如下: 收银员、商品、交易、小票、支付、信用卡、会员卡、现金、购物卡

提炼方法

找动词,筛选提炼,并分配对特定的软件类

● 增加商品:很明显应该分配给“交易”类; ● 计算商品总额:分配给“交易”类; ● 显示商品总额:分配给“交易”类; ● 删除商品:分配给“交易”类; ● 现金支付:分配给“现金”类; ● 信用卡支付:分配给“信用卡”类; ● 购物卡支付:分配给“购物卡”类; ● 会员卡积分支付:分配给“会员卡”类;

第二步: 应用设计原则和设计模式

设计原则主要用于指导“类的定义”的设计,而设计模式主要用于指导“类的行为”的设计。更通俗一点讲,设计原则是类的静态设计原则,而设计模式是类的动态设计原则。

设计原则

![截屏2020-08-24 下午1.56.47](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午1.56.47.png)

仔细观察我们通过领域类映射得到的软件类,可以发现一个很明显的不符合SOLID原则中的DIP原则的地方,即:“交易”类直接依赖“会员卡”、“购物卡”、“信用卡”、“现金”4个子类,这样的实现不符合DIP原则,当需要增加新的支付方式时,“交易”类也需要跟着修改。

![截屏2020-08-24 下午1.57.13](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午1.57.13.png)

设计模式

通过分析应用设计原则优化后的类,我们发现“信用卡”这个类存在优化的空间,因为国际上存在不同的信用卡,最常见的有中国银联(UnionPay)、Visa、MasterCard这几种,每种信用卡在支付的时候都需要接入不同的机构,其接入方式和协议肯定都是有一定差异的。为了封装这种差异以支持后续更好地扩展,我们应用设计模式的Bridge模式,提取出“信用卡处理”类,

![截屏2020-08-24 下午1.58.34](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-24 下午1.58.34.png)

设计原则

内聚

内聚指一个模块内部元素彼此结合的紧密程度。

判断一个模块(函数、类、包、子系统)“内聚性”的高低,最重要的是关注模块的元素是否都忠于模块的职责,

内聚的分类

偶然内聚

模块内部的元素之所以被划分在同一模块中,仅仅是因为“巧合”

这是内聚性最差的一种内聚,从名字上也可以看出,模块内的元素没有什么关系,元素本身的职责也各不相同。基本上可以认为这种内聚形式实际上是没有内聚性的。 但在实际应用中,这种内聚也是存在的,最常见的莫过于类似“Utils”这样的包,

逻辑内聚

模块内部的元素之所以被划分在同一模块中,是因为这些元素逻辑上属于同一个比较宽泛的类别 模块的元素逻辑上都属于一个比较宽泛的类别,但实际上这些元素的职责可能是不一样的。例如,将“鼠标”和“键盘”划分为“输入”类,将“打印机“、“显示器”等划分为“输出”类

时间内聚

模块内部的元素之所以被划分在同一模块中,是因为这些元素在时间上是相近的。

例如“异常处理”操作,一般的异常处理都是“释放资源”(例如打开的文件、连接、申请的内存)、“记录日志”、“通知用户”,那么把这几个处理封装在一个函数中,

过程内聚

过程内聚和时间内聚比较相似,也是在函数级别的模块中比较常见。例如读/写文件操作,一般都是按照这样的顺序进行的:判断文件是否存在、判断文件是否有权限、打开文件、读(或者写)文件,那么把这些处理封装在一个函数中,它们之间的内聚就是“过程内聚”

虽然过程内聚和时间内聚看起来比较类似,但其实它们有一个非常核心的差别:时间内聚中元素的顺序不是固定的,可以随意调整;而过程内聚中元素的先后顺序是严格要求的,不能轻易调整。例如,在“时间内聚”章节中提到的异常处理,我们完全可以调整一下顺序:

信息内聚

模块内部的元素之所以被划分在同一模块中,是因为这些元素都操作相同的数据。

信息内聚最典型的例子莫过于“增、删、改、查”某个数据了,

顺序内聚

模块内部的元素之所以被划分在同一模块中,是因为某些元素的输出是另外元素的输入。

顺序内聚其实就像一条流水线一样,上一个环节的输出是下一个环节的输入。最常见的就是“规则引擎”一类的处理,一个函数负责读取配置,将配置转换为执行指令;另一个函数负责执行这些指令。

功能内聚

模块内部的元素之所以被划分在同一模块中,是因为这些元素都是为了完成同一个单一任务

功能内聚是内聚性最好的一种方式,但在实际操作过程中,对于是否满足功能内聚并不能很好地判断出来,原因在于“同一个单一任务”这个定义是比较模糊的。

关键就在于“都是”这个核心点,英文是“all contribute to”,即:所有元素都是为了同一个任务,缺一不可。按照这个标准,我们就可以将“功能内聚”和“过程内聚”、“顺序内聚”等区分开来。例如,在“过程内聚”章节中提到的读取文件的样例就不符合“all contribute to”的定义,因为“checkFileExist”、“checkFilePrivilege”、“openFile”这些方法并不是只为“readFile”而设计的,这些方法同样可以为“写入文件”、“删除文件”等任务服务。

耦合

耦合(或者称依赖)是程序模块相互之间的依赖程度。

耦合和内聚是相反的:内聚关注模块内部的元素结合程度;耦合关注模块之间的依赖程度。

耦合的分类

无耦合

无耦合意味着模块间没有任何关系或者交互。

完全无耦合的模块虽然不受其他模块的影响,但同时也失去了重用其他模块的机会,每个轮子都要自己发明,这样的效率非常低下。

消息耦合

模块间的耦合关系表现在消息传递上。

系统/子系统——两个系统交互的接口,比如HTTP接口、Java RPC接口等。

消息耦合是一种耦合程度很低的耦合,是比较理想的耦合,因为调用方仅仅依赖被调用方的“消息”,既不需要传参数,也不需要了解被调用方的内部逻辑,更不需要控制调用方内部的逻辑。

数据耦合

两个模块间通过参数传递基本数据,称为数据耦合

这里有两点需要特别关注,这也是数据耦合区别于其他耦合类型的关键特征。 ● 通过参数传递,而不是通过全局数据、配置文件、共享内存等其他方式; ● 传递的是基本数据类型,

数据结构耦合

两个模块通过传递数据结构的方式传递数据,称为数据结构耦合

控制耦合

当一个模块可以通过某种方式来控制另一个模块的行为时,称为控制耦合。

最常见的控制方式是通过传入一个控制参数来控制函数的处理流程或者输出,例如常见的工厂类。

外部耦合

当两个模块依赖相同的外部数据格式、通信协议、设备接口时,称为外部耦合。

全局耦合

当两个模块共享相同的全局数据时,称为全局耦合

内容耦合

当一个模块依赖另一个模块的内部内容(主要是数据成员)时,称为内容耦合。内容耦合是最差的一种耦合方式,因此它有另外一个很形象的名称:病态耦合(Pathological coupling)。

高内聚低耦合

对于内聚来说,最强的内聚莫过于一个模块只完成一项功能,例如一个类只写一个方法,这样内聚性绝对是最高的。但这会带来一个明显的问题:模块的数量急剧增多,这样就导致了其他模块的耦合特别多,于是整个设计就变成了“高内聚高耦合”了。由于高耦合,整个系统变动同样非常频繁。 同理,对于耦合来说,最弱的耦合是一个模块完成所有的功能,例如一个类将所有的方法都包含了,这样的类完全不依赖其他类,耦合性是最低的。但这样会带来一个明显的问题:内聚性很低,于是整个设计就变成“低耦合低内聚”了。由于低内聚,整个系统的变动同样非常频繁。 对于“低耦合低内聚”来说,还有另外一个明显的问题:几乎无法被其他类重用。原因很简单,类本身太庞大了,要么实现很复杂,要么数据很大,其他类无法明确该如何重用这个类。 所以,内聚和耦合两个属性,排列组合一下,只有“高内聚低耦合”才是最优的设计。

类设计原则

SRP,Single Responsibility Principle 单一职责原则

● 职责是站在他人的角度来定义的; ● 职责不是一件事,而是很多事情,但这些事情都是和职责紧密相关的。

SRP 不能应用于聚合类,那么如何保证聚合类的设计质量呢?即:优先使用对象组合,而不是类继承。

OCP,Open-Closed Principle,开闭原则

对扩展开放,对修改封闭。

当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。

1. 对软件测试的影响

软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。

2. 可以提高代码的可复用性

粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。

3. 可以提高软件的可维护性

遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护

可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

LSP,Liskov Substitution Principle 里氏替换原则

子类必须能替换成它们的父类。

● 子类必须实现或者继承父类所有的公有方法,否则调用者调用了一个父类中有,而子类中没有的方法,运行时就会出错; ● 子类每个方法的输入参数必须和父类一样,否则调用父类的代码不能调用子类; ● 子类每个方法的输出(返回值、修改全局变量、插入数据库、发送网络数据等)必须不比父类少,否则基于父类的输出做的处理就没法完成

ISP,Interface Segregation Principle,接口隔离原则

客户端不应该被强迫去依赖它们并不需要的接口

ISP原则承认对象需要非内聚接口,然而ISP原则建议客户端不需要知道整个类,只需要知道具有内聚接口的抽象父类即可。

DIP,Dependency Inversion Principle,依赖反转原则

● 高层模块不应该直接依赖低层模块,两者都应该依赖抽象层; ● 抽象不能依赖细节,细节必须依赖抽象

如何应用设计原则

(1)SRP原则:用于类的设计 当我们想出一个类,或者设计出一个类的原型后,使用SRP原则核对类的设计是否符合SRP要求。 (2)OCP原则:总的指导思想 OCP原则是一个总的指导思想,在面向对象的设计中,如果能够符合LSP/ISP/DIP原则,一般情况下就能够符合OCP原则了。 除了在面向对象的软件设计中应用外,OCP也可以用于指导系统架构设计,例如常见的CORBA、COM协议,其实都可以认为是OCP原则的具体应用和实现。 (3)LSP原则:用于指导类继承的设计 当我们设计类之间的继承关系时,使用LSP原则来判断这种继承关系是否符合LSP要求。 (4)ISP原则:用于指导接口的设计 ISP原则可以认为是SRP原则的一个变种,本质上和SRP的思想是一样的。SRP用于指导类的设计,而ISP用于指导接口的

(5)DIP原则:用于指导如何抽象 当我们设计类之间的依赖关系时,可以使用DIP原则来判断这种依赖是否符合DIP原则。DIP原则和LSP原则相辅相成:DIP原则用于指导抽象出接口或者抽象类,而LSP原则用于指导从接口或者抽象类派生出新的子类。

设计模式

对变化的概念进行封装

找到变化,封装变化

“找到变化”解决了“在哪里”使用设计模式的问题

“封装变化”解决了“为什么”使用设计模式的问题

在实际应用的时候,我们不要一开始就想着要把某个模式塞到某个地方,而是先找到可能变化的地方,再来看具体使用哪个模式可以封装这种变化。

工厂模式

工厂”表示一个负责创建其他类型对象的类。通常情况下,作为一个工厂的类有一个对象以及与它关联的多个方法。客户端使用某些参数调用此方法,之后,工厂会据此创建所需类型的对象,然后将它们返回给客户端。

工厂具有下列优点。 松耦合,即对象的创建可以独立于类的实现。 客户端无需了解创建对象的类,但是照样可以使用它来创建对象。它只需要知道需要传递的接口、方法和参数,就能够创建所需类型的对象了。这简化了客户端的实现。 可以轻松地在工厂中添加其他类来创建其他类型的对象,而这无需更改客户端代码。最简单的情况下,客户端只需要传递另一个参数就可以了。 工厂还可以重用现有对象。但是,如果客户端直接创建对象的话,总是创建一个新对象。

简单工厂模式

![截屏2020-08-25 下午2.43.13](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午2.43.13.png)

客户端类使用的是Factory类,该类具有create_type()方法。当客户端使用类型参数调用create_type()方法时,Factory会根据传入的参数,返回Product1或Product2。

工厂方法模式

我们定义了一个接口来创建对象,但是工厂本身并不负责创建对象,而是将这一任务交由子类来完成,即子类决定了要实例化哪些类。 Factory方法的创建是通过继承而不是通过实例化来完成的。 工厂方法使设计更加具有可定制性。它可以返回相同的实例或子类,而不是某种类型的对象(就像在简单工厂方法中的那样)。

![截屏2020-08-25 下午2.47.45](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午2.47.45.png)

抽象工厂模式

抽象工厂模式的主要目的是提供一个接口来创建一系列相关对象,而无需指定具体的类。工厂方法将创建实例的任务委托给了子类,而抽象工厂方法的目标是创建一系列相关对象。

![截屏2020-08-25 下午2.49.09](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午2.49.09.png)

![截屏2020-08-25 下午2.50.43](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午2.50.43.png)

Decorator模式

动态地给一个对象添加一些额外的职责。

![截屏2020-08-25 下午1.47.44](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午1.47.44.png)

Facade模式

为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

封装了外部模块与内部模块之间的调用关系,

首先,Facade模块对外提供统一的接口服务,所有的外部模块都只需要知道Facade模块并与之通信即可,这样使得新增外部模块的时候,不会导致整个系统的依赖关系变得更复杂。 其次,Facade模块依赖内部模块完成具体的接口功能,当内部模块发生变更时,只需要修改Facade模块,外部模块并不需要修改,外部模块甚至都不知道内部模块已经修改了。 Facade模块可以针对不同的接口采取不同的处理方式。 (1)转发方式 Facade模块收到外部模块的请求后,直接转发给对应的内部模块处理,然后将内部模块的处理结果返回。 例如:登录模块只需要获取用户基本信息即可,Facade 类可以将请求直接转发给UserBasicInfo类处理。 (2)组合方式 Facade模块收到外部模块的请求后,请求多个内部模块处理,最后将多个内部模块的处理结果组合起来返回给外部模块。 例如:广告模块同时需要用户基本信息和用户兴趣爱好信息,Facade 模块可以组合UserBasicInfo和UserHobby提供的信息,一次返回给广告模块。

![截屏2020-08-25 下午1.51.04](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午1.51.04.png)

Observer模式

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

● 发布,又可以称为通知,即:当某个对象的状态发生变化时,需要将变化的事件通知其他对象,就像某厂商开发了一款新产品,需要“广而告之”一样。我们称发出通知的对象为“发布者”。 ● 订阅,又可以称为观察,即:某个对象对某个“发布者”感兴趣,需要观察或者监视发布者的相关状态变化。当发布者发出通知后,订阅者需要做出相应的响应。我们称响应通知的对象为“订阅者”或者“观察者”。

![截屏2020-08-25 下午1.53.12](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午1.53.12.png)

 命令模式——封装调用

命令模式通常使用以下术语:Command、Receiver、Invoker和Client:

Command对象了解Receiver对象的情况,并能调用Receiver对象的方法; 调用者方法的参数值存储在Command对象中; 调用者知道如何执行命令; 客户端用来创建Command对象并设置其接收者。

命令模式的主要意图如下: 将请求封装为对象; 可用不同的请求对客户进行参数化; 允许将请求保存在队列中 提供面向对象的回调。

命令模式可用于以下各种情景: 根据需要执行的操作对对象进行参数化; 将操作添加到队列并在不同地点执行请求; 创建一个结构来根据较小操作完成高级操作。

![截屏2020-08-25 下午2.53.49](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午2.53.49.png)

 模板方法模式——封装算法

通过一种称为模板方法的方式来定义程序框架或算法。例如,你可以将制作饮料的步骤定义为模板方法中的算法。模板方法模式还通过将这些步骤中的一些实现推迟到子类来帮助重新定义或定制算法的某些步骤。这意味着子类可以重新定义自己的行为。

模板方法模式适用于以下场景: 当多个算法或类实现类似或相同逻辑的时候; 在子类中实现算法有助于减少重复代码的时候; 可以让子类利用覆盖实现行为来定义多个算法的时候。

![截屏2020-08-25 下午2.56.17](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午2.56.17.png)

桥接模式

桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。

**意图:**将抽象部分与实现部分分离,使它们都可以独立的变化。

**主要解决:**在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。

**何时使用:**实现系统可能有多个角度分类,每一种角度都可能变化。

**如何解决:**把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。

**关键代码:**抽象类依赖实现类。

**应用实例:**墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。

优点: 1、抽象和实现的分离。 2、优秀的扩展能力。 3、实现细节对客户透明。

**缺点:**桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。

使用场景: 1、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。 2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。

**注意事项:**对于两个独立变化的维度,使用桥接模式再适合不过了。

![截屏2020-08-25 下午3.17.15](/Users/isea/Library/Application Support/typora-user-images/截屏2020-08-25 下午3.17.15.png)

建造者模式

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

**意图:**将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。

**主要解决:**主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。

**何时使用:**一些基本部件不会变,而其组合经常变化的时候。

**如何解决:**将变与不变分离开。

**关键代码:**建造者:创建和提供实例,导演:管理建造出来的实例的依赖关系。

应用实例: 1、去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。 2、JAVA 中的 StringBuilder。

优点: 1、建造者独立,易扩展。 2、便于控制细节风险。

缺点: 1、产品必须有共同点,范围有限制。 2、如内部变化复杂,会有很多的建造类。

使用场景: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。

**注意事项:**与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。

参考目录

面向对象葵花宝典:思想、技巧与实践

精通设计模式