目录

关于面向对象的思考

关于面向对象的思考

面向对象有三个特征,继承,多态和封装。其中继承是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();//使用刀叉

总结

继承和聚合/组合都可以达到代码重用的目的。继承有自身的优点,父类的大部分功能可以通过继承关系自动进入子类;修改或扩展继承而来的实现较为容易。

但是,继承同样有缺点,

无法通过继承达到多个类代码的重用。 父类的方法子类无条件继承,很容易造成方法污染。 从父类中继承的方法,是一种静态的复用。不能在运行时发生改变,不够灵活。 继承可以用,但使用继承需要谨慎。一般来说,使用继承有两个条件:

父类中所有的属性和方法,在子类中都适用。 子类不需要再去重用别的类中的代码。 如果不能满足这两个条件,那么就应该使用聚合/组合关系去替代继承,来达到代码的复用。