什么是面向对象?


版权声明:本文由 Hov 所有,发布于 https://chenhy.com ,转载请注明出处。



0 前言


面向对象的编程语言(如 Java、C++ 等)是现在主流的编程语言。熟悉 Java 的开发人员知道,编写代码第一步就是创建一个类。可是,我们用了这么久的 Java (或其它面向对象语言),有没有想过面向对象是什么呢?为什么我们要用类和对象来堆砌我们的代码?我们进行程序设计时有规则可遵循吗?类与类之间的关系又应该怎么设计呢?

本文先从理论角度讲解面向对象的一些重要概念,然后根据面向对象程序设计的方法编写一个小程序,最后通过设计模式进一步优化。


1 什么是面向对象?


1.1 定义

面向对象(Object-oriented,简称 OO)方法是一种运用对象、类、继承、封装、多态、聚合、关联、消息等概念来构造系统的软件开发方法。

面试对象思想将一组数据结构和处理它们的方法组成对象,把相同行为的对象归纳为;通过类的封装隐藏内部细节;通过继承实现类的特化/泛化;通过多态实现基于对象的动态分派。

1.2 基本思想

面向对象强调直接 以问题域(现实世界)中的事物为中心 来思考问题、认识问题,并根据这些事物的本质特征,把它们抽象为系统中的对象,作为系统的基本构成单位。这样做的好处是,可以使系统直接映射问题域,保持问题域中事物及其相互关系的本来面貌。

1.3 面向过程 VS. 面向对象

面向过程:分析解决问题所需要的步骤,然后用函数逐步调用,即面向过程程序设计是先确定算法,再确定数据结构

面向对象:考虑问题域中的独立个体,将程序分解成不同对象之间的交互过程。通常,可以将数据(成员数据)及处理这些数据的方法(成员方法)封装成一个类(Class),而使用类的变量则称为对象(Object)

面向过程程序设计(如 C 语言),习惯先建立数据结构存放数据,并定义函数(方法)来操作数据(注意:数据和方法是分离开的);面向对象程序设计(如 Java、C++),则先构造一个对象模型,将数据和方法组织在一起。

举一个简单的例子:计算长方形的周长和面积

(1)面向过程的程序设计

  1. 确定计算长方形周长和面积的算法;
  2. 编写两个函数分别计算长方形的周长和面积;
  3. 求周长和面积的函数各需要两个参数,分别是长方形的长和宽。

代码如下:

int Perimeter(int x, int y)     //计算周长
{
    return 2*(x+y);
}
int Area(int x, int y)          //计算面积
{
    return x*y;
}

int main()
{
    int a;
    int b;
    cin >> a;
    cin >> b;

    cout << “长方形的周长是:” << Perimeter(a,b) << endl;
    cout << “长方形的面积是:” << Area(a,b);

    return 0;
}

(2)面向对象的程序设计

  1. 一个长方形可看成一个长方形对象(类);
  2. 一个长方形对象有两个属性(长、宽),两个行为(求周长、求面积);
  3. 通过长方形对象的行为,求出某个具体的长方形对象的周长和面积。

代码如下:

class Retangle
{
    private:        //成员变量,即对象的属性
        double a;
        double b;    
    public:         //成员方法,即对象的行为
        Retangle(double x, double y);
        double Perimeter();
        double Area(); 
};

int main()
{
    Rectangle obj(1.0,2.0);
    cout << obj.Perimeter() << endl;
    cout << obj.Area() << endl;
    return 0;
}

Retangle::Retangle(double x, double y)
{
    a = x;
    b = y;
}

double Perimeter::Retangle()        //计算周长
{
    return 2*(a+b);
}

double Area::Retangle()             //计算面积
{
    return a*b;
}

注:上述例子只为说明两种设计思路的区别,省略了预处理相关的代码。


2 类与对象


2.1 类

类是具有相同属性和操作的一组对象的集合,它为属于该类的全部对象提供了统一的抽象描述,其内部包括 属性操作 两个主要部分。类的作用是创建对象,对象是类的一个实例。


2.2 对象

对象是系统中用来描述客观事物的一个实体,是构成系统的基本单位。对象一般是现实世界中某个实际存在的事物,它可以是有形的(如一辆汽车),也可以是无形的(如一项计划)。

对象由一组属性和施加于这些属性的操作构成。

属性:用来描述对象的静态特征的数据项;

操作:用来描述对象的动态特征的动作序列;

对象标识:对象的名字。


2.3 类与对象的区别

(1)类是用来描述实体的“模版”或“原型”;
(2)每一个对象都是类的一个具体实例;
(3)类用来定义对象所有的属性和方法,同一个类的所有对象都拥有相同的特征和操作;
(4)可将类理解成生产产品的模具,而对象则是根据此模具生产的一个个产品。


3 面向对象的核心


3.1 封装

封装 是指把对象的属性和操作结合成一个独立的系统单位,并尽可能隐蔽对象的内部细节。

封装的意义在于,使对象能够集中而完整地描述一个具体的事物,体现了事物的相对独立性。对象外部不能随意存取对象的内部数据,避免了外部错误对它的“交叉感染”;对象的内部修改对外部影响很小,减少了修改引起的“波动效应”。

3.2 继承

继承:子类拥有其父类的全部属性与操作,称作子类对父类的继承。继承意味着自动地拥有,它简化了人们对事物的认识和描述,有利于软件复用,这也是面向对象技术能提高软件开发效率的重要原因之一。

父类与子类(一般类与特殊类):如果类 A 具有类 B 的全部属性和全部操作,而且具有自己特有的某些属性或操作,则类 A 为类 B 的子类(特殊类),类 B 为类 A 的父类(一般类)。

3.3 多态

多态 是指同一个命名可具有不同的语义。面向对象方法中,常指父类定义的属性或操作被子类继承后,可以具有不同的数据类型或表现出不同的行为。

多态的实现机制:

  1. 重写(Override):在子类中对继承来的属性或操作重新定义其实现;
  2. 重载(Overload):动态绑定,在运行时根据对象接收的消息动态确定要连接哪一段操作代码。


4 通过面向对象思维编写程序


现在,我们试试通过面向对象程序设计的方法编写 模拟鸭子 的程序。

假设我们的需求是这样的:

  1. 鸭子有 绿头鸭红头鸭 两种;
  2. 所有的鸭子都会 嘎嘎叫游泳

根据面向对象思维,程序中的对象有:鸭子、绿头鸭、红头鸭。

因此,我们可将鸭子作为父类,绿头鸭和红头鸭作为父类鸭子的子类,这样两个子类可以复用父类的部分属性和操作。

(1)设计一个抽象类 Duck

/* Duck.java */

public abstract class Duck{             //抽象类:鸭子
    public Duck(){
    
    }

    public abstract void display();     //外观
    
    public void quack(){                //嘎嘎叫
        System.out.println("--gaga--");
    }

    public void swim(){                 //游泳
        System.out.println("--swimming--");
    }
}

(2)绿头鸭 GreenHeadDuck 继承抽象父类 Duck

  1. 因为 quack 和 fly 方法在父类 Duck 中已经定义,子类 GreenHeadDuck 直接继承了这些方法,无需重新编写。
  2. 因为父类 Duck 中有抽象方法 display ,所以子类 GreenHeadDuck 需要重写 display 方法,这里我们通过重写 display 方法实现了不同鸭子(子类)外观的差异化。
/* GreenHeadDuck.java */

public class GreenHeadDuck extends Duck{            //子类:绿头鸭
    @Override
    public void display(){
        System.out.println("**GreenHeadDuck**");    //我是绿头鸭
    }
}

(3)同理,红头鸭 RedHeadDuck 也继承抽象父类 Duck

/* RedHeadDuck.java */

public class RedHeadDuck extends Duck{              //子类:红头鸭
    @Override
    public void display(){
        System.out.println("**RedHeadDuck**");      //我是红头鸭
    }
}

(4)模拟鸭子

/* StimulateDuck.java */

public class StimulateDuck {
    public static void main(String[] args) {
        GreenHeadDuck mGreenHeadDuck = new GreenHeadDuck(); //绿头鸭
        RedHeadDuck mRedHeadDuck = new RedHeadDuck();       //红头鸭
        
        //绿头鸭出场
        mGreenHeadDuck.display();
        mGreenHeadDuck.quack();
        mGreenHeadDuck.swim();
        
        //红头鸭出场
        mRedHeadDuck.display();
        mRedHeadDuck.quack();
        mRedHeadDuck.swim();
    }
}

程序输出结果如下:

绿头鸭嘎嘎叫和游泳,红头鸭嘎嘎叫和游泳。

**GreenHeadDuck**
--gaga--
--swimming--
**RedHeadDuck**
--gaga--
--swimming--


4.2 新需求

回顾一下原始的需求:

  1. 鸭子有 绿头鸭红头鸭 两种;
  2. 所有的鸭子都会 嘎嘎叫游泳

现在,我们需要新增一种 黑头鸭 ,而且 让所有鸭子都会飞

我们只需改动一小部分代码即可实现。

(1)修改父类 Duck

在父类 Duck 中增加 fly 方法,其它不变。这样使得所有鸭子都会飞。

/* Duck.java */

public abstract class Duck{             //抽象类:鸭子
    public Duck(){
    
    }
    
    public abstract void display();     //外观
    
    public void quack(){                //嘎嘎叫
        System.out.println("--gaga--");
    }

    public void swim(){                 //游泳
        System.out.println("--swimming--");
    }
    
    /* 增加 fly 方法 */
    public void fly(){                  //飞
        System.out.println("--flying--");
    }
}

(2)新增子类 BlackHeadDuck 继承抽象类 Duck

编写一个新的类 BlackHeadDuck ,并继承抽象类 Duck 。这样我们就增加了一种新的鸭子,而且它具有和其它鸭子类似的属性和操作。

/* BlackHeadDuck.java */

public class BlackHeadDuck extends Duck{            //子类:黑头鸭
    @Override
    public void display(){
        System.out.println("**BlackHeadDuck**");    //我是黑头鸭
    }
}

代码修改完毕,我们已经满足了新的需求,新增了一种黑头鸭,并让所有鸭子都会飞。

这个例子虽然很简单,但我们可以体会到,面向对象程序设计在扩展性方面具有不错的表现。


5 面向对象的高级进阶:设计模式


设计模式是软件开发人员在开发过程中解决问题的方案,这些解决方案是众多软件开发人员经过长时间的试验和错误总结出来的。设计模式是软件开发过程中的优秀实践,通常被有经验的面向对象的软件开发人员所采用。

注:设计模式是解决问题的思路,而不是具体的代码,它也不依托于特定的语言(如 Java、C++ 等)。强调一下,设计模式是一种软件开发思想。设计模式众多,本文无法一一展开,如有兴趣可查看本站专题:设计模式

还记得前面的例子吗?

我们想要让鸭子会飞,所以在父类 Duck 中增加了 fly 方法。但这样所有的鸭子都会飞,如果我们希望有的鸭子会飞,有的鸭子不会飞呢?

一种方案是在子类中重写 fly 方法。如此一来,每次增加新的鸭子,我们可能就需要重写子类的方法,来实现不同鸭子的差异化。

针对这个问题,策略模式提供了优秀的解决方案。

(1)通过接口封装行为(算法族)

FlyBehavior 接口(算法族):飞行行为族

/* FlyBehavior.java */

public interface FlyBehavior {      // FlyBehavior 行为族
    void fly();
}

GoodFlyBehavior 类(算法实现类)实现 FlyBehavior 接口(算法族):会飞

/* GoodFlyBehavior.java */

public class GoodFlyBehavior implements FlyBehavior {   //会飞
    @Override
    public void fly() {
        System.out.println("--GoodFly--");  
    }
}

BadFlyBehavior 类(算法实现类)实现 FlyBehavior 接口(算法族):不会飞

/* BadFlyBehavior.java */

public class BadFlyBehavior implements FlyBehavior {    //不会飞
    @Override
    public void fly() {
        System.out.println("--BadFly--");   
    }
}

我们通过 FlyBehavior 接口将飞行行为封装起来, FlyBehavior 就是我们所说的行为族(算法族)

GoodFlyBehavior 、 BadFlyBehavior 算法实现类是 FlyBehavior 行为族下的不同行为策略,分别对应会飞、不会飞。GoodFlyBehavior 、 BadFlyBehavior 算法实现类可被用来实例化不同的鸭子,这样就可以让鸭子有不同的行为策略,即有的鸭子会飞,有的鸭子不会飞。

(2)修改超类 Duck

接下来,我们需要修改父类 Duck 。值得注意的是,以后我们新增鸭子时不再需要修改父类 Duck 的代码,因为父类 Duck 已经按照策略模式封装好代码。

/* Duck.java */

public abstract class Duck{
    FlyBehavior mFlyBehavior;   //声明 FlyBehavior 接口的引用变量    
    
    public Duck(){
    
    }
    
    public void quack(){
        System.out.println("--gaga--");
    }

    public abstract void display();

    public void swim(){
        System.out.println("--swimming--");
    }
    
    //注意这里我们没有设定鸭子会不会飞,而是让 mFlyBehavior 去考虑
    public void fly(){
        mFlyBehavior.fly();     
    }
    
    //动态设置 FlyBehavior ,这里可根据 Duck 的子类来动态设置行为族
    public void SetFlyBehavior(FlyBehavior fb) {
        mFlyBehavior = fb;
    }
}

(3)在子类设定绿头鸭、红头鸭会不会飞

因为 GreenHeadDuck 和 RedHeadDuck 都继承了 Duck ,所以它们也继承了 FlyBehavior 类型的 mFlyBehavior 变量。我们可以在它们的构造器中根据不同的飞行行为来实例化 mFlyBehavior。不同的 FlyBehavior 实例对应不同的飞行行为,比如 GoodFlyBehavior(会飞)、BadFlyBehavior(不会飞)。

GreenHeadDuck 绿头鸭:会飞

/* GreenHeadDuck.java */

public class GreenHeadDuck extends Duck{
    public GreenHeadDuck() {
        mFlyBehavior = new GoodFlyBehavior();   //实例化为 GoodFlyBehavior(会飞)
    }
    
    @Override
    public void display(){
        System.out.println("**GreenHeadDuck**");
    }
}

RedHeadDuck 红头鸭:不会飞

/* RedHeadDuck.java */

public class RedHeadDuck extends Duck{
    public RedHeadDuck() {
        mFlyBehavior = new BadFlyBehavior();    //实例化为 BadFlyBehavior(不会飞)
    }
    
    @Override
    public void display(){
        System.out.println("**RedHeadDuck**");
    }
}

(4)测试

/* StimulateDuck.java */

public class StimulateDuck {
    public static void main(String[] args) {
        Duck mGreenHeadDuck = new GreenHeadDuck();  //绿头鸭
        Duck mRedHeadDuck = new RedHeadDuck();      //红头鸭
        
        //绿头鸭出场
        mGreenHeadDuck.display();
        mGreenHeadDuck.quack();
        mGreenHeadDuck.swim();
        mGreenHeadDuck.fly();
        
        //红头鸭出场
        mRedHeadDuck.display();
        mRedHeadDuck.quack();
        mRedHeadDuck.swim();
        mRedHeadDuck.fly();
        
        //前面在构造函数中定义鸭子会不会飞,这里动态的将绿头鸭从 GoodFly 变成 BadFly 
        mGreenHeadDuck.display();
        mGreenHeadDuck.SetFlyBehavior(new BadFlyBehavior());
        mGreenHeadDuck.fly();
    }

}

程序运行结果:

首先,绿头鸭和红头鸭都嘎嘎叫、游泳,但是绿头鸭会飞(GoodFly),红头鸭不会飞(BadFly)。

最后,我们动态地将绿头鸭从会飞设置成不会飞,所以绿头鸭输出了 BadFly 。

**GreenHeadDuck**
--gaga--
--swimming--
--GoodFly--
**RedHeadDuck**
--gaga--
--swimming--
--BadFly--
**GreenHeadDuck**
--BadFly--

这就是借鉴策略模式的解决思路。通过封装行为族,超类不考虑具体的行为,而让子类去选择各自的行为。如此一来,我们增加新的鸭子,超类的代码无需改动,子类也不需覆盖超类的代码,只需构造不同的实例即可实现子类的行为差异化


6 总结


本文简要介绍了面向对象的相关概念,并引入一个模拟鸭子的程序实例讲解面向对象程序设计的思路,最后通过设计模式进一步优化程序。

本文要点如下:

(1)面试对象思想将一组数据结构和处理它们的方法组成对象,把相同行为的对象归纳为类;通过类的封装隐藏内部细节;通过继承实现类的特化/泛化;通过多态实现基于对象的动态分派;
(2)面向过程:先确定算法,再确定数据结构;面向对象:先构造一个对象模型,将数据和方法组织在一起;
(3)类为对象提供了统一的抽象描述,包括属性和操作;对象由一组属性和施加于这些属性的操作构成,对象是类的具体实例;
(4)封装把对象的属性和操作结合成一个独立的系统单位,并尽可能隐蔽对象的内部细节;
(5)继承简化了人们对事物的认识和描述,有利于软件复用;
(6)多态是指同一个命名可具有不同的语义,主要实现机制有重写和重载;
(7)设计模式是解决问题的思路,而不是具体的代码,它也不依托于特定的语言,是软件开发过程中的优秀实践。


 
comments powered by Disqus