面向对象之羊村运动会
这篇主要说的是利用面向对象方法给具体的场景来建模并实现程序。同时也会演示建模的迭代过程以及在这过程当中会明显犯的错误以及如何解决这些问题的方法。
羊村要举办一个动物运动会,喜羊羊,懒羊羊以及灰太郎还有机器羊等都报名参赛。参赛的规则是谁能最快到达终点就是赢。在赛跑开始以后,大家都是一个大步往前冲。开始后只有机器羊还在准备,看看机器羊在准备什么,原来机器羊可以变形成电动车,机器羊开着车去。这个时候灰太郎想开着也行,马上就回家去把自己的飞机搬来,开着飞机去。而这时的懒羊羊就对喜羊羊对,我们别跑了,赶不上他们啦,还不如睡觉呢。喜羊羊想也是,他们一个开电动车,一个开飞机,我有孙悟空的本事就才能赢他们了。突然灵光一闪,有没有规定不可以直接到终点,也就是我不往前,反而往后到达终点(这里假设起点和终点在一起),那我就得第一。
场景分析
在分析这个场景的时候,我们发现真的是“八仙过海,各显神通”。基本会想到有羊羊可以作为一个类,这里可以包括喜羊羊,兰羊羊以及机器羊。灰太郎也归为一类。可以看出这样的分类法是基于生物学上的分类。因此可以得到下面的类图。
首先看看我们前面的故事并从中提取行为,比如机器羊是开电动车的。喜羊羊是直接到达终点的,灰太郎是开飞机的。这个时候发现,机器羊也是羊,不过他的行为不一致,所以再给羊类加一个子类叫机器羊类。接着在给这些动物们加上行为。
namespace GameApplication
{
class Animal
{
public string Name { get; set; }
}
}
namespace GameApplication
{
class Sheep : Animal
{
// 喜羊羊是直接到终点的
public void Back()
{
Console.WriteLine("喜羊羊是直接到终点的");
}
}
}
namespace GameApplication
{
class Wolf: Animal
{
// 灰太郎是开飞机的
public void Fly()
{
Console.WriteLine("灰太郎是开飞机的");
}
}
}
namespace GameApplication
{
class RobotSheep:Sheep
{
// 机器羊是开电动车的
public void Drive()
{
}
}
}
场景实现
然后开始建一个运动会类Game,并实现Register以及Start。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace GameApplication
{
class Game
{
private List<Animal> players;
public void Register()
{
players = new List<Animal>();
// 喜羊羊和懒羊羊参赛
Sheep player1 = new Sheep();
player1.Name = "喜羊羊";
players.Add(player1);
Sheep player2 = new Sheep();
player1.Name = "懒羊羊";
players.Add(player2);
// 机器羊
RobotSheep player3 = new RobotSheep();
player3.Name = "机器羊";
players.Add(player1);
// 灰太郎
Wolf player4 = new Wolf();
player4.Name = "灰太郎";
players.Add(player4);
}
// 关键问题在这里
public void Start()
{
foreach (Animal player in players)
{
Type type = player.GetType();
switch (type.ToString())
{
case "GameApplication.Sheep":
Sheep sheep = player as Sheep;
sheep.Back();
break;
case "GameApplication.RobotSheep":
RobotSheep robotSheep = player as RobotSheep;
robotSheep.Drive();
break;
case "GameApplication.Wolf":
Wolf wolf = player as Wolf;
wolf.Fly();
break;
default :
break;
}
}
}
}
}
我们运行程序看看,得到如下的结果.
喜羊羊是直接到终点的
喜羊羊是直接到终点的
机器羊是开电动车的
灰太郎是开飞机的
基本上结果是正常的。接着要改善现在的程序实现,有的人说你把Start重构一下,那么多的Switch,肯定是Switch出了问题。
场景重构1
那么现在就来改改Game中的Start方法,把Game判断类型,然后分别调用不同的行为接口移到相应的类型中去定义。
public void Start()
{
foreach (Animal player in players)
{
player.Run();
}
}
上面的Start方法看起来漂亮了好多,简单直接。接下来在看看羊类,机器羊类以及灰太郎类怎么实现。
namespace GameApplication
{
class Sheep : Animal
{
// 喜羊羊是直接到终点的
public void Back()
{
Console.WriteLine("喜羊羊是直接到终点的");
}
public override void Run()
{
Back();
}
}
}
namespace GameApplication
{
class RobotSheep:Sheep
{
// 机器羊是开电动车的
public void Drive()
{
Console.WriteLine("机器羊是开电动车的");
}
public override void Run()
{
// base.Run();
Drive();
}
}
}
namespace GameApplication
{
class Wolf: Animal
{
// 灰太郎是开飞机的
public void Fly()
{
Console.WriteLine("灰太郎是开飞机的");
}
public override void Run()
{
Fly();
}
}
}
果然世界清静了很多。我们再来看一下机器羊的实现。
namespace GameApplication
{
class RobotSheep:Sheep
{
// 机器羊是开电动车的
public void Drive()
{
Console.WriteLine("机器羊是开电动车的");
}
public override void Run()
{
// base.Run();
Drive();
}
}
}
注意一道这里加粗的部分//base.Run()是覆盖了父类的行为,因为是违反了里氏原则(也就是子类不应该覆盖父类的实现,而应该扩展)。有的人会反问,不这样覆盖,怎么实现啊?的确是的,现在的结构决定了机器羊只能违反原则覆盖父类的实现。
场景重构2
既然出现了问题,我们再来看看到底是那个过程出了问题,看看半天,原来是把机器羊作为羊的子类是错误的!
假设机器羊与羊是一种继承关系,那么机器羊就应该完全符合羊类的行为,而机器羊偏偏又不符合这种行为,所以行为在场景中不一样,即使是生物学上的分类一样也必须新建一个类别而不是采用继承,更何况机器羊不见得是羊的一个种族。
找到了问题,那么就来改改机器羊的结构与实现。
namespace GameApplication
{
class RobotSheep:Animal
{
// 机器羊是开电动车的
public void Drive()
{
Console.WriteLine("机器羊是开电动车的");
}
public override void Run()
{
Drive();
}
}
}
通过上面的修改以后,机器羊没有违反面向对象的里氏原则。而且程序运行也是和上面的结果一致。
接着再来看问题。我们所有的动物类型的Run行为都是调用自己本身的骑电动车,开飞机等。这个为何会出现这样的实现方式。
主要是在抽象动物一类的时候没有很好的抽象动物在赛跑中的行为。也就是只注重了个体的特殊性,而没有抽象出整体的一般性,才会导致我们走了很多的冤枉路。
解决方法
接下来从上面的例子看建模中一般犯的小小的错误,可能大家都不以为然。可是这是因为这种小小的错误导致大家去不断修改才能得到一个真正面向对象的程序。
问题类型1: 总是喜欢判断对象的类型,然后根据类型去和对象交互
这个例子就是上面的Switch语句
解决方法: 要做到对象有类而无型。也就是可以给对象定义一个类,但是在具体操作这个类的对象实例的时候不要关注对象的类型。只有这样才能真正面向对象。这个也是很多面向对象程序的最基本的问题,也是最常见的问题。
问题类型2:分类错误导致子类需要覆盖父类的行为
这个例子是机器羊是羊的一个子类问题
解决方法: 不服从父类的子类应该把子类建立另外一个和父类平行的类。我们也往往会犯这个错误,喜欢子类去覆盖父类的行为。
实际上问题的根本是因为面向对象的抽象没有理解好。在抽象过程中,我们可能会过多地关注对象的对象类型结构比如如何继承啊,而忽视了对对象行为的抽象。假如在前面把喜羊羊,机器羊和灰太郎抽象的时候加入开电动车,开飞机等行为的抽象,一步到位抽象成赛跑行为,那么就不会出现机器羊的开电动车方法,灰太郎的开飞机方法。也就是抽象过程中,抽象出了个性与共性,但往往关注了个性细节。而在程序运行时候又过多关注对象的类型导致需要检查对象的类型。
总之一句话: 面向对象先抽象,外部对这个抽象类看到的是共性,对象内部注重的是个性而有不同的实现方法。在与外部对象交互中,外部对象不要企图知道对象内部怎么做,只要告诉内部对象你要做什么,然后内部对象发挥自己的个性,这就是面向对象的封装机制。
从中我们也看到面向对象的抽象和封装的不同体现,抽象是一个静态过程也就是注重定义,封装在运行时候表现的特别好,让外部不用关注自己的实现。
后面的文章会关注封装。