读薄《设计模式之美》-理论
# 面向对象编程
- 一种编程范式或编程风格。
- 以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
# 面向对象编程语言
面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
如何判定一个编程语言是否是面向对象编程语言?
按照严格的定义(语法支持四大特征),很多语言都不能算得上面向对象编程语言
按照不严格的定义(只以类或对象作为组织代码的基本单元)来讲,现在流行的大部分编程语言都是面向对象编程语言。
实际上,对于什么是面向对象编程、什么是面向对象编程语言,并没有一个官方的、统一的定义。向对象编程诞生开始,这两个概念就在不停地演化,所以,也无法给出一个明确的定义,也没有必要给出一个明确定义。
# 面向对象编程和面向对象编程语言之间有何关系?
面向对象编程一般使用面向对象编程语言来进行,但是,不用面向对象编程语言,我们照样可以进行面向对象编程。反过来讲,即便我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的。
# 面向对象分析和面向对象设计
面向对象分析就是要搞清楚做什么
面向对象设计就是要搞清楚怎么做
两个阶段最终的产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法、类与类之间如何交互等等。
# 什么是 UML?我们是否需要 UML?
UML(Unified Model Language),统一建模语言。
UML 在互联网公司的项目开发中,用处可能并不大。为了文档化软件设计或者方便讨论软件设计,大部分情况下,我们随手画个不那么规范的草图,能够达意,方便沟通就够了,而完全按照 UML 规范来将草图标准化,所付出的代价是不值得的。
# 四大特征
# 封装(Encapsulation)
封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
笔记
说白了就是,对象中的属性不允许随意被访问( private
),只能通过特定的方法( getId()
)访问,如果对象没有提供这样的方法,说明对应的属性是不允许访问的。
封装的意义是什么?它能解决什么编程问题?
如果我们对类中属性的访问不做限制,意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。
除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。
这就好比,如果一个冰箱有很多按钮,你就要研究很长时间,还不一定能操作正确。相反,如果只有几个必要的按钮,比如开、停、调节温度,你一眼就能知道该如何来操作,而且操作出错的概率也会降低很多。
# 抽象(Abstraction)
抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
在面向对象编程中,我们常借助编程语言提供的两种语法机制来实现抽象这一特性:
- 接口类(比如 Java 中的 interface 关键字语法)
- 抽象类(比如 Java 中的 abstract 关键字语法)
点击查看
public interface IPictureStorage {
void savePicture(Picture picture);
Image getPicture(String pictureId);
void deletePicture(String pictureId);
void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}
public class PictureStorage implements IPictureStorage {
// ...省略其他属性...
@Override
public void savePicture(Picture picture) { ... }
@Override
public Image getPicture(String pictureId) { ... }
@Override
public void deletePicture(String pictureId) { ... }
@Override
public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... }
}
调用者在使用图片存储功能的时候,只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑。
并不是说一定要为实现类(PictureStorage)抽象出接口类(IPictureStorage),才叫作抽象。即便不编写 IPictureStorage 接口类,单纯的 PictureStorage 类本身就满足抽象特性。
为什么抽象有时候会被排除在面向对象的四大特性之外?
抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供 “函数” 这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的 “特异性”,有时候并不被看作面向对象编程的特性之一。
抽象的意义是什么?它能解决什么编程问题?
- 在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节。而抽象作为一种只关注功能点不关注实现的设计思路,正好帮我们的大脑过滤掉许多非必要的信息。
- 很多设计原则都体现了抽象这种设计思想:
- 基于接口而非实现编程
- 开闭原则(对扩展开放、对修改关闭)
- 代码解耦(降低代码的耦合性)
- 等等
换一个角度来考虑,我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。
举个简单例子,比如
getAliyunPictureUrl()
就不是一个具有抽象思维的命名,因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作getPictureUrl()
,那即便内部存储方式修改了,我们也不需要修改命名。
笔记
在本书中,作者的观点,只要代码中用到 “函数” 这一语法机制,就可以认为具备抽象这一设计思想,因为对于调用函数者来说,他不需要关心函数内的实现;而接口类、抽象类这两个语法机制是丰富了抽象设计思想
# 继承(Inheritance)
继承是用来表示类之间的 is-a 关系,分为两种模式:单继承和多继承。
单继承表示一个子类只继承一个父类;比如猫是一种哺乳动物
多继承表示一个子类可以继承多个父类;比如猫既是哺乳动物,又是爬行动物;
继承主要是用来解决代码复用的问题。
继承存在的意义是什么?它能解决什么编程问题?
继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。
如果我们再上升一个思维层面,去思考继承这一特性,可以这么理解:我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 is-a 关系。我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。
继承的概念很好理解,也很容易使用。不过,过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看 “父类、父类的父类……” 的代码。还有,子类和父类高度耦合,修改父类的代码,会直接影响到子类。
所以,继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。我们应该尽量少用,甚至不用。关于这个问题,在后面讲到 “多用组合少用继承” 这种设计思想的时候,我会非常详细地再讲解,这里暂时就不展开讲解了。
# 多态(Polymorphism)
多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
public class DynamicArray {
private static final int DEFAULT_CAPACITY = 10;
protected int size = 0;
protected int capacity = DEFAULT_CAPACITY;
protected Integer[] elements = new Integer[DEFAULT_CAPACITY];
public int size() { return this.size; }
public Integer get(int index) { return elements[index];}
//...省略n多方法...
public void add(Integer e) {
ensureCapacity();
elements[size++] = e;
}
protected void ensureCapacity() {
//...如果数组满了就扩容...代码省略...
}
}
public class SortedDynamicArray extends DynamicArray {
@Override
public void add(Integer e) {
ensureCapacity();
int i;
for (i = size-1; i>=0; --i) { //保证数组中的数据有序
if (elements[i] > e) {
elements[i+1] = elements[i];
} else {
break;
}
}
elements[i+1] = e;
++size;
}
}
public class Example {
public static void test(DynamicArray dynamicArray) {
dynamicArray.add(5);
dynamicArray.add(1);
dynamicArray.add(3);
for (int i = 0; i < dynamicArray.size(); ++i) {
System.out.println(dynamicArray.get(i));
}
}
public static void main(String args[]) {
DynamicArray dynamicArray = new SortedDynamicArray();
test(dynamicArray); // 打印结果:1、3、5
}
}
多态这种特性也需要编程语言提供特殊的语法机制来实现。在上面的例子中,我们用到了三个语法机制来实现多态。
- 第一个语法机制是编程语言要支持父类对象可以引用子类对象,也就是可以将 SortedDynamicArray 传递给 DynamicArray。
- 第二个语法机制是编程语言要支持继承,也就是 SortedDynamicArray 继承了 DynamicArray,才能将 SortedDyamicArray 传递给 DynamicArray。
- 第三个语法机制是编程语言要支持子类可以重写(override)父类中的方法,也就是 SortedDyamicArray 重写了 DynamicArray 中的 add () 方法。
还有其他两种比较常见的的实现方式,
- 利用接口类语法
- 利用 duck-typing 语法(由于 Java 不支持此语法,这里就不展开了)
利用接口类来实现多态特性:
点击查看
public interface Iterator {
boolean hasNext();
String next();
String remove();
}
public class Array implements Iterator {
private String[] data;
public boolean hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}
public class LinkedList implements Iterator {
private LinkedListNode head;
public boolean hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}
public class Demo {
private static void print(Iterator iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
public static void main(String[] args) {
Iterator arrayIterator = new Array();
print(arrayIterator);
Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
}
}
在这段代码中,Iterator 是一个接口类,定义了一个可以遍历集合数据的迭代器。Array 和 LinkedList 都实现了接口类 Iterator。我们通过传递不同类型的实现类(Array、LinkedList)到 print (Iterator iterator) 函数中,支持动态的调用不同的 next ()、hasNext () 实现。
具体点讲就是,当我们往 print (Iterator iterator) 函数传递 Array 类型的对象的时候,print (Iterator iterator) 函数就会调用 Array 的 next ()、hasNext () 的实现逻辑;当我们往 print (Iterator iterator) 函数传递 LinkedList 类型的对象的时候,print (Iterator iterator) 函数就会调用 LinkedList 的 next ()、hasNext () 的实现逻辑。
多态特性存在的意义是什么?它能解决什么编程问题?
多态特性能提高代码的可扩展性和复用性。为什么这么说呢?我们回过头去看讲解多态特性的时候,举的第二个代码实例(Iterator 的例子)。
在那个例子中,我们利用多态的特性,仅用一个 print () 函数就可以实现遍历打印不同类型(Array、LinkedList)集合的数据。当再增加一种要遍历打印的类型的时候,比如 HashMap,我们只需让 HashMap 实现 Iterator 接口,重新实现自己的 hasNext ()、next () 等方法就可以了,完全不需要改动 print () 函数的代码。所以说,多态提高了代码的可扩展性。
如果我们不使用多态特性,我们就无法将不同的集合类型(Array、LinkedList)传递给相同的函数(print (Iterator iterator) 函数)。我们需要针对每种要遍历打印的集合,分别实现不同的 print () 函数,比如针对 Array,我们要实现 print (Array array) 函数,针对 LinkedList,我们要实现 print (LinkedList linkedList) 函数。而利用多态特性,我们只需要实现一个 print () 函数的打印逻辑,就能应对各种集合数据的打印操作,这显然提高了代码的复用性。
除此之外,多态也是很多设计模式、设计原则、编程技巧的代码实现基础:
- 策略模式
- 基于接口而非实现编程
- 依赖倒置原则
- 里式替换原则
- 利用多态去掉冗长的 if-else 语句
- 等等