但行好事  莫问前程

聊聊面向对象编程的几个基本原则

进行面向对象编程,有下面几个原则:

一. 面向抽象原则
二. 开闭原则
三. 多用组合少用继承原则
四. 高内聚-低耦合原则

一. 面向抽象原则

下面首先先介绍抽象类和接口,然后介绍面向抽象编程。

1. 抽象类和接口

1.1 抽象类
抽象类(abstract)具有如下特点:

1. 抽象类中可以有abstract方法,也可以有非abstract方法。

2. 抽象类不能使用new运算符创建对象。

3. 如果一个非抽象类是某个抽象类的子类,那么它必须重写父类的abstract方法,即在子类中将abstract方法重新声明,但必须去掉abstract修饰符,同时要保证声明的方法名字,返回类型,参数个数和类型与父类的abstract方法完全相同。

4. 作为上转型对象。尽管抽象类不能使用new运算符创建对象,但其非abstract子类必须要重写全部abstract方法,这样一来,就可以让抽象类声明的对象成为其子类对象的上转型对象,并调用子类重写的方法。

例如,下面抽象类A中有一个abstract方法add(int x,int y);

public abstract class A {
    public abstract int add(int x, int y);
}

下列BA的一个非abstract子类,子类B在重写父类A中的abstract方法add(int x,int y)时,将其实现为计算参数xy的和。

public class B extends A {

    @Override
    public int add(int x, int y) {
        return x + y;
    }
}

假设b是子类B创建的对象,那么可以让A类声明的对象a成为对象b的上转型对象,即让a存放b的引用。上转型对象能够调用子类重写的add()方法,例如:

public class Application {
    public static void main(String[] args) {
        A a;
        a = new B(); // a是B类对象的上转型对象
        int m = a.add(3, 2); // a调用子类B重写的add()方法
        System.out.println(m);// 输出结果为5
    }
}

1.2 接口
接口(interface)具有如下特点:

1. 接口中只可以有public权限的abstract方法,不能有非abstract方法。

2. 接口由类去实现,即一个类如果实现一个接口,那么它必须重写接口中的abstract方法,即将abstract方法重新声明,但必须去掉abstract修饰符,同时要保证声明的方法名字,返回类型,参数个数和类型与接口中的方法完全相同。

3. 接口回调。接口回调是指可以把实现接口的类的对象的引用赋给该接口声明的接口变量中,那么该接口变量就可以调用被类实现的接口中的方法,当接口变量调用被类实现的接口中的方法时,就是通知相应的对象调用接口的方法,这一过程称为对象方法的接口回调。

例如,下面接口Com中有一个abstract方法sub(int x,int y);

public interface Com {
    public abstract int sub(int x, int y);
}

ComImp是实现Com接口的类,ComImp类在重写Com接口中的abstract 方法sub(int x,int y)时,将其实现为计算参数xy的差:

public class ComImp implements Com {

    @Override
    public int sub(int x, int y) {
        return x - y;
    }
}

可以让Com接口声明的接口变量com存放ComImp类的对象引用,那么com就可以调用ComImp类实现的接口中的方法。例如:

public class Application {
    public static void main(String[] args) {
        Com com;
        com = new ComImp(); // com变量存放ComImp类的对象引用
        int m = com.sub(8, 2); // com回调ComImp类实现的接口方法
        System.out.println(m);// 输出结果为6
    }
}

2. 面向抽象

所谓面向抽象编程,是指当设计一个类时,不让该类面向具体的类,而是面向抽象类或者接口,即所设计类中的重要数据是抽象类或接口声明的变量,而不是具体类声明的变量。

以下通过一个简单的例子说明面向抽象编程的思想。比如,已经有了一个Circle类,该类创建的对象circle调用getArea()方法可以计算圆的面积,Circle类的代码如下:

public class Circle {
    double r;

    Circle(double r) {
        this.r = r;
    }

    public double getArea() {
        return (3.14 * r * r);
    }
}

现在要设计一个Pillar类(柱类),该类的对象调用getVolume()方法可以计算柱体体积,Pillar类的代码如下:

public class Pillar {
    Circle bottom;
    double height;

    Pillar(Circle bottom, double height) {
        this.bottom = bottom;
        this.height = height;
    }

    public double getVolume() {
        return (bottom.getArea() * height);
    }
}

上述Pillar类中,bottom是用具体类Circle声明的变量,如果不涉及用户需求的变化,上面的Pillar类的设计没有任何不妥,但是在某个时候,用户希望Pillar能创建出底是三角形的柱体。显然上述Pillar类无法创建出这样的柱体,即上述设计的Pillar类不应对用户的这种需求。

现在重新来设计Pillar类。首先,注意到柱体计算体积的关键是计算出底面积,一个柱体在计算底体积时不应该关系它的底是怎样形状的具体图形,应该只关心这种图形是否具有计算面积的方法。因此,在设计Pillar类时不应当让它的底是某个具体类声明的变量,一旦这样做,Pillar类就依赖具体的类,缺乏弹性,难以应对需求的变化。

下面将面向抽象重新设计Pillar类。首先编写一个抽象类Geometry(或接口),该抽象类(接口)中定义了一个抽象的getArea()方法。

public abstract class Geometry { // 如果使用接口需要用interface来定义Geometry
    public abstract double getArea();
}

现在Pillar类的设计者可以面向Geometry类编写代码,即Pillar类应当把Geometry对象作为自己的成员,该成员可以调用Geometry的子类重写的getArea()方法。这样一来,Pillar类就可以将计算底面积的任务指派给实现Geometry类的子类的实例(如果Geometry是一个接口,Pillar类就可以将计算底面积的任务指派给实现Geometry接口的类的实例)。

以下Pillar类的设计不再依赖具体类,而是面向Geometry类,即Pillar类中的bottom是用抽象类Geometry声明的变量,而不是具体类声明的变量。重新设计Pillar类的代码如下:

public class Pillar {
    Geometry bottom; // bottom是抽象类Geometry声明的变量
    double height;

    Pillar(Geometry bottom, double height) {
        this.bottom = bottom;
        this.height = height;
    }

    public double getVolume() {
        return (bottom.getArea() * height); // bottom可以调用子类重写的getArea方法
    }
}

下面Circle类和Rectangle类都是Geometry的子类,二者都必须重写Geometry 类的getArea()方法来计算各自的面积。
Circle.java

public class Circle extends Geometry{
    double r;

    Circle(double r) {
        this.r = r;
    }

    public double getArea() {
        return (3.14 * r * r);
    }
}

Rectangle.java

public class Rectangle extends Geometry{
    double a, b;

    Rectangle(double a, double b) {
        this.a = a;
        this.b = b;
    }

    public double getArea() {
        return a * b;
    }
}

现在,就可以用Pillar类创建出具有矩形底或者圆形底的柱体了,如下列Application.java所示:

public class Application {
    public static void main(String[] args) {
        Pillar pillar;
        Geometry bottom;
        bottom = new Rectangle(12, 22);
        pillar = new Pillar(bottom, 58); // pillar是具有矩形底的柱体
        System.out.println("矩形底的柱体的体积" + pillar.getVolume());
        bottom = new Circle(10);
        pillar = new Pillar(bottom, 58); // pillar是具有圆形底的柱体
        System.out.println("圆形底的柱体的体积" + pillar.getVolume());
    }
}

通过面向抽象来设计Pillar类,使得该Pillar类不再依赖具体类,因此每当系统增加新的Geometry的子类时,比如增加一个Triangle子类,那么不需要修改Pillar类的任何代码,就可以使用Pillar创建出具有三角形底的柱体。

二. 开闭原则

所谓”开闭原则”(Open-Closed Principle)就是让设计对拓展开放,对修改关闭。怎么理解对拓展开放,对修改关闭呢?实际上这句话的本质是指当一个设计中增加新的模块时,不需要修改现有的模块。在给出一个设计是,应该首先考虑到用户需求的变化,将应对用户变化的部分设计为对拓展开放,而设计的核心部分是经过精心考虑过之后确定下来的基本结构,这部分应该是对修改关闭的,即不能因为用户的需求变化而再发生变化,因为这部分不是用来应对需求变化的。如果设计遵守了”开-闭原则”,那么这个设计一定是易维护的,因为在设计中增加新的模块时,不必去修改设计中的核心模块。比如上面代码给出的设计中有四个类,类图如下所示:
1
该设计中的GeometryPillar类就是系统中队修改关闭的部分,而Geometry的子类是对拓展开放的部分。当向系统再增加任何Geometry的子类时(对拓展开放),不必修改Pillar类,就可以使用Pillar创建出具有Geometry的心子类指定的底的柱体。

通常无法让设计的每个部分都遵守”开-闭原则”,甚至不应当这样去做,应当把主要精力集中在应对设计中最有可能因需求变化而需要改变的地方,然后想办法应用”开-闭原则”。

当设计某些系统时,经常需要面向抽象来考虑系统的总体设计,不要考虑具体类,这样就容易设计出满足”开-闭原则”的系统,在程序设计好后,首先对abstract类的修改关闭,否则,一旦修改abstract类,比如,为它增加一个abstract方法,那么abstract类所有的子类都需要做出修改;应该对增加abstract类的子类开放,即在程序中再增加子类时,不需要修改其他面向抽象类而设计的重要类。

三.多用组合少用继承原则

方法复用的两种最常用的技术就是类继承和对象组合

1. 继承和复用

子类继承父类的方法作为自己的一个方法,就好像它们是在子类中直接声明一样,可以被子类中自己声明的任何实例方法调用。也就是说,父类的方法可以被子类以继承的方式复用。

通过继承来复用父类的方法的优点是:

子类可以重写父类的方法,即易于修改或者拓展那些被复用的方法。

通过继承来复用父类的方法的缺点是:

1. 子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。

2. 子类和父类的关系是强耦合关系,也就是说当父类的方法的行为更改时,必然导致子类发生变化。

3.通过继承进行复用也称”白盒”复用,其缺点是父类的内部细节对于子类而已是可见的。

2. 组合和复用

一个类的成员变量可以是Java允许的任何数据类型,因此,一个类可以把对象当作自己的成员变量,如果用这样的类创建对象,那么该对象中就会有其他对象,也就是说,该对象将其他对象作为自己的组成部分(这就是人们常说的Has——A),或者说该对象是由几个对象组合而成。

如果一个对象a组合了对象b,那么对象a就可以委托对象b调用其方法,即对象a以组合的方式复用对象b的方法/
通过组合对象来复用方法的优点是:

1. 通过组合来复用方法也称”黑盒”复用,因为当前对象只能委托所包含的对象调用其方法,这样一来,当前对象所包含对象方法的细节对当前对象是不可见的。

2.对象与所包含对象属于弱耦合关系,因为,如果修改当前对象所包含对象类的代码,不必修改当前对象类的代码。

3.当前对象可以在运行时动态指定所包含的对象,例如,假设Com是以恶搞接口,该接口中有一个computer()方法,那么下列Computer类的对象可以在运行时动态指定所包含的对象,即运行期间,Computer类的实例可调用setCom(Com com)方法将其中的com变量存放任何实现Com接口对象的引用。

public class Computer {
    Com com;

    public void setCom(Com com) {
        this.com = com;
    }

    public void f() {
        com.computer();
    }
}

通过组合对象来复用方法的缺点是:

1.容易导致系统中的对象过多。

2.为了能够组合多个对象,必须仔细的对接口进行定义

3. 多用组合,少用继承

之所以提倡多用组合,少用继承,是因为在许多设计中,人们希望系统的类之间尽量是低耦合关系,而不希望是强耦合关系。即在许多情况下需要避开继承的缺点,而需要组合的优点。怎么样合理地使用组合,而不是使用继承来获得方法的复用需要经过一定时间的认真思考,学习和编程实践才能悟出其中的道理。

四. 高内聚-低耦合原则

如果类中的方法是一组相关的行为,则称该类是高内聚的,反之称为低内聚的。搞内聚便于类的维护,而低内聚不利于类的维护。所谓低耦合就是尽量不要让一个类含有太多其他类的实例引用,以避免修改系统的其中一部分会影响到其他部分。

打赏
欢迎关注人生设计师的微信公众账号
公众号ID:longjiazuoA

未经允许不得转载:人生设计师 » 聊聊面向对象编程的几个基本原则

分享到:更多 ()

人生设计师-接受不同的声音

联系我关于我