关于面向对象的思考
关于面向对象的思考
面向对象有三个特征,继承,多态和封装。其中继承是OOP中最核心的抽象概念,一直以来,人们都在使用继承,而很少思考继承在开发复杂系统中的存在的一些问题。
了解继承的优点、缺点和解决缺点的一些可能方案,将使我们设计开发复杂系统的能力具备更广阔的空间。
继承概念的起源
在最初的汇编语言、C语言设计里是没有继承概念的。OOP的继承概念源于现实客观世界,很多不同事物拥有类似的行为和特征。子类通过继承父类,从而调用父类的方法和属性,从抽象层次说,这种方式达到了代码复用的目的。
通过继承概念,子类本身不但可以使用父类的方法和属性,而在子类内部,其还可以添加和扩展子类自己特有的行为方法和属性。
举例来说,人类和老虎这两类生物,有什么共同的行为和特征呢。他们都有年龄的属性,都有重量的属性,都有吃饭,睡觉,喝水等等行为。因此,在建立模型时,可以将前述这些共有的属性和行为提取出来,作为父类抽象,动物类。那么在创建人类和老虎这个子类时,直接继承动物类,就可以重用该父类定义的属性和方法,从而免去每个子类都要重写一遍的麻烦了。
再仔细思考下,人类会思考,有语言,老虎有斑纹,这些是两个子类自己特有的属性和行为方法。因此,在子类继承父类同时,每个子类还可以因地制宜的定义自己的特有行为属性,从而扩展抽象模型。
class Animal{
private int age;
private int weight;
public void sleep(){}
}
class ManKind extends Animal {
public void speak(){}
}
......
ManKind m = new ManKind();
m.speak();//本类中定义的方法
m.sleep();//父类中继承的方法
通过继承,父类的大部分功能自动进入子类。那么,如何判断两个类之间是否有继承关系呢? 用“是”来判断。就是“is-a”表示法。马是动物,草是植物。很符合我们对客观世界的认识。
继承的缺点
面对复杂多变的客观世界,继承概念同样存在很多缺点。人类想要飞行的功能,可以选择具有飞行功能的鸟类继承。人类想要入海,可以选择能在海里自由活动的鱼类继承。那么当人类想要同时获得飞行和入海的行为功能,怎么办?同时继承鸟类和鱼类,不就行了?可惜,在Java的OOP世界里,类只能单继承。也就是说一个Class只能通过extends继承一个父类。当然java也提供别的方式来实现多继承的功能,就是定义接口interface方式。
除了不能多重继承外,继承的第二个缺点,是子类将无条件继承父类的全部属性和方法。例如海里的鱼会吃虫子,鸟类会筑巢,那么当选择人类继承鸟类和鱼类时,也同时会拥有前述的行为方法。很显然,这里面于逻辑是存在问题的。这个在OOP中,称之为继承带来的方法污染。 如下代码:
class Fish{
public void swimInTheSea(){}
public void eatBugs(){}
}
class ManKind extends Bird{
public void speak(){}
}
……
ManKind m = new ManKind();
m.speak();
m.swimInTheSea();//人类希望拥有的入海行为
m.eatBugs();//人类不希望拥有的吃虫子行为
另一点,从父类继承而来的实现是静态的,不能在运行时发生改变,不够灵活。比如,有一个人吃饭,是先吃主食,再喝汤,再吃甜点。
class Man{
public void eat(){
System.out.println("先吃主食,再喝汤,再吃甜点");
}
}
class SomeMan extends Man{
}
……
SomeMan c = new SomeMan();
c.eat();
思考下,如果我们模型中设计一个人先喝汤,再吃甜点,最后才吃主食,那怎么办呢? 我们选择重载方法。
class SomeMan extends Man{
@Override
public void run(){
System.out.println("先喝汤,再吃甜点,最后才吃主食");
}
}
但是如果某天,这个人突然改变了次序,增加了一项新任务,在喝汤和吃甜点之间,要先读一下报纸,怎么办呢? 显然,就没法通过继承和重写来实现了。所以,无论是从父类中继承的方法,还是子类重写的父类方法,实现的都是一种静态的复用。不能在运行时发生改变,灵活性比较差。
解决继承的缺点
为了解决继承机制带来的种种缺陷,使用聚合/组合达到代码的复用。人类想要飞行怎么办?直接使用飞机。人类想要入海怎么办?直接使用潜水艇。这就避免了继承鸟类和鱼类带来的很多逻辑问题。
从抽象层面讲,这是将问题从“is-a”变化为“has-a”,从“是什么”到“用什么”的变化。用聚合/组合复用,去代替继承复用。把一些特征和行为抽取出来,形成工具类。然后通过聚合/组合成为当前类的属性。再调用其中的属性和行为达到代码重用的目的。
class Plane{
public void fly(){}
}
class Submarine{
public void swim(){}
}
class ManKind {
private Plane p = new Plane();
private Submarine s = new Submarine();
public void fly(){
p.fly();
}
public void swim(){
s.swim();
}
}
通过聚合/组合关系,可以解决继承的缺点。由于一个类可以建多个属性,也就是可以聚合多个类。所以,可以通过聚合/组合关系,重用多个类中的代码。
我们可以选择一个类中是否具有某种行为,从而决定聚合哪些类,不聚合哪些类。通过聚合组合关系,在保持很高的灵活性和复用性同时,也避免了方法污染。
还有一点好处时,聚合/组合关系的设计,可以在运行时动态进行。创建新对象使用该关系,将新的属性或行为派发给合适的对象。
举例:
//吃饭工具接口
interface DietTools{
public void eat();
}
//吃饭工具 - 刀叉实现类
class Knife implements DietTools{
public void eat(){
System.out.println("使用刀叉吃饭");
}
}
//吃饭工具 - 筷子实现类
class Chopsticks implements DietTools{
public void eat(){
System.out.println("使用筷子吃饭");
}
}
//人类
class Mankind {
private DietTools dTools;
public void run(){
dTools.eat();
}
//更换工具
public void changeDietTools(DietTools dTools){
this.dTools = dTools;
}
}
……
Mankind c = new Mankind();
c.changeDietTools(new Chopsticks());
c.eat();//使用筷子
c.changeDietTools(new Knife());
c.eat();//使用刀叉
总结
继承和聚合/组合都可以达到代码重用的目的。继承有自身的优点,父类的大部分功能可以通过继承关系自动进入子类;修改或扩展继承而来的实现较为容易。
但是,继承同样有缺点,
无法通过继承达到多个类代码的重用。 父类的方法子类无条件继承,很容易造成方法污染。 从父类中继承的方法,是一种静态的复用。不能在运行时发生改变,不够灵活。 继承可以用,但使用继承需要谨慎。一般来说,使用继承有两个条件:
父类中所有的属性和方法,在子类中都适用。 子类不需要再去重用别的类中的代码。 如果不能满足这两个条件,那么就应该使用聚合/组合关系去替代继承,来达到代码的复用。