[OO]深入理解面向对象软件设计(一) —— 从具体例子谈起

    java和C#通常被认为是完全面向对象的语言,所有基本代码必须写在某个类中。但是,很多java和C#程序员编写的代码并不是真正面向对象的。有这种事?确实有,面向对象的编程语言只是提供了封装、继承和多态的机制,并不能保证我们用它写出的程序是面向对象的,即使我们把“人”和“狗”的代码糅合在一起,也不会导致编译和运行出错,我们来看一个c#编写的“人与狗的故事”:     

 


    class Program
{
static void Main(string[] args)
{
Story story = new Story();
Console.WriteLine(story.GetStory1());
Console.Read();
}
}
public class Story
{
private string dogName = "Aliths";//狗的名字        
        private string masterName = "Peter";//狗主人的名字        
        private string masterBabyName = "Jim";//狗主人的baby的名字        
        private string friendA = "Borbo";//A朋友的名字        
        private string babyA = "LiLy";//A朋友的baby的名字        
        public string GetStory1()
{
StringBuilder storyStr = new StringBuilder("         人与狗的故事\r\n\r\n");
storyStr.Append(string.Format("    傍晚,{0}带着儿子{1}和爱犬{2}出门散步,"
, masterName, masterBabyName, dogName));
storyStr.Append(string.Format("遇到了熟人{0},{0}带着女儿{1}在玩。\r\n"
, friendA, babyA));
storyStr.Append(string.Format("    {0}将{1}和{2}交给{3}照看,"
, masterName, masterBabyName, dogName, friendA));
storyStr.Append("自己去附近商店买烟。");
storyStr.Append(string.Format("这时{0}咬了{1}。{1}大哭,{2}气得遍地找板儿砖,"
, dogName, babyA, friendA));
storyStr.Append(string.Format("然后一板儿砖下去将{0}拍得满地找牙。\r\n"
, dogName));
//……            

storyStr.Append(string.Format("\r\n\r\n……"));
return storyStr.ToString();
}
}

 

    这段代码包含两个类Program和Story,单纯从语言层面讲,是面向对象的。但Story中把故事、狗和多个人等的代码糅合在一起(如果故事情节涉 及板砖和商店的细节,还会更乱,而且不仅用到人的名字,还有年龄穿着等,故事有时间地点等等,为例子简单,没有提及),称为面向对象的设计,完全说不过 去。

    如果需求不发生变化,这段代码不会有太大问题,没必要把类分的那么清楚。但需求还是变了,要求增加一个故事二:狗主人在另一天出门时遇到了朋友B,给B讲述了发生在前几天的故事一,并且讲述中他添油去醋,并没有按故事一的实际情节讲,我们来看代码:

 


    class Program
{
static void Main(string[] args)
{
StructStory story = new StructStory();
Console.WriteLine(story.GetStory1(1));//GetStory1增加了int参数

Console.WriteLine(story.GetStory2());//GetStory1增加了int参数

Console.Read();
}
}
public class StructStory
{
private string dogName = "Aliths";//狗的名字
        private string masterName = "Peter";//狗主人的名字
        private string masterBabyName = "Jim";//狗主人的baby的名字

private string friendA = "Borbo";//A朋友的名字
        private string babyA = "LiLy";//A朋友的baby的名字

private string friendB = "Jance";//B朋友的名字

public string GetStory1(int storyIndex)
{//为了给GetStory2调用,增加参数storyIndex,并根据不同参数进行不同的讲述

StringBuilder storyStr = new StringBuilder("         人与狗的故事\r\n\r\n");
storyStr.Append(string.Format("    傍晚,{0}带着儿子{1}和爱犬{2}出门散步,"
, masterName, masterBabyName, dogName));
storyStr.Append(string.Format("遇到了熟人{0},{0}带着女儿{1}在玩。\r\n"
, friendA, babyA));
if (storyIndex == 2)
{
//添油……
            }
if (storyIndex != 2)
{//去醋……

storyStr.Append(string.Format("    {0}将{1}和{2}交给{3}照看,"
, masterName, masterBabyName, dogName, friendA));
storyStr.Append("自己去附近商店买烟。");
}
storyStr.Append(string.Format("这时{0}咬了{1}。{1}大哭,{2}气得遍地找板儿砖,"
, dogName, babyA, friendA));
storyStr.Append(string.Format("然后一板儿砖下去将{0}拍得满地找牙。\r\n"
, dogName));
//……
            storyStr.Append(string.Format("\r\n\r\n……\r\n\r\n"));
return storyStr.ToString();
}
public string GetStory2()
{
StringBuilder storyStr = new StringBuilder("      人与狗的故事2\r\n\r\n");
storyStr.Append(string.Format("    第二天,{0}出门时遇到了熟人{1},"
, masterName, friendB));
storyStr.Append("给他讲了狗咬人的故事:\r\n");
storyStr.Append(this.GetStory1(2));
return storyStr.ToString();
}
}

 

    这时代码已经变得无法忍受了,如果需求再变,不堪设想。也许有的朋友要说,需求不至于一直变吧,我想说:需求不变才不正常,因为需求是人类思想的反映,人的想法是不断变化的,整个世界也在变化。高楼大厦之所以没有经常拆了重建,并不是人们对它满意,而是反复拆建需要大量时间和金钱。而 软件相对于建筑来说,只有设计过程(写文档和编码都是设计),没有建造过程,真正的建造过程是在代码写完后由计算机瞬间完成(编译成二进制exe、 dll),所以人们只要不满意就可能让它“重建”。可以设想一下,假如代码完成后要由人工打纸带(像最初的程序,0打孔,1不打)来编译,或许随便找一个 B/S程序够一个人打几年的,这时需求还是会变,但不会要求程序员去修改了——将就着用吧,不然就洗洗睡先。

    认同了需求是变化的,就要找办法解决问题,面向对象相对于面向过程的主要优势之一就在应对变化上,可能也是面向对象在系统软件开发方面流行的主要原因。上述代码有些过于初级,有一定经验的程序员可能会把代码写成这样: 

     Story有发生时间、地点等属性和GetStory方法,Person有被咬(被咬后的反映)和打狗等方法,如果故事情节涉及Brick(板砖)和 Shop(商店)的细节,还会有这两个类,这样基本有了貌似单一职责的几个类。其实,这仅仅是对代码的“归类”,没有做到单一职责,比如Person中的 BefallBite(被狗咬)方法会遇到这样的问题:

    如果Person实例是小孩,反映可能是坐在地上大哭

    如果是大人,可能是找板砖

    如果是狗主人家的小孩,狗可能只是轻咬他玩得,不会哭,会跟狗一起玩

    如果是大人,又是狗主人被咬,不一定舍得板砖拍……

    ……

    显然,Person还应该有一些子类:

 

    这样就基本有了职责单一的类结构。但我们还是发现了问题,Person的子类比较多,如果再加上“根据被狗咬的部位不同反映不同”(Baby被咬屁股不会 坐地上哭,可能是趴着哭;大人被咬右手可能无法拿板砖),那么子类将会更多。这时仅仅靠最基本的单一职责原则及其它几条原则已经不能做出良好的面向对象设 计,设计模式就是在面向对象的基础上进一步提高软件应对变化能力的“良药”。本例是典型的桥接模式应用场景,笔者将在后续博文中用更复杂更完整的例子和大家共同学习设计模式的综合运用。

 

PS 1:

    本文提到的三种编码方式应该可以代表三种编码阶段,在结尾提到了设计模式,其实在知道用设计模式后还有三个阶段,纯属个人看法:

    1、在单一职责等基本原则做的不太好时就接触了设计模式,根据各种模式定义的场景大量运用。这种阶段去做大项目,遇到的问题往往比最初级的混合编码阶段还多。

    2、基本功底打得扎实后,逐步学习运用设计模式,遇到问题能够根据设计模式定义进行思索,合理解决问题

    3、设计模式的定义经常不记得,设计和编码时只根据基本原则进行,遇到问题就重构代码,重构后发现好像和某种模式定义的场景类似,查书后确定是一样的……

   

    呵呵,设计模式最好不要强求,自然形成就好。

 

PS 2:

    刚开始写博客,对编辑器不熟悉,我贴的代码好好的,发布后对齐格式有点乱,反复几次搞不定;还有贴个类图要先截图存gif,再传上来,很麻烦。请高手留言指点,多谢!我用的编辑器是 TinyMCE(推荐)

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏