设计模式笔记(一)– 必备知识

本系列讲解设计模式,本文是第一篇,介绍学习设计模式的一些必备知识。


说明

学习本系列,您首先得有基本的编程经验,其次要对面向对象有初步的了解,本系列不讲算法(有机会可以出一个系列),不创建应用,只关注如何创建可重用、可扩展的面向对象软件。


什么是设计模式

软件设计中经常有重复的代码,设计模式就是优雅解决重复问题的方案。

例如,我们经常有重复撤销(undo)的操作,有几种不同模式可以实现undo操作,其中之一是备忘录模式(memento pattern)。

设计模式定义了我们如何构建类以及这些类应该如何相互通信。

在本系列中,我们将探索 23 种设计模式,这些模式最初记录在 90 年代的《设计模式-可重用的面向对象软件元素》一书中,该书由四位被称为“四人帮”(GOF, gang of four)的作者撰写,所以我们经常将设计模式称为“四人帮”模式。

23种设计模式分3种类型,创造模式、结构模式以及行为模式。

创造模式是有关创建对象的各种模式,结构模式用于表示对象之间的关系,行为模式则是有关对象间的交互和通信。我们将首先介绍行为设计模式。

需要知道的是,23种设计模式并非包含了这个世界的全部设计模式,还有很多非官方和未记录的设计模式。

学习设计模式有什么好处?

首先我们将学会如何设计可重用、可扩展和可维护的软件;其次可以方便与同行交流,比如有人跟你讨论是否可以用备忘录模式解决问题,你就能马上理解什么意思,而无需对方画出UML说明设计意图;其三,可以让我们更能理解各种框架,目前流行的各类框架背后都有设计模式在支撑,理解了设计模式,你会觉得框架的设计很熟悉,只是语言有差异而已。


如何学习本系列

介绍设计模式的教程,一般是列出一种模式,UML图,然后进行讲解。本系列稍有不同,将尝试探索设计的艺术,理解“四人帮”的设计思路。

本系列使用Java语言,读者当然可以用Python或C# 等自己喜欢的语言,我们的焦点是面向对象的设计艺术而不是具体的语言。下一节开始我们会简单介绍下Java,以便更好地理解面向对象的封装、抽象、继承和多态性。


JAVA基本设置

首先选择Java的编辑器,这里用的是vscode。如果想使用大名鼎鼎的IntelliJ IDEA,可以移步到https://www.jetbrains.com/idea/ 下载安装。

我的操作系统环境是ubuntu20.04,设置步骤是:

安装JDK并检查安装成功:

sudo apt install openjdk-8-jdk 

java -version

安装微软Java插件:Extension Pack for Java

创建Java项目根目录比如JavaProjects,然后vscode打开该目录。

command+shift+p打开命令控制板,搜索选择 Java:Create Java Project,新建项目“DesignPatterns”。

设置了包名为top.kelemi,可以是任意的,根据Java约定,包一般是倒装域名,这里也遵循。 我们看到,有一个与文件同名的类Main,一个静态的public方法main,返回值为void,参数是字符数组args,用户输入的命令行参数可以被args捕捉并使用。

类体和函数体用花括号包起来,在Java中,左花括号一般放在同一行的左侧,不像C#那样另起一行。


我们在Main类同目录下创建User类,设置属性name,方法sayHello,并创建构造函数User,Java中构造函数与类同名。然后再在Main类中调用。

完成编码后,按下Command+F5运行查看结果。


耦合

面向对象有一个重要概念是耦合,它表示一个类对其他类的依赖程度。

Main类依赖User类,如果User类有修改,比如增加一个age属性,Main类也会受其影响,我们必须重新编译和重新部署。

当前我们的程序只有两个类,修改还比较容易,但实际项目有几百上千个类,如果耦合紧密,修改就很困难了。

举个例子,我们的车子如果爆胎了,更换备胎即可,无需修改发动机、底盘,说明汽车与轮胎是松散耦合。

我们希望应用程序也类似,各子组件相对独立。如何做?

使用接口!


接口

接口是最容易被误解的结构之一。

接口是一份约定,指定类应提供什么功能。

比如,开餐馆招聘厨师,实际上需要的是厨师这个角色,而不是具体的某个人,不管是John或Mary,只要他们是厨师就可以。

这就是接口的意义,通过接口我们可以构建松散耦合的应用程序。

我们使用代码实现。

假设我们要设计一个税收计算器。税收计算器是很复杂的,每一年都是不同的,我们创建一个2022年的税收计算器,又要创建一个2023年的。可以创建接口TaxCalculator,约定一个方法calculateTax(),2022年和2023年的税收计算器类都实现该接口。

在main函数中,我们使用接口TaxCalculator代替具体的TaxCalculator2022类。这样当具体类有变化时,main函数不会受此影响,耦合变宽松了。

细心的读者可能已发现了,Main类中的getTaxCalculate()使用了具体类TaxCalculate2022,使得Main类依赖了该具体类,会导致难以修改。这只是一个简单的例子,实际项目不会这么做,会使用“依赖注入”获取接口的具体类。

我们已介绍了类和接口,接下来就来介绍面向对象的4个核心原则:封装、抽象、继承、多态性。


封装

看个例子。

创建一个Account类,代表银行的账户,有一个公共属性balance表示余额,接着在Main类中调用获取或设置余额。

Main类中可以设置余额balance为负数,这明显有问题,所以我们应该设置不允许外部修改Account类的balance属性。

我们将balance属性改成private,同时添加setBalance和getBalance方法,隐藏内部的细节,并对外部的输入的进行验证,防止我们的对象变成不正常状态。这就是封装原则。

实际情况不会直接设置余额setBalance,而是存款deposit和取款withdraw操作,我们修改下。


抽象

抽象就是通过隐藏不必要的细节来减少复杂性。

比如我们要创建给用户发送邮件的类MailService,有sendEmail(), connect(), disconnect(), authenticate()等方法,对用户而言,他们关心的只有sendEmail()方法,至于connect,disconnect,authenticate等实现细节并不关心。所以我们应该将这些不必要的细节隐藏,将其变成private。

利用抽象原则,用户将更容易使用,在Main中使用MailService,我们就看到sendMail,使用自然非常容易。更重要的是,如果MailService里的private方法后期进行更改比如connect方法添加参数timeout,Main类将不会受任何影响。


继承

通过继承我们可以重用代码。

假设我们创建一个UI框架,有TextBox,Button,CheckBox等,这些UI组件有一些共同的方法,比如enable,focus,setPosition等,通过创建父类UIControl将这些共同的方法提取出来,可以减少代码的重复。


多态性

面向对象最后一个原则是多态性,它表示对象可以呈现出多种形态。

仍以前面的UI框架为例,每一个部件如TextBox、CheckBox都可以在屏幕上渲染出相应的形状。

我们给UIControl添加一个抽象(abstract)方法,同时也需要将UIControl变成抽象类,抽象类及抽象方法可以认为是一个半成品,不能由其生成实例,需要由继承它的类实现它定义的抽象方法,然后才可生成实例。

再在Main类调用画出UI,我们看到,同样的draw方法根据不同的对象画出不同的形状。

Screenshot

UML

UML是统一建模语言(unified Modeling Language)的简写,是一种可视化的建模语言,我们用它表示类以及类之间的关系。

下面是类的一个例子。

矩形最上方是类名,中间是属性,下面是方法,在UML图中,可以表示属性的类型、方法的返回值等,上图的 + 表示public, – 表示private。在本系列的各设计模式中,类的属性默认是private,方法则是public,我们将省略 + – 符号。

类之间的关系一般有3种。

首先是继承关系,用实线箭头表示。

再是组成关系,用钻石+箭头表示。

最后是依赖关系,用虚线箭头表示。当某个类没有其他类的成员,但有依赖,比如参数,本地变量是其他类,就用这种表示。

我们在本系列用到的类之间的关系主要就是这3种。


小结

本文是设计模式系列的第一篇,介绍了学习设计模式的必备知识。

下一篇将正式开始介绍各种设计模式。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注