改变思维
曾经在学校学习数据结构课程时,第一节课上,老师就告诉我们:程序=数据结构+算法。这句话对我后来学习数据结构起了很大的作用,积极的作用。
可是后来学到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都没有影响,因此它符合依赖倒置原则。
版权声明:本文为博主原创文章,未经博主允许不得转载。