设计模式:封装、抽象、继承、多态分别可以解决哪些问题

封装Encapsulation

什么是

  • 封装也叫做信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(也叫做函数)来访问内部信息或者数据。

  • 对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制。比如public、private之类的

为什么要做封装

  • 如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。
  • 除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。

抽象Abstraction

什么是抽象

  • 封装主要讲的是如何隐藏信息,保护数据,而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关系提供了哪些功能,并不需要知道这些功能是如何实现的
  • 在面向对象编程中,我们常借助编程语言提供的接口类(比如java中的interface关键字)或者抽象类(比如java中的astract关键字语法)这两种机制,来实现现象这一特性。
  • 实际上,抽象这个特性是非常容易实现的,并不需要非得依靠接口类或者抽象类这些特殊语法机制来支持。函数本身就是一种抽象。调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功能,就可以直接使用了。比如,我们在使用 C 语言的 malloc() 函数的时候,并不需要了解它的底层代码是怎么实现的。

抽象有时候被排查在面向对象的四大特性之外,这是为什么呢?

  • 抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一非常基础的语法机制,就可以实现抽象特性。
  • 所以,它没有很强的“特异性”,有时候并不被看做面向对象编程的特性之一。

为什么要有抽象呢?它的意义是什么?能解决什么编程问题?

  • 抽象以及封装都是人类处理复杂性的有效手段。在面向复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节。而抽象作为一种只关注功能点而不关注实现的设计思路,正好帮我妈的大脑过滤掉很多非必要的信息。
  • 除此之外,抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦等。
  • 我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义

继承Inheritance

什么是继承

继承是用来表示类之间的is-a 关系,比如猫是一种哺乳动物。从继承关系上来讲,继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类。比如猫既是哺乳动物,又是爬行动物。

为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如 Java 使用extends 关键字来实现继承,C++ 使用冒号(class B : public A),Python 使用paraentheses(),Ruby 使用 <。不过,有些编程语言只支持单继承,不支持多重继承,比如 Java、PHP、C#、Ruby 等,而有些编程语言既支持单重继承,也支持多重继承,比如C++、Python、Perl 等。

多重继承有副作用:钻石问题(菱形继承),就是两个父类有同名方法,在子类中调用的时候就不知道调用哪个了,出现决议(钻石问题或菱形问题)问题

为什么要有继承

继承最大的一个好处是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码。不过,这一点也不是继承所独有的,我们也可以通过其他方法来解决这个问题,比如利用组合关系而不是继承关系。

如果我们再上升一个思维层面,去思考继承这一特性,可以这么理解:我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 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();
		for (int 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[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() 方法。

通过这三种语法机制配合在一起,我们就实现了在 test() 方法中,子类SortedDyamicArray 替换父类 DynamicArray,执行子类 SortedDyamicArray 的 add()方法,也就是实现了多态特性。

对于多态特性的实现方式,除了利用“继承加方法重写”这种实现方式之外,我们还有其他两种比较常见的的实现方式,一个是利用接口类语法,另一个是利用 duck-typing 语法。

不过,并不是每种编程语言都支持接口类或者 duck-typing 这两种语法机制,比如 C++就不支持接口类语法,而 duck-typing 只有一些动态语言才支持,比如 Python、JavaScript 等。

接下来,我们先来看如何利用接口类来实现多态特性。我们还是先来看一段代码。

public interface Iterator {
	String hasNext();
	String next();
	String remove();
}
public class Array implements Iterator {
	private String[] data;
	public String hasNext() { ... }
	public String next() { ... }
	public String remove() { ... }
	//... 省略其他方法...
}
public class LinkedList implements Iterator {
	private LinkedListNode head;
	public String 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() 的实现逻辑。

刚刚讲的是用接口类来实现多态特性。现在,我们再来看下,如何用 duck-typing 来实现多态特性。我们还是先来看一段代码。这是一段 Python 代码。

class Logger:
	def record(self):
		print(“I write a log into file.)
class DB:
	def record(self):
		print(“I insert data into db.)
		
def test(recorder):
	recorder.record()
def demo():
	logger = Logger()
	db = DB()
	test(logger)
	test(db)

从这段代码中,我们发现,duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,但是只要它们都有定义了record() 方法,就可以被传递到 test() 方法中,在实际运行的时候,执行对应的 record()方法。

也就是说,只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing,是一些动态语言所特有的语法机制。而像 Java 这样的静态语言,通过继承实现多态特性,必须要求两个类之间有继承关系,通过接口实现多态特性,类必须实现对应的接口。

为什么要有多态

多态特性能提高代码的可扩展性和复用性。以第二个例子为例,在这里,我们利用多态的特性,仅用一个 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 语句等等。

总结

(1)关于封装特性

  • 封装也叫做信息隐藏或者数据访问保护
  • 类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息和数据
  • 它需要通过编程语言提供权限访问控制语法来支持,比如Java 中的 private、protected、public 关键字
  • 封装特性存在的意义,一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性

(2)关于抽象特性

  • 封装主要关于如何隐藏信息、保护数据,而抽象就是关于如何隐藏方法的具体实现,让使用者只需要关心方法提供了哪些功能,不需要知道这些功能是如何实现的
  • 抽象可以通过接口类或者抽象类来实现,但也并不需要特殊的语法机制来支持
  • 抽象存在的意义,一方面是提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围;另一方面,它也是处理复杂系统的有效手段,能有效的过滤掉不必要关注的信息

(3)关于继承特性

  • 继承是用来表示类之间的is-a关系。分为两种模式:单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类。
  • 为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持。
  • 继承主要是为了解决代码复用的问题

(4)关于多态特性

  • 多态是指子类可以替换父类,在实际的代码运行过程中,调用此类的方法实现。
  • 多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类、duck-typing
  • 多态可以提高代码的扩展性和复用性,是很多设计模式,设计原则,编程技巧的代码实现基础
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值