今天这堂培训课讲什么呢?我既不讲Spring,也不讲Hibernate,更不讲Ext,我不讲任何一个具体的技术。咱们抛开任何具体的技术,来谈谈如何提升代码质量。如何提升代码质量,相信不只是在座全部人苦恼的事情,也是全部软件项目苦恼的事情。如何提升代码质量呢,我认为咱们首先要理解什么是高质量的代码。前端
高质量代码的三要素程序员
咱们评价高质量代码有三要素:可读性、可维护性、可变动性。咱们的代码要一个都不能少地达到了这三要素的要求才能算高质量的代码。web
1. 可读性强spring
一提到可读性彷佛有一些老生常谈的味道,但使人沮丧的是,虽然你们一而再,再而三地强调可读性,但咱们的代码在可读性方面依然作得很是糟糕。因为工做的须要,我经常须要去阅读他人的代码,维护他人设计的模块。每当我看到大段大段、密密麻麻的代码,并且尚未任何的注释时经常感慨不已,深深体会到了这项工做的重要。因为分工的须要,咱们写的代码不免须要别人去阅读和维护的。而对于许多程序员来讲,他们不多去阅读和维护别人的代码。正由于如此,他们不多关注代码的可读性,也对如何提升代码的可读性缺少切身体会。有时即便为代码编写了注释,也经常是注释语言晦涩难懂形同天书,令阅读者反复斟酌依然不明其意。针对以上问题,我给你们如下建议:编程
1)不要编写大段的代码windows
若是你有阅读他人代码的经验,当你看到别人写的大段大段的代码,并且还不怎么带注释,你是怎样的感受,是否是“嗡”地一声头大。各类各样的功能纠缠在一个方法中,各类变量来回调用,相信任何人多不会认为它是高质量的代码,但却频繁地出如今咱们编写的程序了。若是如今你再回顾本身写过的代码,你会发现,稍微编写一个复杂的功能,几百行的代码就出去了。一些比较好的办法就是分段。将大段的代码通过整理,分为功能相对独立的一段又一段,而且在每段的前端编写一段注释。这样的编写,比前面那些杂乱无章的大段代码确实进步了很多,但它们在功能独立性、可复用性、可维护性方面依然不尽人意。从另外一个比较专业的评价标准来讲,它没有实现低耦合、高内聚。我给你们的建议是,将这些相对独立的段落另外封装成一个又一个的函数。设计模式
许多大师在本身的经典书籍中,都鼓励咱们在编写代码的过程当中应当养成不断重构的习惯。咱们在编写代码的过程当中经常要编写一些复杂的功能,起初是写在一个类的一个函数中。随着功能的逐渐展开,咱们开始对复杂功能进行概括整理,整理出了一个又一个的独立功能。这些独立功能有它与其它功能相互交流的输入输出数据。当咱们分析到此处时,咱们会很是天然地要将这些功能从原函数中分离出来,造成一个又一个独立的函数,供原函数调用。在编写这些函数时,咱们应当仔细思考一下,为它们取一个释义名称,并为它们编写注释(后面还将详细讨论这个问题)。另外一个须要思考的问题是,这些函数应当放到什么地方。这些函数可能放在原类中,也可能放到其它相应职责的类中,其遵循的原则应当是“职责驱动设计”(后面也将详细描述)。数组
下面是我编写的一个从XML文件中读取数据,将其生成工厂的一个类。这个类最主要的一段程序就是初始化工厂,该功能概括起来就是三部分功能:用各类方式尝试读取文件、以DOM的方式解析XML数据流、生成工厂。而这些功能被我概括整理后封装在一个不一样的函数中,而且为其取了释义名称和编写了注释:服务器
Java代码框架
/** * 初始化工厂。根据路径读取XML文件,将XML文件中的数据装载到工厂中 * @param path XML的路径 */ public void initFactory(String path){ if(findOnlyOneFileByClassPath(path)){return;} if(findResourcesByUrl(path)){return;} if(findResourcesByFile(path)){return;} this.paths = new String[]{path}; } /** * 初始化工厂。根据路径列表依次读取XML文件,将XML文件中的数据装载到工厂中 * @param paths 路径列表 */ public void initFactory(String[] paths){ for(int i=0; i<paths.length; i++){ initFactory(paths[i]); } this.paths = paths; } /** * 从新初始化工厂,初始化所需的参数,为上一次初始化工厂所用的参数。 */ public void reloadFactory(){ initFactory(this.paths); } /** * 采用ClassLoader的方式试图查找一个文件,并调用<code>readXmlStream()</code>进行解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findOnlyOneFileByClassPath(String path){ boolean success = false; try { Resource resource = new ClassPathResource(path, this.getClass()); resource.setFilter(this.getFilter()); InputStream is = resource.getInputStream(); if(is==null){return false;} readXmlStream(is); success = true; } catch (SAXException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } catch (IOException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } return success; } /** * 采用URL的方式试图查找一个目录中的全部XML文件,并调用<code>readXmlStream()</code>进行解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findResourcesByUrl(String path){ boolean success = false; try { ResourcePath resourcePath = new PathMatchResource(path, this.getClass()); resourcePath.setFilter(this.getFilter()); Resource[] loaders = resourcePath.getResources(); for(int i=0; i<loaders.length; i++){ InputStream is = loaders[i].getInputStream(); if(is!=null){ readXmlStream(is); success = true; } } } catch (SAXException e) { log.debug("Error when findResourcesByUrl:"+path,e); } catch (IOException e) { log.debug("Error when findResourcesByUrl:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findResourcesByUrl:"+path,e); } return success; } /** * 用File的方式试图查找文件,并调用<code>readXmlStream()</code>解析 * @param path XML文件的路径 * @return 是否成功 */ protected boolean findResourcesByFile(String path){ boolean success = false; FileResource loader = new FileResource(new File(path)); loader.setFilter(this.getFilter()); try { Resource[] loaders = loader.getResources(); if(loaders==null){return false;} for(int i=0; i<loaders.length; i++){ InputStream is = loaders[i].getInputStream(); if(is!=null){ readXmlStream(is); success = true; } } } catch (IOException e) { log.debug("Error when findResourcesByFile:"+path,e); } catch (SAXException e) { log.debug("Error when findResourcesByFile:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findResourcesByFile:"+path,e); } return success; } /** * 读取并解析一个XML的文件输入流,以Element的形式获取XML的根, * 而后调用<code>buildFactory(Element)</code>构建工厂 * @param inputStream 文件输入流 * @throws SAXException * @throws IOException * @throws ParserConfigurationException */ protected void readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{ if(inputStream==null){ throw new ParserConfigurationException("Cann't parse source because of InputStream is null!"); } DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(this.isValidating()); factory.setNamespaceAware(this.isNamespaceAware()); DocumentBuilder build = factory.newDocumentBuilder(); Document doc = build.parse(new InputSource(inputStream)); Element root = doc.getDocumentElement(); buildFactory(root); } /** * 用从一个XML的文件中读取的数据构建工厂 * @param root 从一个XML的文件中读取的数据的根 */ protected abstract void buildFactory(Element root);
在编写代码的过程当中,一般有两种不一样的方式。一种是从下往上编写,也就是按照顺序,每分出去一个函数,都要将这个函数编写完,才回到主程序,继续往下编写。而一些更有经验的程序员会采用另一种从上往下的编写方式。当他们在编写程序的时候,每一个被分出去的程序,能够暂时只写一个空程序而不去具体实现功能。当主程序完成之后,再一个个实现它的全部子程序。采用这样的编写方式,可使复杂程序有更好的规划,避免只见树木不见森林的弊病。
有多少代码就算大段代码,每一个人有本身的理解。我编写代码,每当达到15~20行的时候,我就开始考虑是否须要重构代码。同理,一个类也不该当有太多的函数,当函数达到必定程度的时候就应该考虑分为多个类了;一个包也不该当有太多的类······
2)释义名称与注释
咱们在命名变量、函数、属性、类以及包的时候,应当仔细想一想,使名称更加符合相应的功能。咱们经常在说,设计一个系统时应当有一个或多个系统分析师对整个系统的包、类以及相关的函数和属性进行规划,但在一般的项目中这都很是难于作到。对它们的命名更多的仍是程序员来完成。可是,在一个项目开始的时候,应当对项目的命名出台一个规范。譬如,在个人项目中规定,新增记录用new或add开头,更新记录用edit或mod开头,删除用del开头,查询用find或query开头。使用最乱的就是get,所以我规定,get开头的函数仅仅用于获取类属性。
注释是每一个项目组都在不断强调的,但是依然有许多的代码没有任何的注释。为何呢?由于每一个项目在开发过程当中每每时间都是很是紧的。在紧张的代码开发过程当中,注释每每就渐渐地被忽略了。利用开发工具的代码编写模板也许能够解决这个问题。
用咱们经常使用的MyEclipse为例,在菜单“window>>Preferences>>Java>>Code Style>>Code Templates>>Comments”中,能够简单的修改一下。
“Files”表明的是咱们每新建一个文件(多是类也多是接口)时编写的注释,我一般设定为:
Java代码
/* * created on ${date} */
“Types”表明的是咱们新建的接口或类前的注释,我一般设定为:
Java代码
/** * * @author ${user} */
第一行为一个空行,是用于你写该类的注释。若是你采用“职责驱动设计”,这里首先应当描述的是该类的职责。若是须要,你能够写该类一些重要的方法及其用法、该类的属性及其中文含义等。
${user}表明的是你在windows中登录的用户名。若是这个用户名不是你的名称,你能够直接写死为你本身的名称。
其它我一般都保持为默认值。经过以上设定,你在建立类或接口的时候,系统将自动为你编写好注释,而后你能够在这个基础上进行修改,大大提升注释编写的效率。
同时,若是你在代码中新增了一个函数时,经过Alt+Shift+J快捷键,能够按照模板快速添加注释。
在编写代码时若是你编写的是一个接口或抽象类,我还建议你在@author后面增长@see注释,将该接口或抽象类的全部实现类列出来,由于阅读者在阅读的时候,寻找接口或抽象类的实现类比较困难。
Java代码
/** * 抽象的单表数组查询实现类,仅用于单表查询 * @author 范钢 * @see com.htxx.support.query.DefaultArrayQuery * @see com.htxx.support.query.DwrQuery */ public abstract class ArrayQuery implements ISingleQuery { ...
2. 可维护性
软件的可维护性有几层意思,首先的意思就是可以适应软件在部署和使用中的各类状况。从这个角度上来讲,它对咱们的软件提出的要求就是不能将代码写死。
1)代码不能写死
我曾经见个人同事将系统要读取的一个日志文件指定在C盘的一个固定目录下,若是系统部署时没有这个目录以及这个文件就会出错。若是他将这个决定路径下的目录改成相对路径,或者经过一个属性文件能够修改,代码岂不就写活了。通常来讲,我在设计中须要使用日志文件、属性文件、配置文件,一般都是如下几个方式:将文件放到与类相同的目录,使用ClassLoader.getResource()来读取;将文件放到classpath目录下,用File的相对路径来读取;使用web.xml或另外一个属性文件来制定读取路径。
我也曾见另外一家公司的软件要求,在部署的时候必须在C:/bea目录下,若是换成其它目录则不能正常运行。这样的设定经常为软件部署时带来许多的麻烦。若是服务器在该目录下已经没有多余空间,或者已经有其它软件,将是很挠头的事情。
2)预测可能发生的变化
除此以外,在设计的时候,若是将一些关键参数放到配置文件中,能够为软件部署和使用带来更多的灵活性。要作到这一点,要求咱们在软件设计时,应当有更多的意识,考虑到软件应用中可能发生的变化。好比,有一次我在设计财务软件的时候,考虑到一些单据在制做时的前置条件,在不一样企业使用的时候,可能要求不同,有些企业可能要求严格些而有些要求松散些。考虑到这种可能的变化,我将前置条件设计为可配置的,就可能方便部署人员在实际部署中进行灵活变化。然而这样的配置,必要的注释说明是很是必要的。
软件可维护性的另外一层意思就是软件的设计便于往后的变动。这一层意思与软件的可变动性是重合的。全部的软件设计理论的发展,都是从软件的可变动性这一要求逐渐展开的,它成为了软件设计理论的核心。
3. 可变动性
前面我提到了,软件的变动性是全部软件理论的核心,那么什么是软件的可变动性呢?按照如今的软件理论,客户对软件的需求时时刻刻在发生着变化。当软件设计好之后,为应对客户需求的变动而进行的代码修改,其所须要付出的代价,就是软件设计的可变动性。因为软件合理的设计,修改所付出的代价越小,则软件的可变动性越好,即代码设计的质量越高。一种很是理想的状态是,不管客户需求怎样变化,软件只需进行适当的修改就可以适应。但这之因此称之为理想状态,由于客户需求变化是有大有小的。若是客户需求变化很是大,即便再好的设计也没法应付,甚至从新开发。然而,客户需求的适当变化,一个合理的设计可使得变动代价最小化,延续咱们设计的软件的生命力。
1)经过提升代码复用提升可维护性
我曾经遇到过这样一件事,我要维护的一个系统由于应用范围的扩大,它对机关级次的计算方式须要改变一种策略。若是这个项目统一采用一段公用方法来计算机关级次,这样一个修改实在太简单了,就是修改这个公用方法便可。可是,事实却不同,对机关级次计算的代码遍及整个项目,甚至有些还写入到了那些复杂的SQL语句中。在这样一种状况下,这样一个需求的修改无异于须要遍历这个项目代码。这样一个实例显示了一个项目代码复用的重要,然而不幸的是,代码没法很好复用的状况遍及咱们全部的项目。代码复用的道理十分简单,但要具体运做起来很是复杂,它除了须要很好的代码规划,还须要持续地代码重构。
对整个系统的总体分析与合理规划能够根本地保证代码复用。系统分析师经过用例模型、领域模型、分析模型的一步一步分析,最后经过正向工程,生成系统须要设计的各类类及其各自的属性和方法。采用这种方法,功能被合理地划分到这个类中,能够很好地保证代码复用。
采用以上方法虽然好,但技术难度较高,须要有高深的系统分析师,并非全部项目都能广泛采用的,特别是时间比较紧张的项目。经过开发人员在设计过程当中的重构,也许更加实用。当某个开发人员在开发一段代码时,发现该功能与前面已经开发功能相同,或者部分相同。这时,这个开发人员能够对前面已经开发的功能进行重构,将能够通用的代码提取出来,进行相应的改造,使其具备必定的通用性,便于各个地方可使用。
一些比较成功的项目组会指定一个专门管理通用代码的人,负责收集和整理项目组中各个成员编写的、能够通用的代码。这个负责人同时也应当具备必定的代码编写功力,由于将专用代码提高为通用代码,或者之前使用该通用代码的某个功能,因为业务变动,而对这个通用代码的变动要求,都对这个负责人提出了很高的能力要求。
虽而后一种方式很是实用,可是它有些亡羊补牢的味道,不能从总体上对项目代码进行有效规划。正由于两种方法各有利弊,所以在项目中应当配合使用。
2)利用设计模式提升可变动性
对于初学者,软件设计理论经常感受晦涩难懂。一个快速提升软件质量的捷径就是利用设计模式。这里说的设计模式,不只仅指经典的32个模式,是一切前人总结的,咱们能够利用的、更加普遍的设计模式。
a. if...else...
这个我也不知道叫什么名字,最先是哪位大师总结的,它出如今Larman的《UML与模式应用》,也出如今出如今Mardin的《敏捷软件开发》。它是这样描述的:当你发现你必需要设计这样的代码:“if...elseif...elseif...else...”时,你应当想到你的代码应当重构一下了。咱们先看看这样的代码有怎样的特色。
Java代码
if(var.equals("A")){ doA(); } else if(var.equals("B")){ doB(); } else if(var.equals("C")){ doC(); } else{ doD(); }
这样的代码很常见,也很是日常,咱们你们都写过。但正是这样日常才隐藏着咱们永远没有注意的问题。问题就在于,若是某一天这个选项再也不仅仅是A、B、C,而是增长了新的选项,会怎样呢?你也许会说,那没有关系,我把代码改改就行。然而事实上并不是如此,在大型软件研发与维护中有一个原则,每次的变动尽可能不要去修改原有的代码。若是咱们重构一下,能保证不修改原有代码,仅仅增长新的代码就能应付选项的增长,这就增长了这段代码的可维护性和可变动性,提升了代码质量。那么,咱们应当如何去作呢?
通过深刻分析你会发现,这里存在一个对应关系,即A对应doA(),B对应doB()...若是将doA()、doB()、doC()...与原有代码解耦,问题就解决了。如何解耦呢?设计一个接口X以及它的实现A、B、C...每一个类都包含一个方法doX(),而且将doA()的代码放到A.doX()中,将doB()的代码放到B.doX()中...通过以上的重构,代码仍是这些代码,效果却彻底不同了。咱们只须要这样写:
Java代码
X x = factory.getBean(var); x.doX();
这样就能够实现以上的功能了。咱们看到这里有一个工厂,放着全部的A、B、C...而且与它们的key对应起来,而且写在配置文件中。若是出现新的选项时,经过修改配置文件就能够无限制的增长下去。
这个模式虽然有效提升了代码质量,可是不能滥用,并不是只要出现if...else...就须要使用。因为它使用了工厂,必定程度上增长了代码复杂度,所以仅仅在选项较多,而且增长选项的可能性很大的状况下才可使用。另外,要使用这个模式,继承我在附件中提供的抽象类XmlBuildFactoryFacade就能够快速创建一个工厂。若是你的项目放在spring或其它可配置框架中,也能够快速创建工厂。设计一个Map静态属性并使其V为这些A、B、C...这个工厂就创建起来了。
b. 策略模式
也许你看过策略模式(strategy model)的相关资料但没有留下太多的印象。一个简单的例子可让你快速理解它。若是一个员工系统中,员工被分为临时工和正式工而且在不一样的地方相应的行为不同。在设计它们的时候,你确定设计一个抽象的员工类,而且设计两个继承类:临时工和正式工。这样,经过下溯类型,能够在不一样的地方表现出临时工和正式工的各自行为。在另外一个系统中,员工被分为了销售人员、技术人员、管理人员而且也在不一样的地方相应的行为不同。一样,咱们在设计时也是设计一个抽象的员工类,而且设计数个继承类:销售人员、技术人员、管理人员。如今,咱们要把这两个系统合并起来,也就是说,在新的系统中,员工既被分为临时工和正式工,又被分为了销售人员、技术人员、管理人员,这时候如何设计。若是咱们仍是使用以往的设计,咱们将不得不设计不少继承类:销售临时工、销售正式工、技术临时工、技术正式工。。。如此的设计,在随着划分的类型,以及每种类型的选项的增多,呈笛卡尔增加。经过以上一个系统的设计,咱们不得不发现,咱们以往学习的关于继承的设计遇到了挑战。
解决继承出现的问题,有一个最好的办法,就是采用策略模式。在这个应用中,员工之因此要分为临时工和正式工,无非是由于它们的一些行为不同,好比,发工资时的计算方式不一样。若是咱们在设计时不将员工类分为临时工类和正式工类,而仅仅只有员工类,只是在类中增长“工资发放策略”。当咱们建立员工对象时,根据员工的类型,将“工资发放策略”设定为“临时工策略”或“正式工策略”,在计算工资时,只须要调用策略类中的“计算工资”方法,其行为的表现,也设计临时工类和正式工类是同样的。一样的设计能够放到销售人员策略、技术人员策略、管理人员策略中。一个一般的设计是,咱们将某一个影响更大的、或者选项更少的属性设计成继承类,而将其它属性设计成策略类,就能够很好的解决以上问题。
使用策略模式,你一样把代码写活了,由于你能够无限制地增长策略。可是,使用策略模式你一样须要设计一个工厂——策略工厂。以上实例中,你须要设计一个发放工资策略工厂,而且在工厂中将“临时工”与“临时工策略”对应起来,将“正式工”与“正式工策略”对应起来。
c. 适配器模式
个人笔记本是港货,它的插头与咱们经常使用的插座不同,全部我出差的时候我必须带一个适配器,才能使用不一样地方的插座。这是一个对适配器模式最经典的描述。当咱们设计的系统要与其它系统交互,或者咱们设计的模块要与其它模块交互时,这种交互多是调用一个接口,或者交换一段数据,接受方经常因发送方对协议的变动而频繁变动。这种变动,多是接受方来源的变动,好比原来是A系统,如今变成B系统了;也多是接受方自身的代码变动,如原来的接口如今增长了一个参数。因为发送方的变动经常致使接受方代码的不稳定,即频繁跟着修改,为接受方的维护带来困难。
遇到这样的问题,一个有经验的程序员立刻想到的就是采用适配器模式。在设计时,我方的接口按照某个协议编写,而且保持固定不变。而后,在与真正对方接口时,在前段设计一个适配器类,一旦对方协议发生变动,我能够换个适配器,将新协议转换成原协议,问题就解决了。适配器模式应当包含一个接口和它的实现类。接口应当包含一个本系统要调用的方法,而它的实现类分别是与A系统接口的适配器、与B系统接口的适配器...
我曾经在一个项目中须要与另外一个系统接口,起初那个系统经过一个数据集的方式为我提供数据,我写了一个接收数据集的适配器;后来改成用一个XML数据流的形式,我又写了一个接收XML的适配器。虽然为我提供数据的方式不一样,可是通过适配器转换后,输出的数据是同样的。经过在spring中的配置,我能够灵活地切换究竟是使用哪一个适配器。
d. 模板模式
32个经典模式中的模板模式,对开发者的代码规划能力提出了更高的要求,它要求开发者对本身开发的全部代码有一个相互联系和从中抽象的能力,从各个不一样的模块和各个不一样的功能中,抽象出其过程比较一致的通用流程,最终造成模板。譬如说,读取XML并造成工厂,是许多模块经常要使用的功能。它们虽然有各自的不一样,可是整体流程都是同样的:读取XML文件、解析XML数据流、造成工厂。正由于有这样的特征,它们可使用共同的模板,那么,什么是模板模式呢?
模板模式(Template Model)一般有一个抽象类。在这个抽象类中,一般有一个主函数,按照必定地顺序去调用其它函数。而其它函数每每是某这个连续过程当中的各个步骤,如以上实例中的读取XML文件、解析XML数据流、造成工厂等步骤。因为这是一个抽象类,这些步骤函数能够是抽象函数。抽象类仅仅定义了整个过程的执行顺序,以及一些能够通用的步骤(如读取XML文件和解析XML数据流),而另外一些比较个性的步骤,则由它的继承类本身去完成(如上例中的“造成工厂”,因为各个工厂各不同,所以由各自的继承类本身去决定它的工厂是怎样造成的)。
各个继承类能够根据本身的须要,经过重载从新定义各个步骤函数。可是,模板模式要求不能重载主函数,所以正规的模板模式其主函数应当是final(虽然咱们经常不这么写)。另外,模板模式还容许你定义的这个步骤中,有些步骤是可选步骤。对与可选步骤,咱们一般称为“钩子(hood)”。它在编写时,在抽象类中并非一个抽象函数,但倒是一个什么都不写的空函数。继承类在编写时,若是须要这个步骤则重载这个函数,不然就什么也不写,进而在执行的时候也如同什么都没有执行。
经过以上对模板模式的描述能够发现,模板模式能够大大地提升咱们的代码复用程度。
以上一些经常使用设计模式,都能使咱们快速提升代码质量。仍是那句话,设计模式不是什么高深的东西,偏偏相反,它是初学者快速提升的捷径。然而,若是说提升代码复用是提升代码质量的初阶,使用设计模式也只能是提升代码质量的中阶。那么,什么是高阶呢?我认为是那些分析设计理论,更具体地说,就是职责驱动设计和领域驱动设计。
3)职责驱动设计和领域驱动设计
前面我提到,当咱们尝试写一些复杂功能的时候,咱们把功能分解成一个个相对独立的函数。可是,应当将这些函数分配到哪一个类中呢?也就是系统中的全部类都应当拥有哪些函数呢?或者说应当表现出哪些行为呢?答案就在这里:以职责为中心,根据职责分配行为。咱们在分析系统时,首先是根据客户需求进行用例分析,而后根据用例绘制领域模式和分析模型,整个系统最主要的类就造成了。经过以上分析造成的类,每每和现实世界的对象是对应的。正由于如此,软件世界的这些类也具备了与现实世界的对象相对应的职责,以及在这些职责范围内的行为。
职责驱动设计(Responsibility Drive Design,RDD)是Craig Larman在他的经典著做《UML和模式应用》中提出的。职责驱动设计的核心思想,就是咱们在对一个系统进行分析设计的时候,应当以职责为中心,根据职责分配行为。这种思想首先要求咱们设计的全部软件世界的对象,应当与现实世界尽可能保持一致,他称之为“低表示差别”。有了低表示差别,一方面提升了代码的可读性,另外一方面,当业务发生变动的时候,也能够根据实际状况快速应对变动。
Craig Larman在提出职责驱动设计理论的同时,还提出了GRASP设计模式,来丰富这个理论。在GRASP设计模式中,我认为,低耦合、高内聚、信息专家模式最有用。
继Craig Larman提出的职责驱动设计数年以后,另外一位大师提出了领域驱动设计。领域驱动设计(Domain Drive Design,DDD)是Eric Evans在他的同名著做《领域驱动设计》中提出的。在以前的设计理论中,领域模型是从用例模型到分析模型之间的一种中间模型,也就是从需求分析到软件开发之间的一种中间模型。这么一个中间模型,既不是需求阶段的重要产物,在开发阶段也不以它做为标准进行开发,仅仅是做为参考,甚至给人感受有一些多余。可是,Evans在领域驱动设计中,将它放到了一个无比重要的位置。按照领域驱动设计的理论,在需求分析阶段,需求分析人员使用领域模型与客户进行沟通;在设计开发阶段,开发人员使用领域模型指导设计开发;在运行维护和二次开发阶段,维护和二次开发人员使用领域模型理解和熟悉系统,并指导他们进行维护和二次开发。总之,在整个软件开发的生命周期中,领域模型都成为了最核心的内容。
领域驱动设计继承了职责驱动设计。在领域驱动设计中强调的,依然是低表示差别,以及职责的分配。可是,如何作到低表示差别呢?如何完成职责分配呢?领域驱动设计给了咱们完美的答案,那就是创建领域模型。领域驱动设计改变了咱们的设计方式。在需求分析阶段,用例模型已再也不是这个阶段的核心,而是创建领域模型。在开发和二次开发阶段,开发人员也再也不是一埋头地猛扎进程序堆里开始编程,而是首先细致地进行领域模型分析。领域驱动设计强调持续精化,使领域模型再也不是一旦完成分析就扔在一边再也不理会的图纸,而是在不断理解业务的基础上不断修改和精化领域模型,进而驱动咱们代码的精化。领域驱动设计强调的再也不是一次软件开发过程当中咱们要作的工做,它看得更加长远,它强调的是一套软件在至关长一段时间内持续升级的过程当中咱们应当作的工做。我认为,领域驱动设计是提升代码质量的最高等级。当时,使用领域驱动设计进行软件开发是一场至关巨大的改革,它颠覆了咱们过去的全部开发模式,咱们必须脚踏实地地一步一步去实践和改变。
职责驱动设计
随着软件业的不断发展,随着软件需求的不断扩大,软件所管理的范围也在不断拓宽。过去一个软件仅仅管理一台电脑的一个小小的功能,而如今被扩展到了一个企业、一个行业、一个产业链。过去咱们开发一套软件,只有少许的二次开发,当它使用到必定时候咱们就抛弃掉从新又开发一套。如今,随着用户对软件依赖程度的不断加大,咱们很难说抛弃一套软件从新开发了,更多的是在一套软件中持续改进,使这套软件的生命周期持续数年以及数个版本。正是由于软件业面临着如此巨大的压力,咱们的代码质量,咱们开发的软件拥有的可变动性和持续改进的能力,成为软件制胜的关键因素,令咱们不能不反思。
代码质量评价的关键指标:低耦合,高内聚
耦合就是对某元素与其它元素之间的链接、感知和依赖的量度。耦合包括:
1.元素B是元素A的属性,或者元素A引用了元素B的实例(这包括元素A调用的某个方法,其参数中包含元素B)。
2.元素A调用了元素B的方法。
3.元素A直接或间接成为元素B的子类。
4.元素A是接口B的实现。
若是一个元素过于依赖其它元素,一旦它所依赖的元素不存在,或者发生变动,则该元素将不能再正常运行,或者不得不相应地进行变动。所以,耦合将大大影响代码的通用性和可变动性。
内聚,更为专业的说法叫功能内聚,是对软件系统中元素职责相关性和集中度的度量。若是元素具备高度相关的职责,除了这些职责内的任务,没有其它过多的工做,那么该元素就具备高内聚性,反之则为低内聚性。内聚就像一个专横的管理者,它只作本身职责范围内的事,而将其它与它相关的事情,分配给别人去作。
高质量的代码要求咱们的代码保持低耦合、高内聚。可是,这个要求是如此的抽象与模糊,如何才能作到这些呢?软件大师们告诉咱们了许多方法,其中之一就是Craig Larman的职责驱动设计。
职责驱动设计(Responsibility Drive Design,RDD)是Craig Larman在他的经典著做《UML和模式应用》中提出的。要理解职责驱动设计,咱们首先要理解“低表示差别”。
低表示差别
咱们开发的应用软件其实是对现实世界的模拟,所以,软件世界与现实世界存在着必然的联系。当咱们在进行需求分析的时候,需求分析员其实是从客户那里在了解现实世界事物的规则、工做的流程。若是咱们在软件分析和设计的过程当中,将软件世界与现实世界紧密地联系到一块儿,咱们的软件将更加本色地还原事物最本质的规律。这样的设计,就称之为“低表示差别”。
采用“低表示差别”进行软件设计,现实世界有什么事物,就映射为软件世界的各类对象(类);现实世界的事物拥有什么样的职责,在软件世界里的对象就拥有什么样的职责;在现实世界中的事物,由于它的职责而产生的行为,在软件世界中就反映为对象所拥有的函数。
低表示差别,使分析设计者对软件的分析和设计更加简单,思路更加清晰;使代码更加可读,阅读者更加易于理解;更重要的是,当需求发生变动,或者业务产生扩展时,设计者只须要遵循事物原本的面貌去思考和修改软件,使软件更加易于变动和扩展。
角色、职责、协做
理解了“低表示差别”,如今咱们来看看咱们应当如何运用职责驱动设计进行分析和设计。首先,咱们经过与客户的沟通和对业务需求的了解,从中提取出现实世界中的关键事物以及相互之间的关系。这个过程咱们一般经过创建领域模型来完成。领域模型创建起来之后,经过诸如Rational Rose这样的设计软件的正向工程,生成了咱们在软件系统中最初始的软件类。这些软件类,因为每一个都扮演着现实世界中的一个具体的角色,于是赋予了各自的职责。前面我已经提到,若是你的系统采用职责驱动设计的思想进行设计开发,做为一个好的习惯,你应当在每个软件类的注释首行,清楚地描述该软件类的职责。
当咱们完成了系统中软件类的制订,分配好了各自的职责,咱们就应该开始根据软件需求,编写各个软件类的功能。在前面我给你们提出了一个建议,就是不要在一个函数中编写大段的代码。编写大段的代码,一般会下降代码的内聚度,由于这些代码中将包含不是该软件类应当完成的工做。做为一个有经验的开发人员,在编写一个功能时,首先应当对功能进行分解。一段稍微复杂的功能,一般均可以被分解成一个个相对独立的步骤。步骤与步骤之间存在着交互,那就是数据的输入输出。经过以上的分解,每个步骤将造成一个独立的函数,而且使用一个能够代表这个步骤意图的释义函数名。接下来,咱们应当考虑的,就是应当将这些函数交给谁。它们有可能交给原软件类,也有可能交给其它软件类,其分配的原则是什么呢?答案是否清楚,那就是职责。每一个软件类表明现实世界的一个事物,或者说一个角色。在现实世界中这个任务应当由谁来完成,那么在软件世界中,这个函数就应当分配给相应的那个软件类。
经过以上步骤的分解,一个功能就分配给了多个软件类,相互协做地完成这个功能。这样的分析和设计,其代码必定是高内聚的和高可读性的。同时,当需求发生变动的时候,设计者经过对现实世界的理解,能够很是轻松地找到那个须要修改的软件类,而不会影响其它类,于是也就变得易维护、易变动和低耦合了。
说了这么多,举一个实例也许更能帮助理解。拿一个员工工资系统来讲吧。当人力资源在发放一个月工资的时候,以及离职的员工确定不能再发放工资了。在系统设计的期初,开发人员商量好,在员工信息中设定一个“离职标志”字段。编写工资发放的开发人员经过查询,将“离职标志”为false的员工查询出来,并为他们计算和发放工资。可是,随着这个系统的不断使用,编写员工管理的开发人员发现,“离职标志”字段已经不能知足客户的需求,于是将“离职标志”字段废弃,并增长了一个“离职时间”字段来管理离职的员工。然而,编写工资发放的开发人员并不知道这样的变动,依然使用着“离职标志”字段。显然,这样的结果就是,软件系统开始对离职员工发放工资了。仔细分析这个问题的缘由,咱们不难发现,确认员工是否离职,并非“发放工资”软件类应当完成的工做,而应当是“员工管理”软件类应当完成的。若是将“获取非离职员工”的任务交给“员工管理”软件类,而“发放工资”软件类仅仅只是去调用,那么离职功能由“离职标志”字段改成了“离职时间”字段,其实就与“发放工资”软件类毫无关系。而做为“员工管理”的开发人员,一旦发生这样的变动,他固然知道去修改本身相应的“获取非离职员工”函数,这样就不会发生以上问题。经过这样一个实例,也许你可以理解“职责驱动设计”的精要与做用了吧。
职责分配与信息专家
经过以上对职责驱动设计的讲述,咱们不难发现,职责驱动设计的精要就是职责分配。可是,在纷繁复杂的软件设计中,如何进行职责分配经常令咱们迷惑。幸运的是,Larman大师清楚地认识到了这一点。在他的著做中,信息专家模式为咱们提供了帮助。
信息专家模式(又称为专家模式)告诉咱们,在分析设计中,应当将职责分配给软件系统中的这样一个软件类,它拥有实现这个职责所必须的信息。咱们称这个软件类,叫“信息专家”。用更加简短的话说,就是将职责分配给信息专家。
为何咱们要将职责分配给信息专家呢?咱们用上面的例子来讲明吧。当“发放工资”软件类须要获取非离职员工时,“员工管理”软件类就是“获取非离职员工”任务的信息专家,由于它掌握着全部员工的信息。假设咱们不将“获取非离职员工”的任务交给“员工管理”软件类,而是另外一个软件类X,那么,为了获取员工信息,软件类X不得不访问“员工管理”软件类,从而使“发放工资”与X耦合,X又与“员工管理”耦合。这样的设计,不如直接将“获取非离职员工”的任务交给“员工管理”软件类,使得“发放工资”仅仅与“员工管理”耦合,从而有效地下降了系统的总体耦合度。
总之,采用“职责驱动设计”的思路,为咱们提升软件开发质量、可读性、可维护性,以及保持软件的持续发展,提供了一个广阔的空间。
PS:暂未找到原出处,如能告知,万分感谢