面对对象编程与面对对象语言的四大特性:封装、抽象、继承、多态。一般来说,我们知道这些特性,但是却不知道怎么使用,为什么要使用。接下来,我们一起去解决这些问题。
封装
封装也叫作信息隐藏或者数据访问保护。 类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
那究竟什么是封装呢?我们通过简单的例子来描述一下封装特性。
下面是一段游戏装备强化的代码实现,在玩家获得装备时,我们为每个装备对象创建 Equip 对象,以此保存每个装备的数据。
/**
* 装备
*/
public class Equip {
// 装备流水id
private int id;
// 装备配置id
private int configId;
// 强化时刻
private long upgradeLevelTime;
// 强化等级
private int level;
// 更多属性
...
private Equip() {}
public Equip(int configId) {
this.id = IdGenerator.obtainId();
this.configId = configId;
this.level = 1;
}
/**
* 装备流水id
*/
public int getId() {
return id;
}
/**
* 装备配置id
*/
public int getConfigId() {
return configId;
}
/**
* 强化时刻
*/
public long getUpgradeLevelTime() {
return upgradeLevelTime;
}
/**
* 获得强化等级
*/
public int getLevel() {
return level;
}
/**
* 强化
* @return 强化是否成功
*/
public boolean upgradeLevel() {
// 判断是否能强化到下一个等级
...
// 强化成功
this.level ++;
this.upgradeLevelTime = System.currentTimeMillis();
return true;
}
...
}
从代码中,我们可以看到装备存在属性:
- id 装备流水id
- configId 装备配置id
- upgradeLevelTime 强化时刻
- level 强化等级
存在方法:
- getId() 装备流水id
- getConfigId() 装备配置id
- getReceiveLevelTime() 强化时刻
- getLevel() 获得强化等级
- upgradeLevel() 强化
装备流水id、装备配置id,这些属性为什么就只有 get 方法呢?因为这些方法都在对象初始化构造时,在构造函数中进行赋值,固化数值,后续不允许修改,因此不应提供修改方法。
upgradeLevelTime 强化时刻、强化等级level 这俩个属性,皆只提供了 get 方法来获取数据,只通过强化方法 upgradeLevel 去修改强化等级,这又为什么呢?之所有这么设计,强化时刻和强化等级这俩个属性的变化,都在会发生在强化操作,因此不应该提供 set 方法修改方式。
对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制。 例子中的 private、public 等关键字就是 Java 语言中的访问权限控制语法。private 关键字修饰的属性只能类本身访问,可以保护其不被类之外的代码直接访问。如果 Java 语言没有提供访问权限控制语法,所有的属性默认都是 public 的,那任意外部代码都可以通过类似 equip.id=30; 这样的方式直接访问、修改属性,也就没办法达到隐藏信息和保护数据的目的了,也就无法支持封装特性了。
什么是封装?如何进行封装?我们在上面都已经解决了。那封装到底解决了什么编程痛点呢?
如果类中属性的访问不做任何限制,任意代码皆可访问或者修改类中属性,虽然使用此类时更灵活了,但是这种“灵活”却要付出代价的,造成类的状态不可控,严重影响代码的 可读性、可维护性 。像上述的例子中,若upgradeLevelTime、level 都提供 set 方法,某个小伙伴在不了解的情况下,setUpgradeLevelTime(0L) ,手动设置了强化时刻,就造成强化时刻与强化等级数据不一致的情况。因强化时刻这个属性的设置,本身的修改就依赖于强化操作,此番操作势必为非法。
除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的 易用性 。如果将所有的属性接口都暴露,除非调用者对此类的业务充分了解,否则很容易出错。这对调用者极不友好,为了降低出错的概率,应该只暴露真正需要的接口。假如我们现在拥有一个电水壶,水壶上有很多按钮开关,用户需要花很长的时间去研究说明书,最终也不一定能看懂,这样太闹腾了。对于用户,拿到一个电水壶,我只想插上电,开始煮水,能尽快喝到热水。简单的开、关按钮足以应对用户场景,效率高,用户体验好,出错率低。
抽象
封装解决的是隐藏信息和数据保护的问题,而抽象解决的是隐藏方法实现的问题。
在面向对象编程中,我们常借助编程语言提供的接口类(比如 Java 中的 interface)或者抽象类(比如 Java 中的 abstract)这两种语法机制,来实现抽象这一特性。
我们可以通过下面的例子来看一下抽象:
public interface IUserService {
/**
* 登录
*/
void login(User user);
/**
* 登出
*/
void logout(User user);
/**
* 改名
*/
void rename(User user);
}
public class UserService implement IUserService {
@Override
public void login(User user){
//...
}
@Override
public void logout(User user){
//...
}
@Override
public void rename(User user){
//...
}
}
抽象这一特性,并不一定要通过接口或者抽象类来实现。 即使不编写 IUserService 接口类,UserService 类本身也能满足抽象特性。因为,类的方法是通过编程语言中的“函数”这一语法机制来实现的。通过函数包裹具体的实现逻辑,这本身就是一种抽象。我们使用函数时,并不需要知道方法内部怎么实现,直接调用即可。
什么是抽象?如何进行抽象?我们在上面都已经解决了。那抽象到底解决了什么编程痛点呢?
我们去处理复杂的问题时,问题的处理实现细节没必要暴露出来给调用者,这时候我们需要做上层抽象,把非必要性的实现细节抽象出来,这样我们可以更好地关注功能,而非具体的实现的设计思路。我们在命名类的方法时,需要抽象思维,不能暴露太多的实现细节,以此确保后续变更内部逻辑时,不用再次修改方法定义。比如 getMySqlDBUrl() 就暴露了实现细节,后续假如将数据库改成 MongoDB ,方法定义与实现就不一致了,需要重新定义方法了,这里应该使用 getDBUrl() 更为合适。
在代码设计中,起到非常重要的指导作用。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等。
继承
如果你熟悉的是类似 Java、C++ 这样的面向对象的编程语言,那你对继承这一特性,应该不陌生了。继承是用来表示类之间的 is-a 关系。
为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如 Java 使用 extends 关键字来实现继承,C++ 使用冒号(class B : public A),Python 使用 parentheses (),Ruby 使用 <。不过,有些编程语言只支持单继承,不支持多重继承,比如 Java、PHP、C#、Ruby 等,而有些编程语言既支持单重继承,也支持多重继承,比如 C++、Python、Perl 等。
什么是继承?如何进行继承?我们在上面都已经解决了。那继承到底解决了什么编程痛点呢?
继承最大的一个好处就是代码复用。 假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。
继承很好理解,也很容易使用。不过,过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。部分人会认为继承是一种反模式,觉得应该少用,甚至不用。在继承层次过深的情况下,接口-组合-委托的方式会比继承更优,这就是“多用组合少用继承”的思想。
多态
多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
我们通过下面的例子可以更好体会多态:
public interface Animal {
void eat();
void sleep();
}
public class Dog implement Animal {
@Override
public void eat(){
//...
}
@Override
public void sleep(){
//...
}
}
public class Bird implement Animal {
@Override
public void eat(){
//...
}
@Override
public void sleep(){
//...
}
}
public class Demo {
public static void main(String[] args){
Dog dog = new Dog();
Bird bird = new Bird();
eat(dog);
sleep(bird);
}
private static void eat(Animal a) {
a.eat();
// ...
}
private static void sleep(Animal a) {
a.sleep();
// ...
}
}
多态有3个机制:
- 父类对象可以引用子类对象。Dog、Bird 皆可传递给 Animal;
- 对象间为继承关系。Dog、Bird 皆继承 Animal;
- 子类可以重写父类的方法。在本例中 Animal 为接口,也可以为抽象类。
我们通过不同的实现类型对象传递给 Animal ,在调用 Animal 的方法时,实质上是调用传递对象的方法实现逻辑。具体点,我们在调用 Animal#eat 时,方法内就会调用 Dog#eat 的实现逻辑。
什么是多态?如何进行多态?我们在上面都已经解决了。那多态到底解决了什么编程痛点呢?
多态提高了代码的可扩展性和复用性。
为什么可以提高可扩展性呢?我们看下上面的例子,Demo#eat 、Demo#sleep 都使用了一个方法,就可以处理来自 Dog 和 Bird 的调用,而且在后续增加 Animal 的继承类时,Demo#eat 、Demo#sleep 的实现逻辑皆不用改动,因此提高了代码的可扩展性。
为什么可以提高复用性呢?我们在执行不同 Animal 的 eat 方法时,不需要实现不同接收类型的函数(Demo#eat(Dog)、Demo#eat(Bird)),我们只需要一个函数处理即可(Demo#eat(Animal)),显然提高了代码的复用性。
除此之外,多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。
回顾
- 封装
- what? 信息隐藏或者数据访问保护。
- how? 通过语法机制就是访问权限控制,暴露有限的接口。
- why? 可读性、可维护性、易用性。
- 抽象
- what? 隐藏方法的实现。
- how? 通过接口类或者抽象类,非必须。
- why? 隐藏实现细节,降低复杂度。
- 继承
- what? 表示类之间的
is-a关系。 - how? 通过特殊的语法机制来支持,比如 Java 使用 extends。
- why? 代码复用。
- what? 表示类之间的
- 多态
- what? 通过子类绑定父类,父类可以调用子类实现逻辑。
- how? 提供特殊的语法机制。比如继承、接口类。
- why? 提高了代码的可扩展性和复用性。