凭什么要用面向对象编程——面向对象重要设计原则概述
本文是我打算离开公司前的一次培训讲座的准备材料,讲得是几个面向对象的设计原则,没什么新鲜东西,都是比较浅显的内容,推荐刚接触面向对象编程的
新手阅读,高手达人们路过飘过即可。文章略长,不过内容过渡上比较平缓,应该比较容易阅读。
第一种讲法
凭什么要用面向对象来编程,不用是否可以?今天我们通过讲这么几个设计原则来说明为什么要用面向对象,它的好处在哪里。
开放-封闭原则:是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。
里氏代换原则:子类型必须能够替换掉它们的父类型。
依赖倒转原则:A. 高层模块不应该依赖低层模块。两个都应该依赖抽象。B.
抽象不应该依赖细节。细节应该依赖抽象。
第二种讲法
开放-封闭原则举例
1982年1月,小平同志在会见美国华人协会主席李耀滋时,第一次正式使用了“一国两制”的名词。
1982年9月,英国首相撒切尔 夫人同小平同志第一次会见。小平谈到:第一,中国决心按照“一国两制”的设想,于1997年收回整个香港地区,主权问题不容谈判;第二,希望中英合作实现
平稳过渡;第三,如谈不成,中方将单独 采取行动;第四,如出现动乱,就将采取非和平方式提前收回香港。撒切尔夫人在走下人民大会堂东大门台阶时摔了一跤。大陆新闻片为留其面子没有这个镜头。但
在香港看片子,片子里老出现这个镜头。
1997年7月,香港正式回归。
2007年6月,英国前首相撒切尔夫人说,香港归还以后的发展证 明,当初英方的担心是过虑了。她又说,当时她对“一国两制”也没信心,现在她想告诉邓小平,他的办法很好。
“伟人牛就牛在不会编程,也是会用设计原则的。‘一国两制’,就是‘开放封闭原则’的应用。在不改变大陆原有社会主义制度的前提下,一个国家在回归
的特区实行一种新的管理制度方式,所有的问题都变得不是问题了。在编程中,面对时常更改的需求,也可以做到类似的思维方式来解决问题。”
里氏代换原则举例
感冒,有点发烧,邻居正好是内科医生,于是就敲门想请帮忙看看病。哪知不巧,邻居不在家。
熟人不在家,难不成这病就不看了?身体吃不消时还 是得去医院。
医院挂完号后,到门诊室前,此时里面医生是男是女,姓啥叫啥,都不清楚。可唯一清楚的就是,他是医生,相对普通人来说,是看病的专
家,可以对症下药。
里氏代换原则:子类型必须能够替换掉它们的父类型。在这里很好理解了,任何一个具体的比如张医生、王医生、李医生,他们的共同
点,都是医生。也就是,他们都可以代表医生这个角色。
依赖倒转原则举例
大家有没有这样的感受,复杂的电脑能修的,而简单的收音机却不会修,这是为什么?
收音机就是典型的耦合过度,只要收音机出故障,不管是声音 没有、不能调频、有杂音,反正都很难修理,不懂的人根本没法修,因为任何问题都可能涉及其它部件,各个部件相互依赖,难以维护。
电脑却不一样, 内存坏了换内存,硬盘坏了换硬盘,主板烧了换主板,反正普通人,只要有点常识,基本都可以鼓捣几下。当然,电脑的所谓修也就是更换配件,真正CPU或内存
坏了,老百姓是没法修这个器件的。
无论主板、CPU、内存、硬盘都是在针对接口设计的,如果针对实现来设计,内存就要对应到具体的某个品牌主板,
那就会出现换内存需要把主板也换了的尴尬。
这就是高层模块不应该依赖低层模块。两个都应该依赖抽象。依赖倒转原则的根本要义。
第三种讲法
编程场景引入,代码重构与演变方式讲解。
凭什么要用面向对象来编程,不用是否可以?今天我们通过实例来说明为什么要用面向对象,它的好处在哪里。
★2007-7-1 20:13 小菜接到创业开公司不久的好朋友老卞的电话
老卞:小菜呀,能帮我个忙吗?帮我写个小网站的程 序好吗?要求不多,也就是对我那几个员工的日常信息管理。比如“登录”……(其它功能略)等。
小菜:好呀,我最近也没什么事。这点功能容易,马上 就可以写好。
1 用户登录界面
2 登录页面代码
★2007-7-25 21:48 小菜再次接到老卞的电话
老卞:小菜,网站效果很不错,能不能再出个手机上网的版 本,现在这个在手机上看起来效果不太好。
小菜:哦,那是因为网站的结构不合适手机浏览器。可以再写一个关于手机的网页就行了,没什么问题。
思路:考虑到同时两个登录页面要有登录功能,所以写了一个可供调用的用户管理类,让两个页面可以都调用它。起到代码复用的作用。
3 手机登录界面
4 用户管理类 登录方法
5 调用登录方法的界面事件代码
★2007-7-29 18:51 小菜第三次接到老卞的电话
老卞:小菜,现在用手机上网站的效果真不错,但因我的员 工大部分时间都在公司,如果它还可以是一个客户端的应用程序,能用一些快捷键和自动运行的网站不容易做到的功能就好了。
小菜:哦,你的意思就是 在现在b/s架构的基础上,再要一个c/s架构的程序呗,容易。
思路:为了便于让桌面应用程序和网站都可以复用登录代码,所以增加了一个DataAccessLayer的Project,基本代码未改变。这样就
可以让网站和客户端程序都调用UserAdmin类,避免了代码的重复。
6 应用程序登录界面
7 源代码结构图
★2007-8-3 18:51 小菜第四次接到老卞的电话
老卞:小菜,你太牛了,我提的需求你都能满足。现在发生了 这样的情况,本来我在托管的服务器里装了盗版的sql
server的,最近人家通知我,微软查得非常紧,所以我想……
小菜:你想把数据库换成 免费版的?
老卞:对呀,MySql或者Access都可以,反正就是别让微软抓到把柄就行。
小菜:这的确是麻烦一些,不过问题也不太大, 等我消息吧。
思路:必须要用到接口、继承和多态等面向对象的特性了(若不理解此样例代码,请先学习继承和多态的基础知识)。
8 源代码结构图 用户管理接口与各个子对象
9 接口代码
10 Access实现接口类(其它类类似)
11 网页或窗体调用代码
说明:由于要考虑Access的实现,所以在所有调用登录接口处都要写一句
IUserAdmin ua = new UserAdminAccess();
目前样例中是三个地方,就需要写三遍。若要改成访问Sqlserver数据库的登录代码,就需要改三个地
方。
★2007-8-8 21:29 小菜第五次接到老卞的电话
老卞:小菜,上次我让你改成Access的代码有改了没有?
小 菜:改好了,明天就部署上去,你可以用了。
老卞:啊,是这样呀,因为我后来想来想去,还是用SqlServer功能更强大一些,以后可扩展的可能
性大一些。能不能两种方式都保留呀?
小菜:嘿嘿,你想法够多。行了,我可以做到的。
思路:如果写死一种方式,比如IUserAdmin ua = new UserAdminAccess();就会使得更换数据库时,带来麻烦。所以面对可能更改数据库代码的变化,所以用一个工厂类来把变化给封装掉,此时,要
修改也只是修改工厂类而已,对外面的引用代码不需要修改。
12 增加用户管理工厂类
13 增加用户管理工厂类
14 各个登录页面的代码
★2007-8-15 19:24 小菜第六次接到老卞的电话
老卞:小菜,真的不好意思,又要麻烦你了。我一朋友看来 你为我做的这个管理软件,感觉非常好,想在他的企业内部也使用,他们已经购买过Oracle正版使用权了,所以你能不能再给出一个访问Oracle的版
本,放心吧,只要好用,他一定不会亏待你的。
小菜:啊,你这变化实在是好快呀。
老卞:不然人家怎么都叫我“老变”呢。帮帮忙!
小 菜:好吧,我有想办法解决的。
思路:可惜原有的工厂类,还是需要修改的,为了更好的满足“开放封闭”,于是把工厂类再次改造,让它依赖于配置文件的信息,此时,就更加灵活的实现
了应对变化(此处用到了.net的反射技术,不理解的请研究相关内容知识)。
15 改进用户管理工厂类
16 web.config的配置信息
★2007-9-23 20:47 小菜第七次接到老卞的电话
老卞:小菜,好久没来骚扰你了。最近可好呀?
小 菜:老卞呀,你好你好,欢迎骚扰,你的“老变”让我的程序越来越适应变化了,哈,我不怕你变的。说,又有什么新想法了?
老卞:那样最好了,是这样 的,你的软件在我公司已经广泛应用,就登录这个功能,还有些小缺陷。我们员工每天上班都要用考勤系统,以前是“打卡”的,现在直接是“指纹”的,其实既然
已经人来上班了,我们的软件系统也就不需要再输入用户名和密码登录了,你看可不可能增加通过“打卡”和“指纹”登录系统的功能?
小菜:不用“用户 名”和“密码”方式登录了?
老卞:不是不用,而是说增加其它的登录方式。
小菜:你这个需求够狠。好吧,我想想看。
思路:如果增加下面两个接口方法,再针对它们做一个实现,其实也没有彻底解决这种变化带来的麻烦。
17 增加两个接口方法
18 实现指纹登录代码
思路:这样的办法,并不是最好的办法,因为难讲以后还可能出现其它的登录要求,难道每次都去增加方法?应该考虑更好的设计方式。登录的本质
是什么?为了方便系统能识别该用户的身份,从而保持该用户的使用习惯或使用数据。显然用户名、密码、员工考勤卡、员工指纹,都是具体的可以代表员工身份的
属性。因此,登录行为的变量不应该是具体的属性,而应该是抽象的用户对象本身。
19 让登录方法的参数是抽象的用户对象
20 用户对象可以包含所有用户可能属性
21 登录调用代码(用户名和密码)
22 登录调用代码(指纹识别登录)
说明:具体实现Login的方法相对复杂一些,但不外乎就是对相关属性的比对判断。由于通过不断的抽象,使得程序越来越能适应变化。
★2007-10-01 13:04 小菜第八次接到老卞的电话
老卞:小菜,你实在是太棒了,我提的要求你都能满足,你写的 代码现在一切运行正常。今天正好休息,我请你吃饭。另外我的做开发的美女郝雪也想通过这个机会向你请教一下你的编程经验。
小菜:你太客气了,这也 没什么的,说白了,就是我在面向对象编程而已。
老卞:太好了,到晚上时,我通知你地点,到时见。
★2007-10-01 19:51 小菜和老卞,以及老卞的朋友郝雪在一起吃饭
郝雪:菜老师,你到底是如何做到,无 论需求如何变,你的程序面对变化,却依然能从容不迫呢?
小菜:这个说来其实不算复杂。
比如,为了使得常用代码可以复用,一般都会把这些常用代码写成许许多多函数的程序库,这样我们就可以在做新项目时,去调用这些低层的函数就可以了。
比如我们做的项目大多要访问数据库,所以我们就把访问数据库的代码写成了函数,每次做新项目时就去调用这些函数。这也就叫做高层模块依赖低层模块。
我们要做新项目时,发现业务逻辑的高层模块都是一样的,但客户却希望使用不同的数据库或存储信息方式,这时就出现麻烦了。我们希望能再次利
用这些高层模块,但高层模块都是与低层的访问数据库绑定在一起的,没办法复用这些高层模块,这就非常糟糕了。
就象刚才说的,PC里如果CPU、内存、硬盘都需要依赖具体的主板,主板一坏,就所有的部件都没用了,这显然不合理。反过来,如果内存坏
了,也不应该造成其它部件不能用才对
而如果不管高层模块还是低层模块,它们都依赖于抽象,具体一点就是接口或抽象类,只要接口是稳定的,那么任何一个的更改都不用担心其它受到影响,这
就使得无论高层模块还是低层模块都可以很容易的被复用。这才是最好的办法。
面向对象的好处是可维护、可扩展、可复用和灵活性好。有了开放封闭思想和依赖倒转原则,我们就可以大胆的应用它实现这些好处
了。
对于这个登录的例子来说,一个没有经验的程序员,可能每一次的设计都只是刚刚满足需求的(比如这个例子经过这么多次演变才成为最终的样子),这本身
也不是大问题,毕竟一开始就考虑太多,完全有可能走入过渡设计的怪圈(过渡设计的最大问题是开发的成本大大增加)。但当发现原有的设计不足以应对变化时,
的确就需要通过重构来让代码更加灵活(比如抽象了一些类或接口,利用某些设计模式来封装变化等)。这是一种自下而上的设计方法。
另外,如果有了一些经验,当客户提出要一个“登录”的功能时,就已经可以考虑到创建一个
int Login(Users user); 这样的接口方法,至于将来是否要把这个方法实现多次,有多少个程序会调用它,那都是以后考虑的问题。这样,当客户表述清楚需求后,其实你已经就开始了接口
代码的设计工作,而这个工作是整个编程工作中最重要的一个环节。这就是自上而下的设计方法。
此两种设计方法没有优劣之分,在需求很清楚时,自上而下,在需求不明时,考虑先做满足当前需求的设计,后再根据情况重构,总的来说,当你的经验越来
越丰富以后,你就越发能做出合适的选择。
依赖倒转其实可以说是面向对象设计的标志,用哪种语言来编写程序不重要,如果编写时考虑的都是如何针对抽象编程而不是针对细节编程,即程序中
所有的依赖关系都是终止于抽象类或者接口,那就是面向对象的设计,反之那就是过程化的设计。
掌握了这几个原则,你以后学习面向对象设计模式,就会发现,它们其实都是做了同样的事,目的都是为了针对抽象编程,从而带来容易维护扩展复用的好
处,也就是说,设计模式用了哪一些、会用的有多少并不重要,你是否有这样的设计想法才是最重要的。
所以说,为什么要用面向对象来编程呢?说白了,就是为了让程序更容易的应对需求变化。
郝雪:你刚才讲得真是太好了,你看哪天有空,到我们公司给大家讲讲你的面向对象编程思想好吗?
小菜:不用了吧,这其实也就是很简单的原则, 我只不过就是谈谈为什么要用面向对象来编程,它的好处是什么。
郝雪:那就以“凭什么要用面向对象编程”来作为标题做一次讲座,好吧,就这么定了,
谢谢!。
小菜:……
好了, 讲解完毕,您辛苦了,如果坚持看到了这里,说明您真的是一个很有耐心的人,程序员同样需要有足够的耐心和坚持,谢谢您的阅读,还请您给予批
评指正,您的回复对我的创作很重要。
源代码下载
后记:本来本篇已经是完整篇,可在第22楼virus的回复中,我发现了这篇文章中的最终解决方案还是存在着本质的缺陷。再看到第24楼richardzeng的回复,仔细想来,的确应该是像richardzeng的写法一样有更深一步的考虑,感谢两位的回复。我将补充方案的讲解加了进来,希望可以给大家以帮助。
前言:本来上篇已经是完整篇,可在上篇第22楼virus的回复中,我发现了上篇文章中的最终解决方案还是存在着本质的缺陷。再看到第24楼richardzeng的回复,仔细想来,的确应该是像richardzeng的写法一样有更深一步的考虑,感谢两位的回复。我将补充方案的讲解加了进来,希望可以给大家以帮助。
接上篇 凭什么要用面向对象编程——面向对象重要设计原则概述
★2007-10-02 08:00 小菜在家中准备
小菜开始准备给人家讲座的内容,一开始都很顺利。可是当要把不同的验证方式给细化时,发现了问题。
24 原有的接口实现关系图
如果要再把是用户名密码验证,还是指纹验证的代码加进来,应该如何写具体的实现类呢?
25 SqlServer实现类改造
此时你会发现,如果要实现这个功能,你必须在你的每个实现类中写出上面的判断语句,如果某一天要增加一种数据访问(比如MySql)你就得再写一遍类似的代码,如果某天增加一个用户验证的方式(比如人脸识别验证),你就得改动所有的实现类的分支判断。这显然是让人难以接受的。
小菜决定换一种思路
26 多个类的多重继承
也就是把分支判断的语句,通过继承的方式给分解掉,这样每一种实现都体现成为一个类。只要有新的数据访问或者用户验证方式,不外乎就是增加类就可以了。应该是解决了问题。
可是,看着这张图,小菜感觉不到设计的美。如果,我们增加一种数据访问(如MySql),我们需要增加至少四个类。如果我们增加一种验证方式(如人脸识别验证),那就需要每个数据访问下都继承一个类。目前是3*3共9个类,当扩展需求来了以后,类的增加会成为一个梦魇。
难道没有办法了吗?
1)当想不出解决办法时,分析自己的代码有什么容易被改变的地方,可能找到解决思路。
回到那个接口
其实当我们要增加一种验证方式时,这个Users类也是要修改的。而且对于Users类来说,验证时通常只会用到当中的一两个属性,而现实中,Users类的属性如性别、生日、姓名等等都对验证来说没有意义。此时发现,验证和用户类本身还是有区别的,验证属性其实只是用户类一小部分属性而已。对于数据库来说,我们把用户名、密码、门禁卡编号、指纹数据存到Users表里的字段中是没有问题,可在面向对象编程时,将它们混在一起的确不利于应对变化。
2)当想不出解决办法时,再次仔细分析需求,并找出需求的本质是个很好的办法。
登录是为了验证用户,验证有很多种表现形式。网络上的验证通常就是用户名和密码,但随着科技的进步,指纹试别、面部试别等技术都可以成为验证用户的手段。而这一切,其本质上,验证就是抽象概念,其它都是它的实现方式。那么验证应该就是接口,那几个表现方式是它的实现类。OK,思路有了。
思路:提炼出验证接口,将不同的验证实现这个接口,将用户管理接口的登录方法聚合这个接口。
27 相对较好实现办法的类图
将验证分离成一个接口,不同的验证方式不过是验证的一种表现形式。
32 更改了原用的参数,改用验证接口
33 Sqlserver实现类的代码(其它实现类类似)
34 用户名密码登录用的界面事件代码
35 指纹登录的界面代码
此时,小菜算是松了口气,不管是增加新的数据访问或是增加新的验证方式,都只不过是增加一个类就可以了,这就把“开放封闭原则”和“依赖倒转原则”再次得到了充分的体现。实际上,在不知不觉中,小菜已经使用了“简单工厂模式”、“门面(外观)模式”、“桥接模式”。在这个完整的例子中,并不是为了模式而模式,而是在需求的变更中,为了应对需求的变化而不断演变出来。相信以后需求再有变化,也可以从容面对。
最后,小菜在新建文档的标题处打上“凭什么要用面向对象编程”,开始培训材料的写作。
附:有朋友质疑一个登录功能写这么多的代码的必要性。我想说的是,这是一个讲解面向对象编程的例子,为了理解的需要,杜撰了一些需求的变更。通过这样的讲解,是希望你可以在你的编程工作中,学会如何去设计和考虑问题,更好的应用面向对象技术来创建可维护、可扩展、可复用,并灵活性好的程序。
源代码下载
|