|
已锁定 主题:面向对象的思维方法
该帖已经被评为精华帖
|
|
|---|---|
| 作者 | 正文 |
|
时间:2003-09-13
面向对象的思维方法
作者:范凯 E-mail: robbin_fan@yahoo.com.cn 我是从学习Java编程开始接触OOP(面向对象编程),刚开始使用Java编写程序的时候感觉很别扭,因为我早以习惯用C来编写程序,很欣赏C的简洁性和高效性,喜欢C简练而表达能力丰富的风格,特别忍受不了Java运行起来慢吞吞的速度,相对冗长的代码,而且一个很简单的事情,要写好多类,一个类调用一个类,心里的抵触情绪很强。 我对Java的面向对象的特性琢磨良久,自认为有所领悟,也开始有意识的运用OOP风格来写程序,然而还是经常会觉得不知道应该怎样提炼类,面对一个具体的问题的时候,会觉得脑子里千头万绪的,不知道怎么下手,一不小心,又会回到原来的思路上去。 举个例子,要发广告邮件,广告邮件列表存在数据库里面。倘若用C来写的话,一般会这样思考,先把邮件内容读入,然后连接数据库,循环取邮件地址,调用本机的qmail的sendmail命令发送。 然后考虑用Java来实现,既然是OOP,就不能什么代码都塞到main过程里面,于是就设计了三个类: 一个类是负责读取数据库,取邮件地址,调用qmail的sendmail命令发送; 一个类是读邮件内容,MIME编码成HTML格式的,再加上邮件头; 一个主类负责从命令读参数,处理命令行参数,调用发email的类。 把一件工作按照功能划分为3个模块分别处理,每个类完成一件模块任务。 仔细的分析一下,就会发现这样的设计完全是从程序员实现程序功能的角度来设计的,或者说,设计类的时候,是自低向上的,从机器的角度到现实世界的角度来分析问题的。因此在设计的时候,就已经把程序编程实现的细节都考虑进去了,企图从底层实现程序这样的出发点来达到满足现实世界的软件需求的目标。 这样的分析方法其实是不适用于Java这样面向对象的编程语言,因为,如果改用C语言,封装两个C函数,都会比Java实现起来轻松的多,逻辑上也清楚的多。 我觉得面向对象的精髓在于考虑问题的思路是从现实世界的人类思维习惯出发的,只要领会了这一点,就领会了面向对象的思维方法。 举一个非常简单的例子:假使现在需要写一个网页计数器,客户访问一次页面,网页计数器加1,计数器是这样来访问的 [code:1]http://hostname/count.cgi?id=xxx[/code:1] 后台有一个数据库表,保存每个id(一个id对应一个被统计访问次数的页面)的计数器当前值,请求页面一次,对应id的计数器的字段加1(这里我们忽略并发更新数据库表,出现的表锁定的问题)。 如果按照一般从程序实现的角度来分析,我们会这样考虑:首先是从HTTP GET请求取到id,然后按照id查数据库表,获得某id对应的访问计数值,然后加1,更新数据库,最后向页面显示访问计数。 现在假设一个没有程序设计经验的人,他会怎样来思考这个问题的呢?他会提出什么样的需求呢?他很可能会这样想: 我需要有一个计数器,这个计数器应该有这样的功能,刷新一次页面,访问量就会加1,另外最好还有一个计数器清0的功能,当然计数器如果有一个可以设为任意值的功能的话,我就可以作弊了。 做为一个没有程序设计经验的人来说,他完全不会想到对数据库应该如何操作,对于HTTP变量该如何传递,他考虑问题的角度就是我有什么需求,我的业务逻辑是什么,软件应该有什么功能。 按照这样的思路(请注意,他的思路其实就是我们平时在生活中习惯的思维方式),我们知道需要有一个计数器类 Counter,有一个必须的和两个可选的方法: [code:1]getCount() // 取计数器值方法 resetCounter() // 计数器清0方法 setCount() // 设计数器为相应的值方法[/code:1] 把Counter类完整的定义如下: [code:1]public class Counter { public int getCount(int id) {} public void resetCounter(int id) {} public void setCount(int id, int currentCount) {} }[/code:1] 解决问题的框架已经有了,来看一下如何使用Counter。 在count.cgi里面调用Counter来计数,程序片断如下: [code:1] // 这里从HTTP环境里面取id值 ... Counter myCounter = new Counter(); // 获得计数器 int currentCount = myCounter.getCount(id); // 从计数器中取计数 // 这里向客户浏览器输出 ...[/code:1] 程序的框架全都写好了,剩下的就是实现Counter类方法里面具体的代码了,此时才去考虑具体的程序语言实现的细节,比如,在getCount()方法里面访问数据库,更新计数值。 从上面的例子中看到,面向对象的思维方法其实就是我们在现实生活中习惯的思维方式,是从人类考虑问题的角度出发,把人类解决问题的思维方式逐步翻译成程序能够理解的思维方式的过程,在这个翻译的过程中,软件也就逐步被设计好了。 在运用面向对象的思维方法进行软件设计的过程中,最容易犯的错误就是开始分析的时候,就想到了程序代码实现的细节,因此封装的类完全是基于程序实现逻辑,而不是基于解决问题的业务逻辑。 学习JDBC编程的经典错误问法是:“我怎样封装对数据库的select操作?” 面向对象的设计是基于解决业务问题的设计,而不是基于具体编程技术的设计。我不会去封装select语句的,我只封装解决问题的业务逻辑,对数据库的读取是在业务逻辑的编码实现阶段才去考虑的问题。 回过头看上面那个发广告邮件的例子,应该如何应用面向对象的思维方法呢? 对于一个邮件来说,有邮件头,邮件体,和邮件地址这三个属性,发送邮件,需要一个发送的方法,另外还需要一个能把所有邮件地址列出来的方法。所以应该如下设计: [code:1]类JunkMail 属性: head body address 方法: sendMail() // 发送邮件 listAllMail() // 列邮件地址 用Java来表示: public class JunkMail { private String head; private String body; private String address; public JunkMain() { // 默认的类构造器 // 从外部配置文件读邮件头和邮件体 this.head=...; this.body=...; } public static boolean sendMail(String address) { // 调用qmail,发送email } public static Collection listAllMail() { // 访问数据库,返回一个邮件地址集合 } }[/code:1] 当把JunkMail设计好了以后,再调用JunkMail类完成邮件的发送,将是非常轻松的事情。 如果说传统的面向过程的编程是符合机器运行指令的流程的话,那么面向对象的思维方法就是符合现实生活中人类解决问题的思维过程。 在面向对象的软件分析和设计的时候,要提醒自己,不要一上来就去想程序代码的实现,应该抛开具体编程语言的束缚,集中精力分析我们要实现的软件的业务逻辑,分析软件的业务流程,思考应该如何去描述和实现软件的业务。毕竟软件只是一个载体,业务才是我们真正要实现的目标。 但是在设计过程中,心里却往往在担心,如果我完全不去考虑程序代码的实现的话,那么我怎么知道我的设计一定合理呢?我怎么知道我设计的类、接口一定可以实现呢?所以经常可以看到的现象就是: 在设计过程中,虽然知道不能过早考虑代码实现,但是每设计一个类,一个接口,心里都要不知不觉的用自己熟悉的编程语言大概的评估一下,看看能否编出来,因此,一不小心,就会又回到按照程序功能实现的思路进行设计的老路上去了。 举个例子来说明,在做Web程序设计的时候,经常要遇到分页显示数据的情况。比如说需要把系统中所有的用户都列出来这样的功能。假设使用User类来表示用户,增加用户addUser(),删除用户deleteUser(),查询所有用户listUsers()方法。而数据库中有一个user表,一条记录是一个用户的信息。下面考虑一下User类的方法的实现: addUser()和deleteUser()方法都好实现,就是对数据库增加记录和删除记录。对于listUsers()方法,其实就是对user表的select,取出一个记录集。但是该怎么从listUsers()方法中得到所有用户的列表呢? 一个方法调用的返回值只有一个,没有多个,所以很多情况下采用的办法就是返回值定义为集合类型,比如Vector。这样就可以在listUsers()方法的具体代码实现的时候,从数据库依次取出一个个记录,插入到Vector里面来。在主程序里面,调用listUsers()方法可以返回一个Vector,然后再对Vector遍历操作,就可以得到用户列表了。 [code:1] public class User { public static void addUser(...) { // 数据库insert一条记录 } public static void deleteUser(...) { // 数据库delete一条记录 } public Vector listUsers(...) { // 数据库select结果放到一个集合里面 } }[/code:1]这样的设计基本合理,但是仍然有点小问题。因为在设计的时候,就考虑到了用Java的集合类Vector来实现对不定长数据集的存放,因而违反了面向对象设计的一个原则:在设计的时候不应过早的考虑具体程序语言的实现。所以必须用抽象的方法,和具体实现无关的方法来表达业务逻辑。 我们知道,通常对具有集合特征的数据结构进行遍历通常可以使用next和hasNext方法,next实现取下一个用户,hasNext判断是否还有元素。 因此我们定义一个接口Iterator,这个接口中定义两个方法next和hasNext: [code:1] public interface Iterator { public boolean hasNext() {} public Object next() {} }[/code:1] 而User类的listUses方法返回值改为Iterator接口的实现类: [code:1]public class User { ... public Iterator listUsers() { } ... }[/code:1] 这样就把User类的设计和具体的实现方法分离开了,因为此时任何实现了next()和hasNext()方法的类都可以做为listUsers的返回值,都可以被用来表达“用户列表”,而不仅仅可以使用Vector而已。比如,我可以用ArrayList来表达用户列表,因为ArrayList也实现了Iterator,当然我也可以自己专门写一个类来存放用户列表,只要实现next()和hasNext()方法就行了。 这样在具体的编写代码的时候,程序员具有了最大的灵活性,可以根据具体的情况,采用不同的编程方法来存放用户列表。特别是降低了程序的耦合度,提高了程序的可移植性。对于上面那个JunkMail的listAllMail()方法也同样应该改为接口类型。 然后,在主程序里面就这样来使用User类的listUsers方法: [code:1] User myUser = new User(); Iterator iterator = myUser.listUsers(); while (iterator.hasNext()) { iterator.next(); }[/code:1] 这样就可以完全不用考虑程序代码实现了,从高层次上把功能抽象出来,定义成为接口,同时又可以把系统设计的很合理,完全根据业务的需求来进行设计。 结语 通过上面的几个例子的设计说明,使用面向对象的思维方法,其实是一个把业务逻辑从具体的编程技术当中抽象出来的过程,而这个抽象的过程是自上而下的,非常符合人类的思维习惯,也就是先不考虑问题解决的细节,把问题的最主要的方面抽象成为一个简单的框架,集中精力思考如何解决主要矛盾,然后在解决问题的过程中,再把问题的细节分割成一个一个小问题,再专门去解决细节问题。 因而一旦牢牢的抓住了这一点,你就会发现在软件设计和开发过程中,你自己总是会不知不觉的运用面向对象的思维方法来设计和编写程序,并且程序的设计和开发也变得不再那么枯燥,而一个合理运用面向对象技术进行设计和架构的软件,更是具备了思维的艺术美感。 最后,愿面向对象的思维方法也能给您的程序设计之路带来创作的乐趣。 声明:JavaEye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
|
|
| 返回顶楼 | |
|
时间:2003-09-19
好文,非常同意。
设计一个类的时候,尽量从这个类的使用者的角度来设计这个类的接口,呵呵,但真正做到这点难啊。 |
|
| 返回顶楼 | |
|
时间:2003-09-19
cinc 写道 好文,非常同意。
设计一个类的时候,尽量从这个类的使用者的角度来设计这个类的接口,呵呵,但真正做到这点难啊。 其实不难,如果你能先写出测试代码,自然就会很清除需要的接口是那些了. |
|
| 返回顶楼 | |
|
时间:2003-09-19
测试代码先行,呵呵,我在写程序的时候虽然不会先写测试代码,但是会先写主程序,然后再写主程序需要调用的接口程序,其实就是这种测试先行的编程方式,这种编程方式真正复合人类习惯的逻辑思维顺序。
|
|
| 返回顶楼 | |
|
时间:2003-10-09
OO考虑了纵向
AOP考虑了横向 OO不是神话,有点AOP的补充,我觉得会更令人满意 |
|
| 返回顶楼 | |
|
时间:2003-10-16
我还没有体会到OO的好处
|
|
| 返回顶楼 | |
|
时间:2003-10-19
真是经典好文啊!
是所有OOP程序员的必读文章! 强烈要求置顶。 另:看了这篇文章,真正开始佩服robbin了。 |
|
| 返回顶楼 | |
|
时间:2003-10-23
>>每设计一个类,一个接口,心里都要不知不觉的用自己熟悉的编程语言大概的评估一下,看看能否编出来,因此,一不小心,就会又回到按照程序功能实现的思路进行设计的老路上去了
这正确的做法,就像robbin自己的例子一样, 只有用代码验证才发现Iterator是适合的选择,直接根据业务是分析不出来用Iterator还是Vector好的。设计工作是在程序实现和客户需求之间的平衡,不可能仅满足客户需求而不考虑实现的问题,反之亦然。 >>我们知道,通常对具有集合特征的数据结构进行遍历通常可以使用next和hasNext方法 这是不是在根据实现考虑需求呢? >>通过上面的几个例子的设计说明,使用面向对象的思维方法,其实是一个把业务逻辑从具体的编程技术当中抽象出来的过程,而这个抽象的过程是自上而下的,非常符合人类的思维习惯,也就是先不考虑问题解决的细节,把问题的最主要的方面抽象成为一个简单的框架,集中精力思考如何解决主要矛盾,然后在解决问题的过程中,再把问题的细节分割成一个一个小问题,再专门去解决细节问题。 自上而下并不是面向对象的专用,面向过程也是这么做的。二者主要的区别应该是抽象的方法和方式。业务逻辑并不能直接转化成对象。 把问题的最主要的方面抽象成为一个简单的框架,集中精力思考如何解决主要矛盾。这是解决问题的关键,但是如何作却是一个大问题,好像没有什么正确答案。 |
|
| 返回顶楼 | |
|
时间:2003-10-23
其实如何设计和实现一个OOP的软件,本身是没有唯一标准答案的,因此也没有唯一的解法,只有趋近于更好的无数个解决方案,至于如何选择,如何平衡,这才体现了一个软件架构师的水平和经验。
对于系统的分析和分解,我个人比较推崇RUP的过程,先有业务建模,然后才是需求分析,而在系统分析阶段,使用UML,从分析和分解Use Case开始,从一个使用者的角度使用系统,得出一个个scenario,然后分析scenario,这就是Use Case,分析Use Case的主事件流和异常流,然后把这些流做出合作图,这样系统的分层架构就有了,界面类是web层,控制类是业务层,实体类是持久层。另外从Use Case中分析出所有的事物,找其中的类,这就可以画出类图。 有机会再多谈谈系统分析和系统设计的问题好了。 贴两个UML的ppt,给大家看看,一个是从UMLChina下载的,一个是我原来公司老板写的(他是个牛人,留美PhD) |
|
| 返回顶楼 | |
|
时间:2003-10-23
robbin,我基本上同意你对面向对象的这些观点。但是在开发过程中我发现面向对象也并不是万能的。比如对于数据做非常复杂的处理,数据的组织非常重要(尤其是 OLAP 一类的应用),这时候数据库设计才是最关键的。如果数据库没有设计好,在一堆杂乱的数据之上即使叠床架屋套上一大堆设计模式,仍然无法很好地解决问题。我在《程序员》等杂志上看到某些专家(我就不点名了)开口闭口设计模式、多层架构,我怀疑他们是否真正有过大型项目成功实施的经验(我们为某大公司做的销售平台项目,每年有上亿元的产品跑在系统里)。即使用了设计模式,也应该在以后对系统做重构(对用了设计模式的代码做重构比对没有用设计模式的代码做重构更容易)以求得到一个更加简练清晰的架构,而不能托词因为系统本身的复杂性所以我做设计也必须这样复杂。如果不能想出更好的方法来简化设计,一个真正非常复杂的项目总有一天会被其自重所压垮的。如果你不是一个强烈的以目标为导向的人而只追求过程的话,你完全可以在最后耸耸肩,“这个,因为系统本身确实非常复杂,所以......没有办法”,然后留下一个难以收拾的烂摊子给客户。然而这是负责任的做法吗?
复杂是很难达到的吗?我认为不然。如何能用一个更加简单的架构解决一系列的复杂问题才是最难的。所以能够流传下来的很多成熟的架构往往都体现了某种简化设计的特点。RUP 是非常有价值的,然而我更愿意采用 XP 等敏捷开发方法。 还有一些非常复杂的问题,目前还没有很好的解决方法(这也是软件领域最富有创新的部分),在这些领域,基本算法的突破才是最关键的。面向对象根本无法解决这样的问题。就象 Brooks 等专家说的,面向对象解决的是“如何做”的问题,但是“做什么”是面向对象所无法解决的。 |
|
| 返回顶楼 | |










