前文介绍了优雅代码的三个标准:风格好、结构好和性能好的三好代码。对于风格好有很多的代码规范供参考,都是编程习惯的问题,养成就好。但对于结构好这种“内功”则需要一定时间的修炼,理论与实践的互相印证,不断的思辨优秀的设计思想,才能游刃有余地将一些优秀的设计原则应用到实际软件开发中。本文,笔者将重点介绍:设计的由来、糟糕的设计给软件带来的隐患以及优秀的设计应该遵循的五大原则。
一.软件设计的前世今生
早期的软件十分简单,开发过程也并没有划分成明确的阶段。随着软件系统越来越复杂,以前的开发方法已不能解决如此复杂的问题。此时,人们将处理复杂问题的通常做法“分解”应用到软件开发领域,就是将复杂问题分解为若干子问题,然后再逐个击破。于是,软件开发被分成了需求、设计、开发和测试等阶段。此时,设计与实现的划分比较清晰。设计指利用计算机知识对实际问题求解的过程,设计的逻辑层次更高,注重的是与人的交流,如画UML图。而实现指的是运用特定的编程语言,基于设计的规约,解决问题的过程。实现的输出可以直接被计算机处理。
天下“分久必合,合久必分”。随着软件工程的不断发展,尤其是敏捷开发思想的大行其道以及高级编程语言的语义越来越丰富,设计与实现的界限越来越模糊。敏捷开发方法提出源代码就是设计。根据IEEE标准(Std 610.12-1990: IEEE Standard Glossary of Software Engineering Terminology)设计的内涵是:1. Design: (a)The process of defining the architecture, components, interfaces, and other characteristics of a system or component;(b)The result of the process in (a); 2. Design description: A document that describes the design of a system or component. Typical contents include system or component architecture, control logic, data structures, input/output formats, interface descriptions, and algorithms. Syn: design document; design specification.
二.糟糕的设计散发的七种臭气:
糟糕的软件设计往往是有迹可循的,可总结为七种臭气,这些臭气使我们的软件变得难以变更、难以理解和难以复用。而且有个奇怪的特点,它们往往是扎堆出现。当你发现软件存在这七种臭气时,意味着你需要重构代码。
1. 僵化性:很难对软件进行改动,因为每个改动都会迫使许多系统其他地方进行改动。通常指导致其他部分重新编译、连接和部署。
2. 脆弱性:对系统的改动导致系统中和改动的地方在概念上无关的许多地方出现问题。往往是指导致其他地方莫名奇妙的报错,如:switch语句会导致这种错误。
3. 牢固性:很难将系统中的某部分与其他部分分开,从而导致系统中的任何部分都难以复用。如果非要用,还需要引入与系统无关的功能。
4. 粘滞性:做错误的事情比做正确的事情容易。表现为两种形式:(a)软件粘滞性:破环软件质量的修改比保持原有设计质量的修改要容易实施;(b)环境粘滞性:当开发环境迟钝,低效时,就会产生环境粘滞性。如果编译时间很长,那么开发人员可能会放弃那些能保持设计质量,但是却需要导致大规模重新编译的改动。
5. 不必要的复杂性:设计中包含不具有任何好处的基础结构。这让我想到基于Asp.net MVC的系统架构文中提到的DAL那层似乎违反这个原则。
6. 不必要的重复:设计中包含一些重复的结构,这些结构本可以通过单一的抽象进行统一。例如:源代码级复用就是典型的不必要的重复。
7. 晦涩性:很难阅读和理解。工程化要求的是简单直接,炫技性的复杂设计只会增加软件后期维护的成本。牢记一点:简单才能长久。
三.优秀的设计应遵循的五大原则:
导致上面七大臭气的产生都是因为软件设计违反了下述五大原则:
1. 单一职责原则(SRP):就一个类而言,应该只有一个导致其变化的原因。多职责将导致脆弱性,有时还会导致僵化性。臃肿的类往往违反这个原则。
2. 开放封闭职责(OCP):软件实体(类、模块、函数等)应该是可以扩展的,同时还可以是不必修改的,即:对扩展是开放的、对更改是封闭的。C#中virtual关键字就是用来做到OCP的。
a. 软件是对现实的抽象,世界在不断的变化,所以对扩展要开放,而如果每次扩展都要对已经存在的代码进行修改,就会大大降低软件质量;
b. 实现OCP的关键是抽象,往往用到多态方法。(a) 违反OCP原则往往导致僵化性、脆弱性、牢固性; (b). 封闭是相对的,不可能对所有变化的情况都封闭。敏捷方法认为我们要预测变化,但是直到我们发现他们才行动。
c. OCP原则其实是要求我们清晰的区分策略和具体实现,允许扩展具体的实现形式(开放),同时将这种扩展与策略隔离开来,使其对上层策略透明(封闭)。
3. LisKov替换原则(LSP):子类型必须能够替换他们的父类,违反这个职责将导致脆弱性和对OCP的违反。
a. 基于契约的设计(CBD):契约通过为每一个方法规定前置条件和后置条件来指定。要使一个方法执行,前置条件一定要为真(对客户要求);函数执行后要保证后置条件为真(对函数编写者的要求)。
b. 基类和派生类在前置条件和后置条件上的关系是:如果在派生类中重新申明了基类中已有的成员函数,这个函数只能使用相等和更弱的前置条件来替换原有的前置条件;并且,只能使用相等或更强的后置条件来替换原有的后置条件。总之:派生类必须接受基类已经接受的一切;并且派生类不能违反基类已经确定的规则。
4. 依赖倒置原则(DIP):高层模块不应该依赖于低层模块。二者应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。高层包含更多的策略和业务模型,而低层包含更多的具体实现细节,高层依赖于低层将导致:难以复用,难以维护。
5. 接口隔离原则(ISP):不应该强迫客户依赖于他们不用的方法;一个类的不内聚的“胖接口”应该被分解成多组方法,每一组方法都服务于一组不同的客户程序。“胖接口”将导致单一职责(SRP),替换原则(LSP)被违反,从而导致脆弱、僵化。解决之道:使用委托或者分拆接口。