2006-04-12
TAG:.net

[如果转摘 请写明出处!]
让我们暂时忘记自己是一个程序员 把自己提升到一个学者的高度来看待OOP,请相信我 这对你看清楚OOp在软件开发中扮演什么角色有着超乎想象的帮助。
OOP是一种建模思想 一种方法论,在本质上它和佛教中的禅宗 并无二致,都是在寻求现实世界中诸多共通问题的解决之道。正因为这些问题都是共通问题,我们不得不需要上升到一个高度去用抽象方法来探讨解决之道。几千年来在哲学家们的努力下 我们发现并精炼出来很多方法论和思想。而这些方法论和思想又陆续被应用到各行各业,从而促进了社会 经济 文化 以及人类思想的进步。

软件业的发展和其他行业 特别是计算机硬件制造行业相比是远远落后其他行业兄弟的。原因何在?除了软件行业起步晚 是新兴行业外,我们领域内的行业执行者们对方法论的引入确实比其他同时出现的行业要晚。庆幸的是 软件行业里从来不缺乏勤奋者和天才,短短几十年来,我们从只能在纸条机上写0101 到现在的视窗多任务操作系统软件上开发同时服务于几千万人的应用程序,功劳除了软件从业者的不断发明创造 辛勤劳动外,从古到今的哲学家和方法主义者们也为我们作出了巨大的贡献,在此向他们以及精炼出十三种设计模式的"四人帮"致敬。

这里我们进入正文 我们的主题,还是试图用XMLOL的思维方式来思想问题吧!
"OOp是什么东西?" 能够问到这个问题 非常好 我们来解释一下OOP是什么!
OOP是英文缩写,英文全称为:Object Oriented Programming,解释成中文应该为"面向对象的程序设计",这是枯燥的书面解释。
你一定感觉很烦感 或者你已经习惯如此的介绍一个概念了。在XMLOL里我们应该有些与众不同(很多时候,我们确实与众不同)。

XMLOL习惯从一个概念的本源找出此概念的本质。OOP属于OO思想的范畴(即面向对象思想的范畴),而OO思想又 是什么呢?
如果这样一直问下去的话,那永远都会有问题的。

让我们用精炼的句子来提取出OO思想的本质:考虑问题的思路从现实世界的人类思维习惯出发,从问题领域中通过分析、精炼、细化、抽象出具有共同属性的抽象类。"考虑问题的思路而从现实世界的人类思维习惯出发"这句话正是OO思想的精髓所在。
此定义除了适用于OOA 、OOD、也同样适用于OOP思想。OOp和前两者的区别在于利用此思维方式具体应用到软件开发项目中的实现领域里来。

OOP四大特性:抽象 封装 继承 多态

抽象(abstract) :在问题领域内对具有大多共同属性 行为的事物单独提取出来的方法即叫抽象。
封装(Encapsulation):定义对象和操作,只提供抽象的接口,并隐藏它们的具体实现。
继承(Inheritance):通过继承现有类型的性质,创建新的数据类型,而不影响原有数据类型。
多态性(Polymorphism):判定数据类型集合中各类型的区别,使程序可以按照它们的共同特性来书写。

(注意:如果你真的理解OOP思想的话,对于四种特性的先后顺序是永远都不会颠倒的)

在OOP思想中 我们首先在问题领域中抽象出具有相同属性、行为的对象,在这里我们把此类的对象叫做抽象类(注意 在这里所说的抽象类并不等同于JAVA和C#中的抽象类)
我们总是这样来谈论"在我们面向对象的编程过程当中,只有实现了"封装" ,"继承"和"多态"才变的有意义!"
那么这个时候你会倔强的问:"为什么?为什么实现了封装 继承和多态才会有意义呢?告诉我真理 我才会信服!"
抱着这样坚持的信念才会让你速度的成长。
让我们用通俗明了的聊天般的语句来讲清楚为什么。

让我们回到现实世界当中,我来问你问题,问什么你买东西还要付钱?为什么你买飞机票还要排队? 不要太惊讶 我知道你会如此回答:"我们本来就应该买东西付钱、买飞机票就要排队啊,如果不付钱 警察叔叔就会抓你,不排队 飞机场的保安就会警告你"! 你看 你很明白 在现实世界中有很多规则 你必须去遵守 如果你违反了就会受到惩罚,有了法规的存在,才可以保护努力工作生产的人的劳动果实得到保护,如果没有法规,到处都会抢 杀 打,谁都不愿意工作 生产 都等着抢别人的,还谈什么进步、发展。
在软件业 同样如此,我们指定一系列规范,具有各种功能的类能够根据规定保护自己的某些属性或者方法,只给接口让外部调用。它保护了自己 同时对外简化了功能实现的复杂度。如果没有"封装"这样的规则的限制,类于类可以随便调用 随便拿其他类的成员属性 以及方法,那么继承就变的很烦琐了,我可以去拿别人的,为什么还这么麻烦的去中规中矩的继承别人的类中的所有东西呢,这给我带了很多麻烦。

现在你是否明白了只有实现了"封装" ,"继承"和"多态"才变的有意义了吗? 如果你说是的话 那我会感觉很自豪的 :)

我们轻松一下 然后再次上路 用武侠小说中的"正"道与"邪"道来人性化的描述"继承"和"多态"。
(注意:在这里我假定读者从多本书上已经了解到 继承 和多态的详细定义并大概的了解它们的使用和规则)

在C#和JAVA中的继承规则是派生类全部继承父类所有成员和方法,这样子类比父类具有更多成员和方法将变的合理,同时子类对父类是很紧密的,子类继承父类 就必须对继承下来的成员负责(零初始化或重写父类中的虚方法、抽象方法),往往这样继承下去 会形成一个单级家族链[在JAVA和C#中只能单继承]。

在如此的描述下我们来把"继承"引入到金庸爷爷的武侠世界中。
"继承"就好象 "武当"和"少林"两大门派一样,从门派第一代行侠仗义 誉满江湖,江湖上都认为次门派确实有存在的价值和意义,随着此门派一代的年老 和时代的更替,一代渐渐跟不上潮流,于是 他开始找接班人 并把所有的技能和思想、做人的道理都传给接班人。
二代得到一代所有的功夫和修为 又开创了新的功夫 继续在江湖上发扬此门派的教义 并去解决江湖纷争,时代更替 日月轮回 ,门派代代单传下来,N代掌门回眼去看他的前辈长辈们,感觉很多地方 他们掌握的功夫和做事情的效能太过于幼稚,但他会很尊重他们 并去执行前辈掌们流传下来的教义。这就是"正道"-----正道。

用专业术语来解释"多态"这个名词的话就是:相同的语意,不同的行为!大家对多态的概念了解的肯定很多,但是否真的理解"多态"这个概念呢?正所谓仁者见仁 智者见智了。在软件领域内特别是在高级语言中,对多态有很多表现实现。因为"继承"的实现,"多态"成为了可能 也更有意义,我们可以使用接口、虚方法、抽象类、等其他来实现多态。很多时候 我们定义一些类来继承其他类,但很多时候 我们发现 在很多类中都要使用同一个 动词 或者名词。这样的话 我们必须多次重复使用这个名词或动词当作属性名或者方法名,可以想象 在我们的程序当中 具有相同命名的 方法和属性 到处存在着,很多时候我们要找其中一个方法 但会迷茫于哪个是哪个。这还不是重点 重点是我们痛恨重复,这让我们看起来象钳工。
在这种情况下 我推荐你使用多态来避免此类重复的事情出现。

我们再次回到金庸爷爷的武侠世界中来讲述关于"多态"的故事

"多态"就象一个完全没有纪律组织的糟糕邪教,它们组织在一起原因是因为突然想到的需求才在一起的(不要和我争"突然想到"这个词用的是否合适),
邪教领袖看起来是权威 其实名存实亡,他存在的意义只是因为领袖这个位置的存在,他什么都不干 只管说话,即便是说了 他也不会管下面的人会不会去执行或者执行的就不是他所说的话,说实话 做这样的教主真的很可悲。而下面教众除了承认这个教主的存在外,对教主传达下来的命令,只是表面接受,然后按自己的需要去执行。我们需要有一个分析:教主有天发话:"去抢"(多么简单明了啊)。下面听到命令后,一窝蜂冲出去去抢,A教徒感觉自己现在缺一件衣服,就去抢衣服给自己穿,B教徒正打算给女朋友一个戒指,必然他抢的目标是戒指,而C教徒得到命令后 感觉这是不对的 他把自己吃的都给了可怜的穷人。A、B、C三人都执行了教主的命令,但各不相同,甚至C教徒都背道而驰了,而在教主一方是完全不理会的 他只管说话 才不管你们做什么呢,只要下面的人听到命令去干就行了,而干什么不是他管的。
这是可怕糟糕的邪教---反观我们的"多态"--也存在这样的情况(我在这里并没有污蔑"多态"的作用和存在意义。)

谈了这么多还是没有正式的提到OOP在软件开发中的实际应用,别急 我们正式引入

我相信大家很多人都是从面向过程的开发语言过度到JAVA AND C#的,我们在过去 用C ASP PHP来编写我们的逻辑,刚开始用C#或JAVA写程序的时候 我们总会感觉很难受,于是我们会怀念C的简洁性和高效性,喜欢C简练而表达能力丰富的风格,实在无法理解实现一个简单的功能还要写好多类,一个类调用另一个类,如此冗长的代码。。。。。。。

时间过去2个月 我们已经面对C#或者JAVA度过了如此长的时间了,自认为有所领悟,也开始有意识的运用OOP风格来写程序,然而还是经常会觉得不知道应该怎样提炼类,面对一个具体的问题的时候,会觉得脑子里千头万绪的,不知道怎么下手,一不小心,又会回到原来的思路上去。 在经过大量反复的学习 我们还是无法在一大堆问题中精练出来类 这实在是一个很头疼的问题。

[下面的例子及主要内容最初来源于《JAVA论坛》---在此声明]

举个例子,要发广告邮件,广告邮件列表存在数据库里面。倘若用C来写的话,一般会这样思考,先把邮件内容读入,然后连接数据库,循环取邮件地址,调用本机的qmail的sendmail命令发送。

然后考虑用C#来实现,既然是OOP,就不能什么代码都塞到main过程里面,于是就设计了四个类:

一个类是负责取邮件地址;
一个类负责读邮件内容,MIME编码成HTML格式的,再加上邮件头;
一个类负责将编码好的邮件发送到指定邮件地址
一个主类负责从命令读参数,处理命令行参数,调用发email的类。

把一件工作按照功能划分为四个模块分别处理,每个类完成一件模块任务。
除了主类,其他类都可以用不同的实现替换.比如一个通过数据库取地址列表的类,一个从文本取地址列表的类,实现相同的接口,那么主类根据需要选择相应的实现就可以了.

仔细的分析一下,就会发现这样的设计完全是从程序员实现程序功能的角度来设计的,或者说,设计类的时候,是自低向上的,从机器的角度到现实世界的角度来分析问题的。因此在设计的时候,就已经把程序编程实现的细节都考虑进去了,企图从底层实现程序这样的出发点来达到满足现实世界的软件需求的目标。

这样的分析方法其实是不适用于C#这样纯粹面向对象的编程语言,因为,如果改用C语言,封装两个C函数,都会比C#实现起来轻松的多,逻辑上也清楚的多。

所以到目前来说 我们不得不又要强调重复上面曾说过的一句话:"面向对象的精髓在于考虑问题的思路是从现实世界的人类思维习惯出发的,只要领会了这一点,就领会了面向对象的思维方法。"

举一个非常简单的例子:假使现在需要写一个网页计数器,客户访问一次页面,网页计数器加1,计数器是这样来访问的

后台有一个数据库表,保存每个id(一个id对应一个被统计访问次数的页面)的计数器当前值,请求页面一次,对应id的计数器的字段加1(这里我们忽略并发更新数据库表,出现的表锁定的问题)。

如果按照一般从程序实现的角度来分析,我们会这样考虑:首先是从HTTP GET请求取到id,然后按照id查数据库表,获得某id对应的访问计数值,然后加1,更新数据库,最后向页面显示访问计数。

现在假设一个没有程序设计经验的人,他会怎样来思考这个问题的呢?他会提出什么样的需求呢?他很可能会这样想:

我需要有一个计数器,这个计数器应该有这样的功能,刷新一次页面,访问量就会加1,另外最好还有一个计数器清0的功能,当然计数器如果有一个可以设为任意值的功能的话,我就可以作弊了。

做为一个没有程序设计经验的人来说,他完全不会想到对数据库应该如何操作,对于HTTP变量该如何传递,他考虑问题的角度就是我有什么需求,我的业务逻辑是什么,软件应该有什么功能。

按照这样的思路(请注意,他的思路其实就是我们平时在生活中习惯的思维方式),我们知道需要有一个计数器类 Counter,有一个必须的和两个可选的方法:

C#代码
getCount() // 取计数器值方法
resetCounter() // 计数器清0方法
setCount() // 设计数器为相应的值方法


把Counter类完整的定义如下:

C#代码
public class Counter {
public int getCount(int id) {}
public void resetCounter(int id) {}
public void setCount(int id, int currentCount) {}
}


解决问题的框架已经有了,来看一下如何使用Counter。 在count.ASPX里面调用Counter来计数,程序片断如下:

C#代码:
// 这里从HTTP环境里面取id值
...
Counter myCounter = new Counter(); // 获得计数器
int currentCount = myCounter.getCount(id); // 从计数器中取计数
// 这里向客户浏览器输出
...


程序的框架全都写好了,剩下的就是实现Counter类方法里面具体的代码了,此时才去考虑具体的程序语言实现的细节,比如,在getCount()方法里面访问数据库,更新计数值。

从上面的例子中看到,面向对象的思维方法其实就是我们在现实生活中习惯的思维方式,是从人类考虑问题的角度出发,把人类解决问题的思维方式逐步翻译成程序能够理解的思维方式的过程,在这个翻译的过程中,软件也就逐步被设计好了。

在运用面向对象的思维方法进行软件设计的过程中,最容易犯的错误就是开始分析的时候,就想到了程序代码实现的细节,因此封装的类完全是基于程序实现逻辑,而不是基于解决问题的业务逻辑。

学习JDBC编程的经典错误问法是:"我怎样封装对数据库的select操作?"

面向对象的设计是基于解决业务问题的设计,而不是基于具体编程技术的设计。我不会去封装select语句的,我只封装解决问题的业务逻辑,对数据库的读取是在业务逻辑的编码实现阶段才去考虑的问题。

回过头看上面那个发广告邮件的例子,应该如何应用面向对象的思维方法呢?

对于一个邮件来说,有邮件头,邮件体,和邮件地址这三个属性,发送邮件,需要一个发送的方法,另外还需要一个能把所有邮件地址列出来的方法。所以应该如下设计:

类JunkMail

属性:
head
body
address
方法:
sendMail() // 发送邮件
listAllMail() // 列邮件地址

用C#来表示:

public class JunkMail {
private String head;
private String body;
private String address;

public String getHead(){
return this.head;
}
......以下为以上三属于的get/set方法,将这个类做成一个纯数据模型类。
}

/**
然后将操作集中成一个静态方法类
*/
public class MailUtil{
private MailUtil(){} //私有静防止被new

//这里参数做了改变,将JunkMail类做为数据类传入
public static bool sendMail(JunkMail mail) {... }

//多加一个方法,来发送批量的mail
public static boo sendMail(Collection mails) {
for (Iterator it = mails.iterator(); it.hasNext();) {
sendMail((JunkMail )it.next());
}
}


//ChenGang:这里的Collection是一个包含JunkMail的集合。
public static Arraylist listAllMail() {....}
}




当把JunkMail设计好了以后,再调用JunkMail类完成邮件的发送,将是非常轻松的事情。

如果说传统的面向过程的编程是符合机器运行指令的流程的话,那么面向对象的思维方法就是符合现实生活中人类解决问题的思维过程。

在面向对象的软件分析和设计的时候,要提醒自己,不要一上来就去想程序代码的实现,应该抛开具体编程语言的束缚,集中精力分析我们要实现的软件的业务逻辑,分析软件的业务流程,思考应该如何去描述和实现软件的业务。毕竟软件只是一个载体,业务才是我们真正要实现的目标。

但是在设计过程中,心里却往往在担心,如果我完全不去考虑程序代码的实现的话,那么我怎么知道我的设计一定合理呢?我怎么知道我设计的类、接口一定可以实现呢?所以经常可以看到的现象就是:

在设计过程中,虽然知道不能过早考虑代码实现,但是每设计一个类,一个接口,心里都要不知不觉的用自己熟悉的编程语言大概的评估一下,看看能否编出来,因此,一不小心,就会又回到按照程序功能实现的思路进行设计的老路上去了。

举个例子来说明,在做Web程序设计的时候,经常要遇到分页显示数据的情况。比如说需要把系统中所有的用户都列出来这样的功能。假设使用User类来表示用户,增加用户addUser(),删除用户deleteUser(),查询所有用户listUsers()方法。而数据库中有一个user表,一条记录是一个用户的信息。下面考虑一下User类的方法的实现:

addUser()和deleteUser()方法都好实现,就是对数据库增加记录和删除记录。对于listUsers()方法,其实就是对user表的select,取出一个记录集。但是该怎么从listUsers()方法中得到所有用户的列表呢?

一个方法调用的返回值只有一个,没有多个,所以很多情况下采用的办法就是返回值定义为集合类型,比如Arraylist。这样就可以在listUsers()方法的具体代码实现的时候,从数据库依次取出一个个记录,插入到Arraylist里面来。在主程序里面,调用listUsers()方法可以返回一个Arraylist,然后再对Arraylist遍历操作,就可以得到用户列表了。

C#代码
public class User {

public static void addUser(...) {
// 数据库insert一条记录
}

public static void deleteUser(...) {
// 数据库delete一条记录
}

public Arraylist listUsers(...) {
// 数据库select结果放到一个集合里面
}
}


这样的设计基本合理,但是仍然有点小问题。因为在设计的时候,就考虑到了用C#的集合类Arraylist这样的设计基本合理,但是仍然有点小问题。因为在设计的时候,就考虑到了用C#的集合类Arraylist来实现对不定长数据集的存放,因而违反了面向对象设计的一个原则:在设计的时候不应过早的考虑具体程序语言的实现。所以必须用抽象的方法,和具体实现无关的方法来表达业务逻辑。

我们知道,通常对具有集合特征的数据结构进行遍历通常可以使用next方法,next实现取下一个用户, 因此我们定义一个接口Iterator,这个接口中定义next方法:
public interface Iterator {
public Object next() {}
}


而User类的listUses方法返回值改为Iterator接口的实现类:
public class User :Iterator
{
public Object next() {........}
public static Arraylist listAllMail() {....}
}
...
}


这样就把User类的设计和具体的实现方法分离开了,因为此时任何实现了next()的类都可以做为listUsers的返回值,都可以被用来表达"用户列表",而不仅仅可以使用Arraylist而已。比如,我可以用Hashtable来表达用户列表,因为Hashtable也实现了Iterator,当然我也可以自己专门写一个类来存放用户列表,只要实现next()就行了。

这样在具体的编写代码的时候,程序员具有了最大的灵活性,可以根据具体的情况,采用不同的编程方法来存放用户列表。特别是降低了程序的耦合度,提高了程序的可移植性。对于上面那个JunkMail的listAllMail()方法也同样应该改为接口类型。
这样就可以完全不用考虑程序代码实现了,从高层次上把功能抽象出来,定义成为接口,同时又可以把系统设计的很合理,完全根据业务的需求来进行设计。
通过上面的几个例子的设计说明,使用面向对象的思维方法,其实是一个把业务逻辑从具体的编程技术当中抽象出来的过程,而这个抽象的过程是自上而下的,非常符合人类的思维习惯,也就是先不考虑问题解决的细节,把问题的最主要的方面抽象成为一个简单的框架,集中精力思考如何解决主要矛盾,然后在解决问题的过程中,再把问题的细节分割成一个一个小问题,再专门去解决细节问题。

因而一旦牢牢的抓住了这一点,你就会发现在软件设计和开发过程中,你自己总是会不知不觉的运用面向对象的思维方法来设计和编写程序,并且程序的设计和开发也变得不再那么枯燥,而一个合理运用面向对象技术进行设计和架构的软件,更是具备了思维的艺术美感。

最后,愿面向对象的思维方法也能给您的程序设计之路带来创作的乐趣。





坯子 @ 10:06:09 | 引用 0 | 编辑



评论
 
tomking (http://江苏·) @ 2008-07-14 15:42:28
写的很好,理解的很透彻,谢谢你发表出来和我们共享!



 
太感谢了 () @ 2008-03-04 20:18:21
真的很牛,确实写的很好啊



 
四川人 () @ 2008-02-22 21:08:54
写的真好,看明白了。谢谢



发表评论
 姓名: 
 E-mail: 
 地址: