丰富的线上&线下活动,深入探索云世界
做任务,得社区积分和周边
最真实的开发者用云体验
让每位学生受益于普惠算力
让创作激发创新
资深技术专家手把手带教
遇见技术追梦人
技术交流,直击现场
海量开发者使用工具、手册,免费下载
极速、全面、稳定、安全的开源镜像
开发手册、白皮书、案例集等实战精华
为开发者定制的Chrome浏览器插件
原则我建议学生们把更多的精力放在学习基本思想上,而不是新技术上,因为新技术在他们毕业之前就有可能过时,而基本思想则永远不会过时。—DavidL.Parnas在本章,我将介绍设计良好的和精心制作的软件需要遵循哪些最基本的原则。这些基本原则的特别之处在于,它们并不是只针对某些编程案例或者编程语言,其中一些原则甚至并不是专用于软件开发的。例如,我们讨论的KISS原则可以适用于生活的很多方面,一般来说,不仅是软件开发,把生活中的一切事情变得尽可能简单并不一定都是坏事。也就是说,下面这些原则我们不应该学一次就忘掉,建议熟练掌握它们。这些原则非常重要,理想情况下,它们会成为每个开发人员的第二天性。我在后面章节中即将讨论到的很多具体原则都是基于以下这些基本原则的。
在本书中,你会发现许多编写更好的C++代码和设计良好的软件的原则,但到底什么是原则呢?许多人都有一些指导他们一生的原则。举个例子,如果你因为某些原因不吃肉,那么这可能就是原则;如果你想保护你的小孩,那么你会教他一些原则,指导他可以自己做出正确的决定,比如“不要和陌生人说话!”只要将这个原则记住,孩子就可以在特定的场合下做出正确的决定。原则是一种规则、信仰或者指引你的观念,原则通常与价值观或价值体系直接联系。例如,我们不需要被告知同类相残是错误的,因为人们对人类生活有天生的价值观。更好的一个例子是AgileManifesto[Beck01]包含了12条原则,指导项目团队开展敏捷项目。原则并不是不可改变的法律,更没有明文规定地刻在石头上。而且在编程的时候有时故意违背其中一些原则是有必要的,只要你有充分的理由违背原则,就可以去做,但是做的时候一定要小心!因为结果很可能会出乎你的意料。以下几项基本原则,将会在本书后面的各个章节分别进行回顾及强化。
复制和粘贴是一个设计错误。—DavidL.Parnas虽然这个原则是最重要的,但我确信开发人员经常有意或无意地违反这个原则。DRY是“Don抰repeatyourself!”的缩写。我们应该尽可能避免复制,因为复制是一个非常不好的行为。该原则也称为“OnceAndOnlyOnce”(OAOO)原则。复制是非常危险的,其原因显而易见:当一段代码被修改的时候,也必须相应地修改这段代码的副本,不要抱着不修改副本的期望,可以肯定的是,一定要修改副本。任何复制的代码片段迟早会被忘记,并且,会因为漏改代码的副本而产生bug。就这样,没什么别的了吗?不是的,还有一些需要我们深入讨论的事情。在DaveThomas和AndyHunt的出色的著作《ThePragmaticProgrammer》[Hunt99]中陈述了DRY原则的含义,就是我们要保证“在一个系统内部,任何一个知识点都必须有一个单一的、明确的、权威的陈述。”值得注意的是,Dave和Andy并没有明确地提到代码,他们谈论的是知识点。一个系统的知识所影响的范围远比它的代码更广泛。例如,DRY原则同样也适用于文档、项目、测试计划和系统的配置数据。可以说,DRY原则影响了每一件事情!你可以想象一下,严格遵守这一原则并不像起初看起来那么容易。
这不是信息隐藏,因为类内部的实现部分暴露给了外部环境,尽管该类看起来封装得很好。注意getState返回值的类型,客户端用到的枚举类State用到了这个类,如下示例所示:
枚举类(结构体)[C++11]在C++11中,枚举类型也有了创新。为了向下兼容早期的C++标准,现在仍存在众所周知的枚举类型及其关键字enum。从C++11开始,我们还引入了枚举类和枚举结构体。旧的C++枚举类型有一个坏处是,它们将枚举成员引入周围的命名空间,导致了名称冲突,如下示例所示:
当使用枚举类(也称为“新枚举”或“强枚举”)时,这些问题将不再存在,它们的枚举成员对枚举来说是局部的,并且它们的值不会隐式转换为其他类型(比如另一个枚举或int类型)。
现在,修改AutomaticDoor类的内部要容易实现得多。客户端代码不再依赖于类的内部实现。现在你可以在不引起该类任何用户注意的情况下,删除State枚举并将其替换为另一种实现。
软件开发中的一条通用建议是,任何软件实体(如模块、组件、单元、类、函数等)应该具有很高的(或强的)内聚性。一般来讲,当模块实现定义确切的功能时,应该具有高内聚的特性。为了深入研究该原则,让我们来看两个例子,这两个例子没有太多的关联,从图3-1开始。
在上面的例子中,模块随意划分,业务领域三个不同的功能放在了一个模块内。功能A、功能B和功能C之间基本没有什么共同点,但是这三个功能却被放在MyModel模块中。阅读模块的代码就会发现,功能A、功能B和功能C在不同的、完全独立的数据上运行。现在,观察图3-1中所有的虚线箭头,箭头指向的每一个模块都是一个被依赖者,箭头尾部的模块需要箭头指向的模块来实现。在这种情况下,系统中的其他模块想要使用功能A、功能B或功能C时,调用的模块就会依赖于整个MyModule模块。这样设计的缺点是显而易见的:这会导致太多的依赖,并且可维护性也会降低。为了提高内聚性,功能A、功能B和功能C应该彼此分离(见图3-2)。
3.7松耦合原则考虑下面的示例代码:
这段代码基本上可以正常运行。首先你需要创建Lamp类的实例,然后通过引用方式将Lamp的实例传递给Switch。这个小例子看起来像图3-4描述的那样。这个设计有什么问题?
这个Switch类不再包含Lamp类的引用。相反,它持有了我们新定义的Switchable接口类。
这个Lamp类实现了我们新定义的Switchable接口。
用UML类图表示,我们新设计的类图看起来像下图3-5那样。
这个设计的优点是显而易见的。Switch已经能完全独立于由它控制的具体类。而且,Switch可以通过实现Switchable接口的测试替身进行独立的测试。如果你想控制一个风扇而不是一盏灯呢?也没有问题,这个设计对扩展是开放的。只需要创建一个实现了Switchable接口的风扇类或者电气设备的其他类就可以了,详见图3-6。
最少惊讶原则(POLA/PLA),也称为最少惊喜原则(POLS),它在用户界面设计和人因工程学设计中很知名。该原则指出不应该让用户对用户界面的意外响应而感到惊讶,也不应该对出现或消失的控件、混乱的错误消息、公认的按键序列的异常响应(记住,Ctrl+C是在Windows操作系统中复制应用程序的标准事务,而不是退出程序)或其他意外行为而感到困惑。这个原则也可以很好地应用到软件开发中的API设计中。调用函数不应该让调用者感知到异常行为或一些隐藏的副作用,函数应该完全按照函数名称所暗示的意义执行(请参阅第4章中4.3.3节“函数命名”)。例如,在类的实例上调用getter时不应该修改该对象的内部状态。