新主板,开机黑屏是什么情况,主板华硕主板cpu超频教程z270

当前位置: &>&&>& >
游戏编程设计模式 -- 引言
Game Programming Patterns -- Introduction
原文地址:/introduction.html
原创翻译,转载请注明出处
五年级的时候,我的朋友们和我被允许进入一间存放着几台非常破旧的TRS-80s电脑的闲置小教室。抱着启发我们的想法,一位老师给我们找来了一些印有简单BASIC程序的打印件让我们对照着来操作。
因为这些电脑的盒式磁带驱动器已经坏了,所以每次我们想要运行一些代码的时候,我们都要从头开始一点点小心地输入这些代码。这让我们更钟爱那些只有几行长度的程序:
10 PRINT "BOBBY IS RADICAL!!!"
20 GOTO 10
可能如果电脑把这些话打印足够次数的话,它们就会神奇地成为现实
即使是这样,这整个过程也是充满着危险的。我们并不懂得如何去编程,所以一个小小的语法错误会困扰我们很久。当程序无法工作的时候(这是经常发生的),我们都会从这段程序代码的开头重新开始。
这叠纸的后面几张是一个真正的怪物:一个代码占据了满满几张纸的程序。我们花了一些时间去鼓起勇气来尝试运行这个程序,即使它看起来是这么的诱人--这段代码的标题是《Tunnels and Trolls》(一个古老的幻想RPG游戏)。我们并不知道这个程序是用来干什么的,但是它听起来像是一个游戏,还能有什么比一个你自己编程的电脑游戏更酷的呢?
我们最终没有让它成功地运行起来,一年之后,我们离开了那间教室(很久之后当我真正了解了一点BASIC语言的时候,我才意识到那段程序只是那个桌面游戏的角色生成器而不是一个完整的游戏)。但是木已成舟的是--从那时开始,我有了成为一个游戏程序员的念头。
在我十几岁的时候,家里迎来了一台可以使用QuiBASIC和THINK C的Macintosh电脑。我花费了几乎整个暑假的时间用它来设计编写游戏。自学的过程是非常漫长和痛苦的。我很轻松地完成了一些小程序--比如一个地图菜单或者一个小拼图之类的--但是随着程序规模的增大,想要成功运行就变得越来越难了。
我也花了很多个夏天的时间在南路易斯安那州的湿地里捉蛇和乌龟。要不是因为外面实在是太热了,我很有可能写出一本两栖爬虫动物学的书而不是这本有关于编程的书。
刚开始的时候,我只是尝试着让一些东西能够运行起来。逐渐的,我想要能够完成一些更大的程序。在有了这种念头之后,我在阅读《How to Program in C++》这类的书之外,开始尝试着去寻找一些有关于如何去组织程序的书。
很多年之后,一个朋友给了我一本书:《Design Patterns:Elements of Reusable Object-Oriented Software》(《设计模式:可复用面向对象软件设计》)。终于出现了!这就是那本我从少年时代一直在寻找的书。我迫不及待地把这本书翻阅了一遍。这么多年了,我一直为我的程序代码焦头烂额,让我欣慰的是,通过这本书我发现还有很多其他人也和我一样,并且他们有了一些不错的解决方案。这感觉就像是我终于有了一些趁手的工具,而不是徒手去修缮我的程序。
这是我和那个朋友第一次见面时发生的事情,在相互介绍5分钟之后,我就坐在他的座位上在完全忽略他的情况下花了几个小时去阅读这本书。我想我现在的社交技能至少不会像当时那样那么糟糕了。
2001年,我得到了我梦寐以求的工作:Electronic Arts的软件工程师。这里有很多图形、AI、动画和视觉特效方面的杰出代码。我们也有那种可以压榨出CPU所有性能并将其妥善使用的人才。很多我觉得不可能出现或完成的事情,这些大神们一顿饭的功夫就解决了。
然而,将这些杰出代码组织起来的架构却往往是事后才想起来要完成的东西。大神们太过于专注功能的开发而忽略了对代码的组织。功能模块之间的耦合层出不穷。新功能被随意地插入任何一处让它们可以正常运作的代码库里。这让我很失望,他们这样和很多阅读了《Design Patterns》,却从来没有看完过Singleton(单例模式,一种比较容易理解的设计模式)这一章的程序员没什么两样。
当然,情况并不是真的如此糟糕。我曾经的想象中的游戏程序员们是坐在一些被白墙包围住的象牙塔中,一连好几周平心静气地进行架构细节上的讨论。但残酷的现实则是,这些我所看到的代码都是那些面临着紧张的项目期限的人们所完成的。他们已经尽了他们最大的努力,而且我也逐渐发现,这些代码其实是非常棒的。随着我在游戏编程上工作的时间越来越多,我也从这些代码的表面情况之下发现越来越多的隐藏着的闪光点。
不幸的是,“隐藏”真的是对我们工作中经常出现的情况的一个很好的描述。在这些代码中埋藏着珍宝,然而很多人都只是匆匆略过而没有发现。我经常看到我的同事们为了找到一个好的解决方案而心力交瘁,而其实他们所需要的东西的例子就躺在他们所正在使用的代码库中。
这正是这本书所要去解决的问题。我挖掘出那些我在游戏开发中所曾看到的设计模式并对它们加以修饰,把它们展示在这里,这样我们才能有更多的时间去研究一些新东西而不是重复地发明轮子。
商店里都有些什么
商店里已经有一大堆关于游戏编程的书了。为什么还要再写一本呢?
大多数我所看过的游戏编程书籍可以归类为以下这两类的其中之一:
关于某个特定领域的书籍。这些专注度很高的书会带你深入到游戏开发的某个特定领域中。它们会教你有关于3D图形学、实时渲染、物理模拟、人工智能或者音频处理等方面的知识。这些领域中的每一个,都是很多游戏开发者需要穷极他们整个职业生涯所去专门研究的。
通览游戏引擎的书籍。相对应的,这些书籍会尝试带读者了解一个完整游戏引擎的每个不同的部分。它们的目标是搭建一个适用于某个特定类型游戏的完整引擎,比较常见的是用于3D第一人称射击类游戏的引擎。
这两种书我都很喜欢,但是我觉得在它们之间有一些空白领域。关于某个特定领域的书籍很少告诉你它们的这些代码块如何与游戏中的其他功能相互作用。你可能是一个物理引擎和渲染方面的大神, 但是你知道如何把它们地联系在一起使用吗?
第二个类型的书包括了这些方面,但是我经常发现,这种引擎类的书籍都过于整体化而且过于专注于某个类型的游戏。因为移动设备和休闲类游戏的崛起,我们处在一个很多不同新的类型的游戏被创造出来的时代。我们再也不是都去克隆Quake(一个3D FPS游戏)。当你的游戏并不符合引擎类书籍中的游戏类型时,这些书对你就没有太大的帮助。
与这两类书所不同的是,我想在这本书里所完成的是让读者更有自主选择性的事情(原文:a la carte,意思是照菜单点菜,与之相对的是regular dinner,配好的套餐)。这本书中的每一章都是一个独立的让你可以在你的代码中使用的点子。这样,你就可以在你想完成的游戏中用你觉得最好的方式去组合和搭配他们。
另一一个这种自主选择式的例子是广受欢迎的《Game Programming Gems》(《游戏编程精粹》)系列。
这本书是如何与设计模式相关联的
任何一本书名中有“设计模式”的编程类书籍很明显都会和经典的《Design Patterns:Elements of Reusable Object-Oriented Software》(《设计模式:可复用面向对象软件设计》)有一定的联系,这本经典的书籍是由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides合作完成的(他们被戏称为“Gang of Four”--GOF、四人帮)。
设计模式这本书本身就是从它之前的一本书中获取到灵感的。这个创造一种用来描述可扩展性问题解决方案的设计模式语言的点子来自于《A Pattern Language》这本书,作者是Chirstopher Alexander(还有Sarah Ishikawa和Murray Silverstein)。他们的这本书是关于架构的(真实的,有关于建筑、墙、材料之类的架构),但是他们希望其他人可以使用同样的结构去描述其他领域的解决方案。“设计模式”正是GOF借鉴他们的点子在软件开发领域的尝试。
虽然我把本书称为《Game Programming Patterns》(《游戏编程设计模式》),但是我并没有暗示GOF的书不适用于游戏开发的意思。与之相反的是,本书“再探设计模式”一章中涵盖了很多来自于“设计模式”中的经典模式,不过重点要更倾向于如何在游戏编程中去使用它们。
同样,我认为本书也适合非游戏软件的开发。我也可以把这本书叫做《更多的设计模式》,但是我认为游戏有助于提供更多有吸引力的例子。你确定你真的想要去看又一本用员工记录和银行账号来做例子的书?
话虽这么说,即使本书中介绍的设计模式同样也适合于其他类型软件的开发,但是我认为它们尤为适合解决那些在游戏编程过程中遇到的问题:
时间和定序通常是一个游戏架构中最核心的部分。游戏中的所有事情都必须按照正确的顺序在正确的时间发生。
开发周期是非常紧凑的,项目中的程序员需要在有一个丰富的包括多种不同行为的集合的基础上去保证项目的快速构建和迭代,避免相互干扰和代码库中的肆意修改。
在上述有关行为的集合定义完成之后,就是完成交互的工作了。诸如怪物攻击英雄,药水被混合在一起,还有炸弹炸到了敌人和友军之类的事情。这些交互行为的功能必须保证不会把代码库弄成一团乱麻。
最后,性能表现是游戏的关键。游戏开发者们永远处在一场看谁可以压榨出他们开发的游戏所在平台更多性能的比赛之中。一些提升性能的技巧可能会给你带来一款A级评分和上百万销量的游戏,而不是掉帧和玩家充满愤怒的评论。
如何阅读本书
《游戏编程设计模式》被划分为三大部分。第一部分是引言和对本书整体框架的介绍。它包括你正在阅读的这一章以及接下里的一章。
第二部分是再探设计模式,在这一部分里会遍历一些来自GOF书中的设计模式。在其每一章中,都会给出我对这些设计模式的新的见解,以及介绍它们是如何与游戏编程关联起来的。
最后一部分是本书的真正精髓之处。这里给大家呈现了13种我觉得有用的设计模式。它们被分为了4个类别:用于时序的设计模式、用于行为的设计模式、用于解耦的设计模式和用于优化的设计模式。
所有的这些设计模式都使用了统一的结构来描述,所以你可以把本书当成字典一样,快速地找到你所需要的那部分内容。
意图部分对这一章中的设计模式及其所要解决的问题进行了简短描述。这是第一部分,这样可以帮助你们很快地从本书中找到对你们当前所遇到的困难有帮助的那一个设计模式。
动机部分列举了一个我们可以应用这一章中设计模式的示例问题。和实际的算法不同,一个设计模式在没有应用在一个特定的问题之前,一般是没有具体的形式的。所以,介绍一个设计模式而没有相关的例子,就好像教人烘焙却完全没有提到生面团一样。这一部分正是在提供接下来的烘焙部分中所需要使用到的生面团。
模式部分从前一部分的例子中提取出其使用模式的精髓。如果你想要一个对一个模式枯燥的教科书式的描述,那这部分正是你所需要的。这里也是确认你对一个模式是否已经熟悉的很好的复习,以保证你没有忘记一个模式中的某个组成部分。
到这里为止,我们只在一个单独的例子中解释了其模式的使用。但是怎么能让你知道这种模式是不是适用于你所遇到的问题呢?何时使用部分提供了一些何时应该使用一种模式以及何时是应该避免使用它的一些参考。而谨记部分则指出了一些使用一种模式的后果和风险。
如果你和我一样,需要从一些具体的例子中去真正地学到一些东西,那么示例代码部分就很适合你。它带你一步一步遍历一种模式的一个完整实现,这样你就能知道这种模式是如何在实际中产生作用的。
模式和单一算法不同,因为模式是可扩展的。每次你用到一种模式,可能都会得出一种完全不同的实现。下一部分,设计决策,就是探讨这一部分的内容,给你展示在使用一种模式时需要面临的不同选择。
最后是一个简短的参见部分,这里给你展示了一个模式是如何与其他模式相关联的,以及介绍一些现实中使用这种模式的开源代码项目。
关于示例代码
本书中的代码示例是使用C++编写的,但是这并不是说本书中的模式只适用于C++,也不是说C++在使用这些模式时要比其他编程语言更好。几乎任何一种编程语言都可以很好地使用这些模式,虽然可能其中某些模式需要你所使用的语言中有对象和类的概念。
我选择使用C++是有一些原因的。首先,C++是在商业发行的游戏中使用最广泛的语言。它是这个行业的通用语。其次,C++基于的C语言,同样也是Java、C#、JavaScript等其他很多种语言的基础。即使你不懂C++,只需要付出一点点的努力,就可以很好地理解书中代码示例。
本书的目标不是教你学会C++。示例都是尽可能的简单,因而没有很好地使用C++的特性及用法。阅读时需要更多地关注示例中传递的思想,而不是这个示例的代码本身。
还有一点需要注意的是,这些代码没有使用一些“现代”特性--即C++11或者更新版本中的特性。它们没有使用标准库,也几乎使用到模板。这对于C++代码来说是不够好的,但是不使用这些特性正是我所期望的,因为这样对于使用C语言、Objective-C、Java和其他语言的读者来说,要更友好一些。
那些你曾经在前文中见过的代码,或者与当前模式无关的代码,有些时候在示例代码中会被省略。此时会有省略号取代这些代码的位置,提示你这里有代码被省略了。
想象一个需要完成某些工作然后返回一个值的方法。在这个方法里,我们所要解释的模式只关注其返回值,而不考虑工作的完成过程。在这种情况下,这个方法的示例代码将如下所示:
bool update()
// Do work...
return isDone();
模式是软件开发中一个不断变化和扩展的组成部分。本书继承了从GOF开始的、记录和分享他们所知软件开发模式的优良传统。相信这个传统在本书完成之后,依然会被一直延续下去。
而你们,是延续这个传统的核心。无论你们是研究出属于自己的模式,或是改善(驳斥也可以哦!)本书中所提到的模式,你们都是在对软件开发行业做出自己的贡献。如果你有任何关于本书的建议、修正或者反馈,欢迎随时联系我!
因为水平有限,翻译的文字会有不妥之处,欢迎大家指正
延伸阅读:
(责任编辑:美朵)
每日重点推荐
夏医生是自己多年老同事的儿子,国外学成归来做事业,老同事担心儿子诊所刚开,门厅罗雀伤了自尊,便偷偷塞钱给李梅,求她装病去“捧场”。
一周热点文章
在线阅读专题2391人阅读
原文作者:赵青青
unity编程众所周知,它是属于脚本化,脚本没有一个具体的概念跟架构,&
导致在项目过程中,经常出现哪里需要实现什么功能,就随便添加脚本,&
结果,就造成了一片混乱,不好管理。&
更有甚者,自己的写的代码闲置一段时间后,再去想找某个功能的实现,都要在视图中翻来覆去找半天。&
哎!请容许我在此感叹一声,这还是你写的东西么?&
因此,一个好的设计模式是多么的重要啊,&
那么,我们在使用unity3d开发东西的时候,脚本架构到底应该如何来写?&
其实,我也给不了你们具体答案,因为每个人的开发习惯,每个团队的开发模式也各有千秋,&
so,在此我只做几种设计模式的总结,&
主要参考书籍有《设计模式》《设计模式之禅》《大话设计模式》以及网上一些零散的文章,&
但主要内容还是我本人的一些经验以及感悟。&
写出来的目的一方面是系统地整理一下,一方面也与广大的网友分享,&
至于你们到底如何使用,&
望君斟酌啊!&
因为设计模式对编程人员来说,的确非常重要。&
当然,如果大家的理解跟我有所不同,欢迎留言,大家共同探讨。
原则1:单一职责原则2:里氏替换原则(子类扩展但不改变父类功能)原则3:依赖倒置原则原则4:接口隔离原则原则5:迪米特法则(最少知道原则)原则6:开闭原则
原则1:单一职责原则
说到单一职责原则,很多人都会不屑一顾。&
因为它太简单了,稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。&
在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。&
而避免出现这一问题的方法便是遵循单一职责原则。&
虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。&
为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责被分化成了更细的职责。&
如:用一个类描述动物呼吸这个场景。
class Animal
public void breathe(string animal)
Debug.Log(animal + &呼吸空气&);
public class Client
Animal animal = new Animal();
void Start()
animal.breathe(&牛&);
animal.breathe(&羊&);
animal.breathe(&猪&);
//运行结果:
//牛呼吸空气
//羊呼吸空气
//猪呼吸空气
程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。
修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:
class Terrestrial
public void breathe(String animal)
Debug.Log(animal + &呼吸空气&);
class Aquatic
public void breathe(String animal)
Debug.Log(animal + &呼吸水&);
public class Client
public static void main(String[] args)
Terrestrial terrestrial = new Terrestrial();
terrestrial.breathe(&牛&);
terrestrial.breathe(&羊&);
terrestrial.breathe(&猪&);
Aquatic aquatic = new Aquatic();
aquatic.breathe(&鱼&);
//运行结果:
//牛呼吸空气
//羊呼吸空气
//猪呼吸空气
//鱼呼吸水
我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。&
而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:
class Animal
public void breathe(String animal)
if (&鱼& == animal)
Debug.Log((animal + &呼吸水&));
Debug.Log((animal + &呼吸空气&));
public class Client
public static void main(String[] args)
Animal animal = new Animal();
animal.breathe(&牛&);
animal.breathe(&羊&);
animal.breathe(&猪&);
animal.breathe(&鱼&);
可以看到,这种修改方式要简单的多。&
但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,&
则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,&
也许某一天你会发现程序运行的结果变为“牛呼吸水”了。&
这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。&
还有一种修改方式:
class Animal
public void breathe(String animal)
Debug.Log(animal + &呼吸空气&);
public void breathe2(String animal)
Debug.Log(animal + &呼吸水&);
public class Client
public static void main(String[] args)
Animal animal = new Animal();
animal.breathe(&牛&);
animal.breathe(&羊&);
animal.breathe(&猪&);
animal.breathe2(&鱼&);
可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,&
但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,&
那么在实际编程中,采用哪一中呢?&
其实这真的比较难说,需要根据实际情况来确定。&
我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则。&
遵循单一职责原的优点有:
可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;提高类的可读性,提高系统的可维护性;变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
原则2:里氏替换原则
肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑。&
其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。&
简单来说的话,就是当我们使用继承时,遵循里氏替换原则。&
注:类B继承类A时,除添加新的方法完成新增功外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。&
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,&
虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,&
就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。&
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。&
比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,&
则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,&
所有涉及到子类的功能都有可能会产生故障。&
那就让我们一起看看继承的风险,如下:
public int func1(int a, int b)
return a -
public class Client
void Start()
A a = new A();
Debug.Log(&100-50=& + a.func1(100, 50));
Debug.Log(&100-80=& + a.func1(100, 80));
运行结果:&
100-50=50&
100-80=20&
后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。&
即类B需要完成两个功能:&
两数相减。&
两数相加,然后再加100。&
由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下
public int func1(int a, int b)
return a +
public int func2(int a, int b)
return func1(a, b) + 100;
public class Client
private void Start()
B b = new B();
Debug.Log(&100-50=& + b.func1(100, 50));
Debug.Log(&100-80=& + b.func1(100, 80));
Debug.Log(&100+20+100=& + b.func2(100, 20));
类B完成后,运行结果:&
100-50=150&
100-80=180&
100+20+100=220&
我们发现原本运行正常的相减功能发生了错误。&
原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。&
在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。&
在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,&
但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。&
如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。
里氏替换原则通俗的来讲就是:
子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:&
1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。&
2.子类中可以增加自己特有的方法。&
3.当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。&
4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。&
所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?&
后果就是:你写的代码出问题的几率将会大大增加。
原则3:依赖倒置原则
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。&
抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。&
依赖倒置原则的核心思想是面向接口编程,我们依旧用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。&
场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:
class Book
public String getContent()
return &很久很久以前有一个阿拉伯的故事……&;
class Mother
public void narrate(Book book)
Debug.Log(&妈妈开始讲故事&);
Debug.Log(book.getContent());
public class Client
void Start()
Mother mother = new Mother();
mother.narrate(new Book());
运行结果:&
妈妈开始讲故事&
很久很久以前有一个阿拉伯的故事……&
运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:
class Newspaper
public String getContent()
return &林书豪38+7领导尼克斯击败湖人……&;
这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。&
假如以后需求换成杂志呢?换成网页呢?&
还要不断地修改Mother,这显然不是好的设计。&
原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。&
我们引入一个抽象的接口IReader。&
读物,只要是带字的都属于读物:
interface IReader
String getContent();
Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,&
他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:
interface IReader
String getContent();
class Newspaper : IReader
public String getContent()
return &林书豪17+9助尼克斯击败老鹰……&;
class Book : IReader
public String getContent()
return &很久很久以前有一个阿拉伯的故事……&;
class Mother
public void narrate(IReader reader)
Debug.Log(&妈妈开始讲故事&);
Debug.Log(reader.getContent());
public class Client
public static void main(String[] args)
Mother mother = new Mother();
mother.narrate(new Book());
mother.narrate(new Newspaper());
运行结果:&
妈妈开始讲故事&
很久很久以前有一个阿拉伯的故事……&
妈妈开始讲故事&
林书豪17+9助尼克斯击败老鹰……
这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。&
这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。&
所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。&
采用依赖倒置原则给多人并行开发带来了极大的便利,
比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。&
修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。&
参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。&
现在很流行的TDD开发模式就是依赖倒置原则最成功的应用。
在实际编程中,我们一般需要做到如下3点:&
1.低层模块尽量都要有抽象类或接口,或者两者都有。&
2.变量的声明类型尽量是抽象类或接口。使用继承时遵循里氏替换原则。&
3.依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
原则4:接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。&
将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。&
举例来说明接口隔离原则:
(图1 未遵循接口隔离原则的设计)&
这个图的意思是:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。&
类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。&
对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。&
对类图不熟悉的可以参照程序代码来理解,代码如下:
interface I
void method1();
void method2();
void method3();
void method4();
void method5();
public void depend1(I i)
i.method1();
public void depend2(I i)
i.method2();
public void depend3(I i)
i.method3();
class B : I
public void method1()
Debug.Log(&类B实现接口I的方法1&);
public void method2()
Debug.Log(&类B实现接口I的方法2&);
public void method3()
Debug.Log(&类B实现接口I的方法3&);
//对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method4() { }
public void method5() { }
public void depend1(I i)
i.method1();
public void depend2(I i)
i.method4();
public void depend3(I i)
i.method5();
class D : I
public void method1()
Debug.Log(&类D实现接口I的方法1&);
//对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method2() { }
public void method3() { }
public void method4()
Debug.Log(&类D实现接口I的方法4&);
public void method5()
Debug.Log(&类D实现接口I的方法5&);
public class Client
void Start()
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D()));
c.depend2(new D());
c.depend3(new D());
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。&
如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。&
在这里我们将原有的接口I拆分为三个接口,拆分后的设计如图2所示:&
(图2 遵循接口隔离原则的设计)&
照例贴出程序的代码,供不熟悉类图的朋友参考:
interface I1
void method1();
interface I2
void method2();
void method3();
interface I3
void method4();
void method5();
public void depend1(I1 i)
i.method1();
public void depend2(I2 i)
i.method2();
public void depend3(I2 i)
i.method3();
class B : I1, I2
public void method1()
Debug.Log(&类B实现接口I1的方法1&);
public void method2()
Debug.Log(&类B实现接口I2的方法2&);
public void method3()
Debug.Log(&类B实现接口I2的方法3&);
public void depend1(I1 i)
i.method1();
public void depend2(I3 i)
i.method4();
public void depend3(I3 i)
i.method5();
class D : I1, I3
public void method1()
Debug.Log(&类D实现接口I1的方法1&);
public void method4()
Debug.Log(&类D实现接口I3的方法4&);
public void method5()
Debug.Log(&类D实现接口I3的方法5&);
接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。&
也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。&
本文例子中,将一个庞大的接口变更为3个专用的接口所采用的就是接口隔离原则。
在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。&
接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。&
说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。&
其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。&
其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;
而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。&
采用接口隔离原则对接口进行约束时,要注意以下几点:&
1.接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。&
2.为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。&
3.提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。&
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
原则5:迪米特法则
定义:一个对象应该对其他对象保持最少的了解。
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。&
因此,尽量降低类与类之间的耦合。&
自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。&
无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。&
低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。
迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。&
通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。&
迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:&
每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。&
耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,&
而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
举一个例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。&
先来看一下违反迪米特法则的设计。
//总公司员工
class Employee
public void setId(String id)
public String getId()
//分公司员工
class SubEmployee
public void setId(String id)
public String getId()
class SubCompanyManager
public List&SubEmployee& getAllEmployee()
List&SubEmployee& list = new List&SubEmployee&();
for (int i = 0; i & 100; i++)
SubEmployee emp = new SubEmployee();
//为分公司人员按顺序分配一个ID
emp.setId(&分公司& + i);
list.Add(emp);
class CompanyManager
public List&Employee& getAllEmployee()
List&Employee& list = new List&Employee&();
for (int i = 0; i & 30; i++)
Employee emp = new Employee();
//为总公司人员按顺序分配一个ID
emp.setId(&总公司& + i);
list.Add(emp);
public void printAllEmployee(SubCompanyManager sub)
List&SubEmployee& list1 = sub.getAllEmployee();
foreach (SubEmployee e in list1)
Debug.Log(e.getId());
List&Employee& list2 = this.getAllEmployee();
foreach (Employee e in list2)
Debug.Log(e.getId());
public class Client
void Start()
CompanyManager e = new CompanyManager();
e.printAllEmployee(new SubCompanyManager());
现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,&
而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,&
与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。&
按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下:
class SubCompanyManager
public List&SubEmployee& getAllEmployee()
List&SubEmployee& list = new List&SubEmployee&();
for (int i = 0; i & 100; i++)
SubEmployee emp = new SubEmployee();
//为分公司人员按顺序分配一个ID
emp.setId(&分公司& + i);
list.Add(emp);
public void printEmployee()
List&SubEmployee& list = this.getAllEmployee();
foreach (SubEmployee e in list)
Debug.Log(e.getId());
class CompanyManager
public List&Employee& getAllEmployee()
List&Employee& list = new List&Employee&();
for (int i = 0; i & 30; i++)
Employee emp = new Employee();
//为总公司人员按顺序分配一个ID
emp.setId(&总公司& + i);
list.Add(emp);
public void printAllEmployee(SubCompanyManager sub)
sub.printEmployee();
List&Employee& list2 = this.getAllEmployee();
foreach (Employee e in list2)
Debug.Log(e.getId());
主要的变化如下图所示:
修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。
迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。&
但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,&
总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。&
过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。&
所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
原则6:开闭原则
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,&
可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。&
因此,当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。&
闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,&
它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。&
以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。&
在仔细思考以及仔细阅读很多设计模式的文章后,终于对开闭原则有了一点认识。&
其实,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。
也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,&
前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;&
如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。&
其实,开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。&
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。&
而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。&
当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
最后说明一下如何去遵守这六个原则。&
对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。&
任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。&
对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。&
如果大家对这六项原则的理解跟我有所不同,欢迎指正
本文转载自
&&相关文章推荐
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:47766次
排名:千里之外
原创:32篇
转载:47篇
(1)(2)(4)(6)(4)(12)(3)(5)(13)(1)(4)(4)(3)(18)}

我要回帖

更多关于 华硕主板cpu超频教程 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信