封装
概念
封装(Encapsulation)
也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
目的
预防不可控
对类中属性的访问不做限制,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。
提高易用性
如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。
实现
通过语言本身的访问 权限控制
这一语法机制支持封装
public class Wallet {
private String id;
private long createTime;
private BigDecimal balance;
private long balanceLastModifiedTime;
// ...省略其他属性...
public Wallet() {
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
}
// 注意:下面对get方法做了代码折叠,是为了减少代码所占文章的篇幅
public String getId() { return this.id; }
public long getCreateTime() { return this.createTime; }
public BigDecimal getBalance() { return this.balance; }
public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime; }
public void increaseBalance(BigDecimal increasedAmount) {
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {
throw new InsufficientAmountException("...");
}
this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
- 从业务的角度来说,
id
、createTime
在创建钱包的时候就确定好了,之后不应该再被改动,所以并没有暴露修改方法。 - 对于钱包余额
balance
这个属性,从业务的角度来说,只能增或者减,不会被重新设置。所以只暴露了increaseBalance()
和decreaseBalance()
方法,并没有暴露set
方法。 - 对于
balanceLastModifiedTime
这个属性,只有在balance
修改的时候,这个属性才会被修改。所以,修改操作完全封装在了increaseBalance()
和decreaseBalance()
两个方法中。这样也可以保证balance
和balanceLastModifiedTime
两个数据的一致性。
抽象
概念
抽象(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) { }
}
- 并不是说一定要为实现类
PictureStorage
抽象出接口类IPictureStorage
,才叫作抽象。即便不编写IPictureStorage
接口类,单纯的PictureStorage
类本身就满足抽象特性。类的方法是通过编程语言中的函数
这一语法机制来实现的。通过函数包裹具体的实现逻辑,这本身就是一种抽象。调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功能,就可以直接使用了。比如,我们在使用 C 语言的malloc()
函数的时候,并不需要了解它的底层代码是怎么实现的。 - 不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。举个简单例子,比如
getAliyunPictureUrl()
就不是一个具有抽象思维的命名,因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作getPictureUrl()
,那即便内部存储方式修改了,我们也不需要修改命名。
继承
概念
继承(Inheritance)
用于表示类之间的 is-a
关系。
目的
代码复用
将相同的属性和方法,抽取到父类中,子类就可以重用父类中的代码,避免代码重复写多遍。不过,也可以通过组合关系来解决这个代码复用的问题。
反应关系
通过继承来关联两个类,反应真实世界中的 is-a
关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。
弊端
过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。
- 为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看“父类、父类的父类……”的代码。
- 子类和父类高度耦合,修改父类的代码,会直接影响到子类。
实现
为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如 Java 使用 extends
关键字来实现继承。有些编程语言只支持单继承,不支持多重继承,而有些编程语言既支持单重继承,也支持多重继承。
组合优于继承
利用 组合(composition)
、接口
、委托(delegation)
三个技术手段,一块儿来解决继承存在的问题。
public interface Flyable {
void fly();
}
public interface Tweetable {
void tweet();
}
public interface EggLayable {
void layEgg();
}
public class FlyAbility implements Flyable {
@Override
public void fly() { //... }
}
public class TweetAbility implements Tweetable {
@Override
public void tweet() { //... }
}
public class EggLayAbility implements EggLayable {
@Override
public void layEgg() { //... }
}
// 鸵鸟不会飞,不用实现飞行接口
public class Ostrich implements Tweetable, EggLayable {
private TweetAbility tweetAbility = new TweetAbility(); //组合
private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
//... 省略其他属性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委托
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委托
}
}
组合 or 继承
简单用继承,复杂用组合。 如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。
无继承关系用组合。 需要代码复用,又不具有继承关系,使用组合就更加合理、更加灵活
需要多态特性用继承。 如果不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承来实现
多态
概念
多态(Polymorphism)
是指,定义中的父类参数,在调用时传入子类,则在实际的代码运行过程中,运行子类的方法实现。
目的
性能提高代码的可扩展性和复用性
利用多态的特性,可以通过一个方法,实现操作不同类型的数据。新增类型的时候,只需要创建对应的子类,不用修改操作方法。
多个设计模式的实现基础
很多设计模式、设计原则、编程技巧都需要用到多态特性,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。
实现
继承重写
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
}
}
子类
SortedDyamicArray
替换父类DynamicArray
,执行子类SortedDyamicArray
的add()
方法,也就是实现了多态特性。
利用接口类
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()
实现。
duck-typing 方式
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 实现多态的方式非常灵活
- 既不是继承关系,也不是接口和实现的关系的类,但是只要它们都有定义了
record()
方法,就可以被传递到test()
方法中,在实际运行的时候,执行对应的record()
方法。duck-typing
,是一些动态语言所特有的语法机制。