2.1 知识储备
序言
设计模式是很多人运用面向对象思想分析、解决问题时总结出来的可复用、可提效的一套经验,在介绍设计模式的时候你会发现很多面向对象的思想在里面。使用好设计模式能够帮助我们设计出一套具备高复用性、高扩展性的应用,能够让我们的设计更加优雅、代码更具扩展性,能够更好的满足软件设计的某些特征。
本章节会带领大家复习一遍之前学过的知识,做好知识储备,开启学习设计模式的大门。
注意:本专栏所有代码实现都是由Java编写,建议非Java读者通过自己所熟悉的面向对象语言进行实现。
面向对象
设计模式是很多人运用面向对象思想分析、解决问题时总结出来的可复用、可提效的一套经验,在介绍设计模式的时候你会发现很多面向对象的思想在里面。所以在正式介绍设计模式之前,让我们重新温习一下面向对象的相关概念。
类和对象
在了解面向对象编程之前,先介绍2个基本概念:类和对象。
类:具有相同特性(属性)和相同行为(方法)的对象的抽象。例如:猫是一个类,具有年龄、性别等属性,具有吃饭、睡觉等行为。
对象:对象是类的实例,有具体的特性和行为。例如:加菲猫是一个对象(猫类的实例)
// 定义猫类 class Cat { private int age; private String gender; public void eat() {} public void sleep() {} } Cat garfield = new Cat(); // 定义加菲猫对象 garfield.eat(); garfield.sleep();
面向对象编程
面向对象编程注重的是“对象”,会关注当前操作的对象。使用学生早晨起床上学作为示例介绍。此时我们应该定义一个学生类:
public class Student { private String name; public Student(String name) {this.name = name;} private void getUp() {} private void brushTeeth() {} private void washFace() {} private void eat() {} private void goToSchool() {} }
小明的顺序是先刷牙再洗脸,那么我们就实例化一个学生对象,名称为小明。让小明先刷牙再洗脸。
Student xiaoming = new Student("xiaoming"); xiaoming.brushTeeth(); xiaoming.washFace();
小红的顺序是先洗脸再刷牙,那么我们就实例化一个学生对象,名称为小红。让小红先洗脸再刷牙。
Student xiaohong = new Student("xiaohong"); xiaohong.brushTeeth(); xiaohong.washFace();
其实可以看到我们编排洗脸刷牙的顺序是只针对小明、小红的,这种就是面向对象编程,关注当前受影响的对象。
面向对象的三大特征
了解了面向对象的基本概念,下面就介绍一下面向对象的三大特征:封装、继承、多态。在具体介绍之前我们想一下面向对象语言为什么需要这三大特征,分别解决了什么问题。
首先我们看一下面向对象语言和面向过程语言的优缺点对比:
面向过程语言 | 面向对象语言 | |
---|---|---|
优点 | 性能高 | 易维护、易复用、易扩展、低耦合 |
缺点 | 维护性、复用性比面向对象语言差 | 性能比面向过程语言查 |
通过对比发现面向过程语言的维护性和复用性较差,所以面向对象语言则通过某些方式去提高代码的维护性和复用性。这某些方式就是面向对象的三大特征:封装、继承、多态。
(1)封装:封装就是类的属性和行为包装在一起,通过主动暴露接口提供给外部访问。封装保证了模块的独立性,提高了程序代码的维护性。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。
(2)继承:子类通过继承父类的行为,很好的实现了代码的可复用性。
(3)多态:是指允许不同对象对同一消息做出不同的响应,能够提高代码的扩展性。具体代码可以参考下文对多态的详细介绍
面试考点:面向对象的三大特征,并介绍相关概念
封装
封装就是将具有具有相同特性(属性)和相同行为(方法)的对象抽象成类。然后通过对属性、方法设置可见性,从而保证类的封装性与安全性。将类封装好之后,只对外暴露提供服务的接口即可,使用者只需知道类能做什么,怎么用即可,而不用关注具体的内部实现。例如:电工将电路封装好之后,只提供给你一个插孔,你只要将电器插入插孔即可工作,而无需关注内部电路的复杂逻辑。
Java中设置可见性的几个修饰符是:private、protected、public、默认(没有前3个关键词修饰即为默认)。修饰符和可见性的关系如下图(Yes表示对应的修饰符可被访问,例如:private修饰符可以同类代码访问,但是不能被同包代码访问):
修饰符 | 同类 | 同包 | 不同包的子类 | 不同包的非子类 |
---|---|---|---|---|
private | Yes | No | No | No |
protected | Yes | Yes | Yes | No |
public | Yes | Yes | Yes | Yes |
默认 | Yes | Yes | No | No |
即:
- 使用private修饰的变量或方法,只能同类下进行访问
- 使用protected修饰的变量或方法,同类、同包或者不同包的子类可以访问
- 使用public修饰的变量或方法,在任何位置都可以访问
- 默认可见性,则只能在同类或同包下进行访问
面试考点:Java中可见性修饰符及其作用域
继承
继承是就是子类自动拥有父类的所有行为和特性,可以在自己增加行为或特性进行拓展。
如下图“人类”是一个父类,拥有姓名、性别、年龄等基本属性,“学生”和“老师”都继承自“人类”,所以他们自动拥有了姓名、性别、年龄等属性,并且可以结合自身特点继续新增属性或行为。例如:学生拥有学号、班级等属性,老师拥有所教科目、教师编号等属性。
代码参考:
class Person { private String name; private String gender; private int age; } class Student extends Person { private String stuNumber; private String stuClass; } class Teacher extends Person { private String teachProject; private String teacherNumber; }
面试考点:Java可以多继承嘛?java类可以多继承嘛?java接口可以多继承嘛?Java一个类可以实现多个接口嘛?Java的接口和抽象类的区别是什么(jdk8带来什么变化,default关键字)?
了解了什么是继承之后,如果我们想让A类拥有B类的属性,那么就可以使用A继承B,则A类自动拥有了B类的属性。那还有没有其他方式呢?答案是肯定的,那就是使用组合,即让B作为A的一个属性。例如:人类拥有胳膊,那么就可以让胳膊类成为人类的一个属性。代码如下:
class Arm { private double length; // 胳膊长度 } class Person { private String name; private String gender; private int age; private Arm arm; }
那可不可以使用Person类继承Arm类呢?从语法上说当然是可以的,不会产生任何错误,但是从语义上就说不通了。因为继承表示的是“is-a”的关系,组合表示的是“has-a”的关系,即学生是人类,人类拥有胳膊。这从语义上是说的通的。如果使用Person类继承Arm类,那就表示人类是胳膊,这显然是说不通的。所以在使用继承和组合的时候需要区分场景,不要滥用。通常情况下:优先使用组合
那为什么优先使用组合,继承有什么问题嘛?
首先看一下我们上面的例子,学生类继承人类之后,就自动拥有了人类的所有特征和属性。此外学生类还应该有具有老师类的属性,例如班主任信息等。如果此时还想通过继承老师类,那是无法实现的,因为Java不允许多继承(当然在允许多继承的编程语言是可以这样做的,但是这样做真的好嘛?)
即使支持了多继承,依旧会存在一些问题。因为继承是全盘接收,学生类继承老师类是想要老师的基本信息,但是并不需要老师的授课、备课等行为,如果使用继承则会拥有老师的所有属性和行为,这显然是不合理的。
总结来说,继承有如下问题:
- 在单继承语言里面,无法通过继承获得多个类的重用
- 全盘接收父类属性和行为,语义不明。
多态
多态就是相同的抽象行为在不同的对象中的具体表现不同。例如:动物的一种行为就是叫,但是不同的对象叫声不一样。代码如下:
interface Animal { public void call(); } class Dog implments Animal { @Override public void call() { System.out.println("dog call:汪汪汪"); } } class Cat implments Animal { @Override public void call() { System.out.println("cat call:喵喵喵"); } } class Main { publci static void main(String[] args) { Animal cat = new Cat(); animalCall(cat); // cat call:喵喵喵 Animal dog = new Dog(); animalCall(dog); // dog call:汪汪汪 } public static void animalCall(Animal animal) { animal.call(); } }
在animalCall
方法中可以看到入参是Animal
类型,这样主要传入Animal
的子类就可执行对应子类的方法,提高了代码的扩展性。例如后续想新增牛叫,则新增一个Cattle
类,实现call
方法。然后实例化一个Cattle
对象传入animalCall
方法即可。(新增代码请自行完成)
面试考点:多态存在的条件
软件设计特征
设计模式能够帮助我们设计出一套具备高复用性、高扩展性的应用。熟练使用设计模式能够让我们的设计更加优雅、代码更具扩展性,能够更好的满足软件设计的某些特征。讲到这里首先让我们了解一下什么是软件设计的特征。
大家可以先想一下如果自己设计一套系统,应该具备哪些特征:
可用性
提供系统可用性,以保证用户对系统的使用体验。如果用户使用时系统产生系统不可用的情况,那必定一种糟糕的体验。衡量系统可用性的指标有这三个:MTTF、MTTR、MTBF。
MTTF(Mean Time To Failure):平均无故障时间,指的是系统从开始正常运行到出现故障的时间平均值
MTTR(Mean Time To Repair):平均修复时间,指的是系统从故障到修复完成的时间平均值
MTBF(Mean Time Between Failure):平均故障间隔,指的是系统两次故障直接的时间平均值
MTTF和MTBF的数值越高,MTTR的数值越小,表示系统的可用性越好。
复用性
我们正处于软件快速迭代的时期,从需求产生到系统上线速度越快越好,“拿来主义”正满足我们快速迭代的要求。但是“拿来主义”并不是直接的复制、粘贴其他系统的代码,而是要复用其他系统的代码或功能。当我们的设计有了足够高的复用性,当再有类似功能的时候,可以将之前的模块进行组装,从而快速完成项目迭代。
良好的复用性可以极大缩减项目开发的成本和周期。
扩展性
大到系统,小到需求,并不是一成不变的,总会随着用户需求的变更而变更。当产生某些变更的时候(可能是新增新功能,也可能是修改之前的某些功能),如果对你代码改动较大,说明系统的扩展性较差,反之说明系统的扩展性教强。
系统在设计的时候应该对扩展开发,对修改关闭,即开闭原则。(该原则会在第三章进行详细讲解)
易维护性
除了上文提到的需求变更,还少不了系统人员的流动,所以会有不同的人来维护之前的系统(也可能是自己一段时间后,维护自己前几个月的代码),如果在逻辑理解、代码风格等方面给维护人员带来困扰,则说明系统的维护性较差。
如何做
在系统设计的时候,要考虑复用性、扩展性,可维护性等多种情况,较为复杂。那如何才能轻松应对系统设计和代码开发呢?那就是好好了解设计模式。设计模式是很多人运用面向对象思想分析、解决问题时总结出来的可复用、可提效的一套经验。前人的经验能够在系统设计、代码开发上助我们一臂之力。
统一建模语言(UML)
在下面的内容中,会经常出现类图及其关联关系用来表示模型的内部结构和模型间的关系。所以我们提前了解一下类图和其中的符号概念。
一个普通类图如下,一共分为三部分。从上到下分别是:类名、属性和方法。
属性的格式:[+,-,#] [属性名] [:] [属性类型] [属性默认值]
方法格式:[+,-,#] [方法名] [方法入参] [:] [方法返回值]
- "+":表示public
- "-":表示private
- "#":表示protected
类图中类之间的关系主要包含:实现、继承、组合、聚合、依赖。表示符号如下所示:(TIPS:大家可以下载绘制UML图的工具,里面也会有相应的符号)
注意事项
上面带大家复习了一遍之前的知识,能够帮助我们更好的理解下面讲解的内容。上面提到了设计模式能给我们带来可复用、可扩展的设计,但是并不是所有场景都应该使用设计模式。有时候其实你的需求只是一个简单的功能,并没有可扩展的空间,如果此时还是大费周章的去进行过度设计,往往达到事倍功半的效果。
大家需要明白:设计模式并不一定使得你的可用性、扩展性大大增强。目前系统设计追求易维护性,千万不要为了用而用,如果使用不当,或者使用过度,往往给你的设计增加复杂性,提高系统的维护成本。
讲到这里,想到曾经我发起的一个讨论话题:如何界定前瞻性设计和过度设计。
其实我感觉他们的界定是视情况而定的不同场景的选择不一样,前瞻性设计就是适度的提高系统的扩展性,这些扩展是以后很大可能会出现的;过度设计就是过度重视系统扩展性,可能扩展的功能很少被使用,所以过度设计带来的就是复杂的设计,较小的收益。设计模式也是如此,我们不能学会了设计模式就一定要用,而且要懂得使用设计模式的场景,巧妙的使用设计模式。巧妙地使用设计模式做一些前瞻性设计,而非过度设计。