从面向过程到面向对象

时间:2022-12-03 19:50:44

改变思维

从面向过程到面向对象
曾经在学校学习数据结构课程时,第一节课上,老师就告诉我们:程序=数据结构+算法。这句话对我后来学习数据结构起了很大的作用,积极的作用。
可是后来学到C++面向对象部分时,这句话让我在有些地方怎么也想不通。想了很久之后,我得出了另一个结论,在面向对象程序中,程序=对象&行为。这里我使用&,是为了说明对象与行为是关系的。行为是对象的行为,对象要对它自己的行为负责。
这种思维上的转换在从PO到OO的过程中非常重要,下面我举个例子:
从面向过程到面向对象从面向过程到面向对象
假如使用PO的思维,我们想要实现“兔子走路”和“人走路”,我们会怎么做呢?
首先,写一个“兔子”结构体。struct rabbit{}r;
然后是一个“兔子走路”的算法。void rabbitWalk(rabbit r);
最后对“兔子”使用“兔子走路”算法。rabbitWalk(r);
“人走路”与“兔子走路”类似:

struct human{}h;
void humanWalk(human h);
humanWalk(h);

这就是数据结构+算法的思维。程序“兔子走路”=数据结构“兔子”+算法“兔子走路”。程序“人走路”=数据结构“人”+算法“人走路”。
如果数据结构“人”+算法“兔子走路”会怎样呢?

首先创建一个“兔子”对象class rabbit{};rabbit r;
由于兔子需要“走路”这一行为,因此向类中添加这一行为class rabbit{ void walk(){}};
让“兔子”对象执行它的“走路”行为。r.walk();
同样地,“人走路”的程序可以这样实现:

Class human : public animal
{void walk(){}};
human h;
h.walk();

在这里,是对象调用它自己的行为。两个“走路”行为虽然同名,但是它们是不相关的。由对象自己决定它们调用哪个“走路”行为,而程序员要做的,只是让对象执行它们的“走路”行为。人没有兔子的走路行为,所以让人用兔子走路的方法是不可能的。

面向对象

上文用一个简单的例子说明了从PO到OO的思维转变,下面开始介绍一些OO相对于PO来说所具体的一些特性。网上能查到相关术语的解释,这里不会详细展开。

对象和类

对象是与现实世界相关的一个实例,而类是对对象的抽象。注意,在面向对象中,通常是先有对象再有类的。首先是我们需要一个对象,就把它抽象成一类,丰富它的行为。然后再使用这个类实例化出一个对象来使用。
类不一定是名字,也可以是动词。
比如一只“喜羊羊”是一个对象,抽象出来的羊是一个类。
喜羊羊“从学校走到村长的家”也可以是一个对象,抽象出走路这个类。

消息传递与动态分发

请看这样两行代码

xiyangyang.walk(hua_ban);
xiyangyang.walk(bike);

消息传递是指,当程序想要喜羊羊用滑板走路时,就使用这条语句,给xiyangyang发送一条walk的消息,并带上hua_ban参数。
那么这个消息会怎么处理呢?这个我们不需要关心,因为这是由xiyangyang决定的。这就是动态分发。

封装

PO也有封装的概念,不过OO做得更好。

组合与继承

也许大家对继承比较熟悉,对组合比较陌生。这里把它们放在一起作比较,是想说明它们的能实现的作用相似,但是目的却不同。
当我们想给一个类增加行为时,通常想到的是继承。但我作为,继承最主要的作用是提供统一接口,而在功能扩展方面更推荐使用组合。
从面向过程到面向对象
比如原本有一个类叫“羊”。但现在需要一个“会说话的羊”的对象,要怎么做呢?
我们很容易就想到一个解决方案:
从面向过程到面向对象
这是一个继承的例子,就好比这种羊进化出一个新的具有speak行为的品种。
其实它还有一种实现方式,就是利用组合:
从面向过程到面向对象
如果把继承比喻成进化,那么组合就可以这么解释:可以这么解释。这种羊还是不会说话,但是它得到了一个发声器,利用这个发声器的speak行为替羊说话。

多态

从面向过程到面向对象
多态是这里面最抽象最难解释的概念,所以这里通过一个故事来说明:
大森林里要举行跑步比赛,小动物们都来的报名。

  • PO:

大象:我是大象,我来报名。 管理员:好的,请你用大象跑步的方式参加比赛。
犀牛:我是犀牛,我来报名。 管理员:犀牛?应该是一种牛吧?请你用牛跑步的方式参加比赛。
小黄牛:我是小黄,我来报名。管理员:小黄是什么?没有“小黄跑步”这种跑步方式,你不能参加跑步比赛。
结局:大象完成了比赛。犀牛中途退赛了,因为牛的跑步方式不适合它。小黄牛没有参赛比赛。

  • OO:

管理员:我不管你是什么动物,也不管你以什么方式跑步。只要你有跑步这种行为,你就可以参加比赛,并且以你自己的跑步方式跑步。
结局:大家都参加了比赛,且以自己的方式完成了比赛。

C++

C++在C的基础做了许多功能上的延伸,但是本文的主题是面向对象,其它方面就不作说明。C++作为一种面向对象语言,必然支持上述的面向对象特性。下面将用例子证明:

对象和类

class animal
{
    string name;
public:
    animal(string n):name(n){}
    void introduce(){cout<<"my name is "<<name;}
    void walk(){cout<<"default walk way"<<endl;}
};

class sheep : public animal
{
public:
    sheep(string n):animal(n){}
    void walk(){introduce();cout<<"walk like a sheep"<<endl;}
};

class human:public animal
{
public:
    human(string n):animal(n){}
    void walk(){introduce();cout<<"walk like a human"<<endl;}
};

void test()
{
    sheep A("xiyangyang");
    human B("xiaohuangren");
    A.walk();
    B.walk();
}

运行结果

my name is xiyangyangwalk like a sheep
my name is xiaohuangrenwalk like a human

在本例中:animal、sheep、human都是类,而是A和B对象。

继承

从面向过程到面向对象

class animal
{
    string name;
public:
    animal(string n):name(n){}
    void introduce(){cout<<"my name is "<<name;}
    void walk(){cout<<"default walk way"<<endl;}
};

class sheep : public animal
{
public:
    sheep(string n):animal(n){}
    void walk(){introduce();cout<<"I walk like a sheep"<<endl;}
    void speak(){introduce();cout<<"sorry, I can't speak"<<endl;}
};

class de_dao_gao_yang : public sheep
{
public:
    de_dao_gao_yang(string n):sheep(n){}
    void speak(){introduce();cout<<"I'm de dao gao yang, I can speak"<<endl;}
};

void test()
{
    de_dao_gao_yang A("yang da xian");
    A.speak();
    A.walk();
    sheep B("normal sheep");
    B.speak();
    B.walk();
}

运行结果:

my name is yang da xianI'm de dao gao yang, I can speak
my name is yang da xianI walk like a sheep
my name is normal sheepsorry, I can't speak
my name is normal sheepI walk like a sheep

在本例中,A羊通过继承实现了说话功能,B羊是普通羊,没有说话功能。但A和B的走路方式是一样的。

组合

从面向过程到面向对象

class speaker
{
public:
    void say_a_word(string word)
    {
        cout<<word<<endl;
    }
};

class animal
{
    string name;
public:
    animal(string n):name(n){}
    void introduce(){cout<<"my name is "<<name;}
    void walk(){cout<<"default walk way"<<endl;}
};

class sheep : public animal
{
    speaker *hu_die_jie_fa_sheng_qi;
public:
    sheep(string n, bool can_speak = false)
        :animal(n),hu_die_jie_fa_sheng_qi(NULL)
    {
        if(can_speak == true)
            hu_die_jie_fa_sheng_qi = new speaker();
    }
    ~sheep()
    {
        if(hu_die_jie_fa_sheng_qi != NULL)
            delete hu_die_jie_fa_sheng_qi;
        hu_die_jie_fa_sheng_qi = NULL;      
    }
    void walk(){introduce();cout<<"I walk like a sheep"<<endl;}
    void speak()
    {
        introduce();
        if(hu_die_jie_fa_sheng_qi == NULL )
            cout<<"sorry, I can't speak"<<endl;
        else
            hu_die_jie_fa_sheng_qi->say_a_word("this is a speaker, hello");
    }
};

void test()
{
    sheep A("xiyangyang", true), B("nuanyangyang");
    A.speak();
    A.walk();
    B.speak();
    B.walk();
}

运行结果

my name is xiyangyangthis is a speaker, hello
my name is xiyangyangI walk like a sheep
my name is nuanyangyangsorry, I can't speak
my name is nuanyangyangI walk like a sheep

在本例中,A羊通过组合实现了说话功能,B羊是普通羊,没有说话功能。但A和B的走路方式是一样的。

多态

从面向过程到面向对象

class animal
{
    string name;
public:
    animal(string n):name(n){}
    void introduce(){cout<<"my name is "<<name;}
    virtual void walk(){cout<<"default walk way"<<endl;}
};

class sheep : public animal
{
public:
    sheep(string n):animal(n) { }
    void walk(){introduce();cout<<"I walk like a sheep"<<endl;}
};

class human : public animal
{
public:
    human(string n):animal(n) { }
    void walk(){introduce();cout<<"I walk like a human"<<endl;}
};
class monkey : public animal
{
public:
    monkey(string n):animal(n) { }
    void walk(){introduce();cout<<"I walk like a monkey"<<endl;}
};
class duck : public animal
{
public:
    duck(string n):animal(n) { }
    void walk(){introduce();cout<<"I walk like a duck"<<endl;}
};
class pig : public animal
{
public:
    pig(string n):animal(n) { }
    void walk(){introduce();cout<<"I walk like a pig"<<endl;}
};

void test()
{
    animal * runner[5];
    runner[0] = new sheep("xi yang yang");
    runner[1] = new human("tai shan");
    runner[2] = new monkey("kong kong");
    runner[3] = new duck("tang lao ya");
    runner[4] = new sheep("zhu ba jie");
    for(int i = 0; i < 5; i++)
        runner[i]->walk();
    for(int i = 0; i < 5; i++)
        delete runner[i];
}

运行结果:

my name is xi yang yangI walk like a sheep
my name is tai shanI walk like a human
my name is kong kongI walk like a monkey
my name is tang lao yaI walk like a duck
my name is zhu ba jieI walk like a sheep

在本例中,runner只是一个animal类型的指针,并不知道它实际指向的是什么动物(只有在开始实例化的时候知道,使用的时候是不知道的),但它仍然会调用具体动物类对应的行为。

面向对象设计原则

clean code的宣传者Uncle Bob提出了面向对象设计的五个原则,简称为SOLID。但在我看来,OO设计最重要的原则就是分析可能发生的变化以及如何响应变化。

SRP - Single responsibility principle(单一职责)

单一职责原则是指,一个类,只有一个原因引起它的改变。
先看这样一个类,它符合单一职责原则吗?
从面向过程到面向对象
正如上文所说,在评价一个OO的设计是否合理时,一个很重要的前提就是推测变化。是在分析这个设计所对应的需求中,哪些地方是有很有可能发生变化的。没有这个前提的评价都是片面武断的。
在本例中,我们假设对于这个羊类来说,它支持哪些行为是容易发生改变的,它叫的方式也是容易发生改变的,那么这个类至于有两个原因导致它的修改,因此不符合单一职责原则。
我们可以怎么修改这个设计呢?一个很常用的方法就是封装变化。在这里我们把“叫”这个容易发生的行为封装起来,封装成另一个类speaker。这样,当发声的方式改变时,只需要修改speaker类就可以了。
从面向过程到面向对象

OCP - Open-Closed Principle(开放封闭)

开放-关闭原则是指,对扩展开放而对修改关闭。也就是说,尽量通过不修改代码的方法能够扩展类的功能,这听起来有点奇怪,但如果设计得好,是可以做到的。比如上一个例子中,把发声的方法提取出来,那么就可以扩展发声的方法而不需要改变sheep类了。
在这次例子中,我们的需求又发生了变化。我们想要这只羊发出mou的声音,而其它(比如发声的频率、单调)则保持不变。我们很快想到了解决方案
从面向过程到面向对象

LSP - Liskov Substitution Principle(李氏代换)

李氏代换是指,在任何可以使用基类的地方,都可以使用派生类代替它。这点似乎是理所当然,既然派生类继承基类,那么基类该有的行为派生类当然会有了,这有什么可说的呢?但是参考上面这个例子,虽然派生类继承了基类的所有行为,但不是说这些继承来的行为对于派生类都是有意义的。比如这个高级发声器,其实它并不支持mie这个行为,因此不满足李氏代换。怎么改进呢?考虑一下这个方案:
从面向过程到面向对象
其实李氏代换,可以理解为基类对于派生类来说没有多余的接口。我们把mie和mou并列放置,并同时继承speaker,父类只是提供接口,而子类实现行为。
从这个例子可以看出,继承和组合虽然相似,却用法不同。继承通常中为了统一接口,而组合可用于扩展功能。

ISP - Interface Segregation Principle(接口隔离)

接口隔离:即使用客户端特定的细粒度接口。
在定义接口时,我们是定义一个大的接口呢还是定义多个小的接口好呢?在这里,我们推荐后者。
多个小接口也许会造成多个接口难以管理,但它带来的好处远多于它的缺点。
当一个接口很大,需要很多参数时,常常是为了适应多种情况。对于某些用例只用到这几个参数而有些用例只用到那几个。
假如某接口有参数A,B,C,D,E,F,G。但某个应用场景其实只用到A,B,C。那么当接口的参数D部分改变时,其实对是这个用例没有关系的。但用例不得不由于这种情况更改它的调用方式,这样不是合理的。
没有想到合适的例子。

DIP - Dependency Inversion Principle(依赖倒置)

依赖倒置是指,我们要依赖抽象而不是具体实现。因为OO最大的特点就是擅长于响应变化。上文说过,我们要封装变化。那么抽象和具体实现,哪个更容易变化呢?当然是具体实现。
在上面这个例子中,speaker是抽象,mie和mou是实现。sheep现在依赖的是speaker,因此不管我们使用的mie发声法还是mou发声法,对于sheep都没有影响,因此它符合依赖倒置原则。