# 关于面向对象的思考 # 关于面向对象的思考 面向对象有三个特征,继承,多态和封装。其中继承是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();//使用刀叉 ## 总结 继承和聚合/组合都可以达到代码重用的目的。继承有自身的优点,父类的大部分功能可以通过继承关系自动进入子类;修改或扩展继承而来的实现较为容易。 但是,继承同样有缺点, 无法通过继承达到多个类代码的重用。 父类的方法子类无条件继承,很容易造成方法污染。 从父类中继承的方法,是一种静态的复用。不能在运行时发生改变,不够灵活。 继承可以用,但使用继承需要谨慎。一般来说,使用继承有两个条件: 父类中所有的属性和方法,在子类中都适用。 子类不需要再去重用别的类中的代码。 如果不能满足这两个条件,那么就应该使用聚合/组合关系去替代继承,来达到代码的复用。