




已阅读5页,还剩66页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
Error! No text of specified style in document. 设计模式 III重构既有代码1. 程序员与软件设计当你开始学习这本书时,应该已经写过上万行代码了吧。此时你已经配得上“程序员”这个称号,虽然需要在前面加上“初级”二字。再积累几万行代码,你就可以摘下“初级”的帽子,换上一顶新帽子:“平庸”或“高级”。两者区别何在?平庸程序员是流水线上的加工员、是一个翻译者,将设计师勾勒的模型简单的转换为代码;高级程序员是思想者、创造者,在技术领域内追求更完美的实现,创造更有价值的代码。前者类似于工匠,后者类似于艺术家,成为前者还是后者,软件设计能力是一个重要的衡量指标。图1 关于平庸程序员的漫画一个很常见的误解是程序员并不需要了解设计,企业中有架构设计师会帮你完成一切。实际上在很多企业中专职的架构设计师只是一种传说,程序员必须自己决定该做什么以及怎么做,设计能力的缺失导致了大量冗余、低效、充满Bug且不易维护的代码,甚至一些工程失败的直接原因就是编码能力的低下。反之具备设计能力的程序员在编写代码时具有章法,不急于动手,先进行全盘的思考、制定方案,再配合优秀的开发模式,往往能在更短的时间内编写出更高质量的代码。有统计数据指出,一个高级程序员与一个平庸程序员的产出比可达10 : 1,显然企业更愿意使用高级程序员以节省成本。具备软件设计能力是高级程序员与平庸程序员的分水岭,如何提升软件设计能力就是本书所关注的内容。本书从四种不同的角度介绍了软件设计,第1章介绍了“重构”这一利器,用于对已有的代码进行二次设计,第2章介绍了面向对象编程的重要原则,可以作为代码结构是否优秀的判断标准,第3、4、5章介绍了常用的设计模式,类似于中国功夫中的“招式”,使用设计模式可以解决大部分常见的问题。在学习本书时应该注意到:软件设计的基本理论好学,但灵活应用却不容易,需要持续对基本理论进行思考、推敲。学习完本书并不是学习软件设计的结束、而只是开始,在后续其他课程的学习中,应该不断将本书所讲内容与新的知识进行比对、参照、应用,这样才能快速的提高软件设计能力。2. 重构重构指在不改变代码功能的情况下,调整代码的架构,使代码具有更好的可读性、可扩展性等优点。有三种典型的情况需要使用重构:1. 拿到了别人的代码,要在其基础上修改以添加功能,这时发现原来的代码结构混乱,功能不全,还有一些隐含的bug。2. 自己最初写代码时只注意实现软件的功能,未注意软件的结构,发生再添加新功能时代码无法扩展。3. 需要将代码包装为公用代码供大家使用,需要设计更好的软件结构和接口。可见重构更类似于一种补救的方式,很多程序员在拿到一个需求后总是热心于快速实现功能,而忽略了代码的结构,为以后的维护、升级增加了困难。在软件功能实现后回过头来进行重构,则可以持续保持软件结构的合理性。在重构领域,最具影响力的著作当数Martin Fowler所写的重构改善既有代码的设计一书。重构一书中通过对一段具有代表性的代码不断进行重构展示了重构技术的特点和魅力,下面以一个简单的例子了解重构的基本要素,然后通过重构一书深入了解重构的各种技术手段。 图2 Martin Fowler与其著作3. 开始重构下面展示了一个简单的个人所得税收据打印的例子。Tax类代表申报人信息,包括了申报人姓名,月收入两个属性,Report类为收据打印类,提供Print打印方法,代码如下:代码演示:重构前Tax类using System;namespace Com.QhIt.Before public class Tax private string name; public string Name get return name; set name = value; private double saraly; public double Saraly get return saraly; set saraly = value; public Tax() public Tax(string name, double saraly):base() = name; this.saraly = saraly; 代码演示:重构前Report类using System;namespace Com. XinZhanedu.Before public class Report public void Print(Tax tax) /打印收据头 Console.WriteLine(个人所得税申报专用收据); Console.Write(申报日期: + DateTime.Now.ToString(yyyy-MM-dd); Console.WriteLine(t操作人:张三); Console.WriteLine(-); /打印收据正文 Console.WriteLine(缴税人姓名:t0 ,tax.Name); Console.WriteLine(缴税人月收入:t0:c2元 , tax.Saraly); int level = 0; if (tax.Saraly 2000) level = 1; if (tax.Saraly 4000) level = 2; if (tax.Saraly 6000) level = 3; if (tax.Saraly 8000) level = 4; if (tax.Saraly 10000) level = 5; Console.WriteLine(应缴税级别:t + level + 级); double t = 0; switch (level) case 0: t = 0; break; case 1: t = tax.Saraly * 0.05; break; case 2: t = tax.Saraly * 0.08; break; case 3: t = tax.Saraly * 0.10; break; case 4: t = tax.Saraly * 0.15; break; case 5: t = tax.Saraly * 0.20; break; Console.WriteLine(应缴收税额:t0:c2元,t); 代码演示:重构前测试类using System;namespace Com. XinZhanedu.Before public class Program public static void Main(string args) Tax tax = new Tax(李四, 3500); Report report = new Report(); report.Print(tax); 运行效果如下图所示:图3 重构前 第一次重构:拆分大方法现在代码已经实现了主要功能,但是Report类中一长段Print方法令人隐隐不安。如果将打印收据看做由打印表头和打印内容两部分的话,可以将Print方法拆分PrintHeader和PrintContent两个方法,将大的方法拆分为小的方法,可以增加方法被复用的机率,每个方法也可以独立变化,不会影响其他方法。拆分后的Report类代码如下:代码演示:第一次重构Reportusing System;namespace Com. QhIt.Verson1 public class Report public void Print(Tax tax) PrintHeader(); PrintContent(tax); public void PrintHeader() /打印收据头 Console.WriteLine(个人所得税申报专用收据); Console.Write(申报日期: + DateTime.Now.ToString(yyyy-MM-dd); Console.WriteLine(t操作人:张三); Console.WriteLine(-); public void PrintContent(Tax tax) /打印收据正文 Console.WriteLine(缴税人姓名:t0, tax.Name); Console.WriteLine(缴税人月收入:t0:c2元, tax.Saraly); int level = 0; if (tax.Saraly 2000) level = 1; if (tax.Saraly 4000) level = 2; if (tax.Saraly 6000) level = 3; if (tax.Saraly 8000) level = 4; if (tax.Saraly 10000) level = 5; Console.WriteLine(应缴税级别:t + level + 级); double t = 0; switch (level) case 0: t = 0; break; case 1: t = tax.Saraly * 0.05; break; case 2: t = tax.Saraly * 0.08; break; case 3: t = tax.Saraly * 0.10; break; case 4: t = tax.Saraly * 0.15; break; case 5: t = tax.Saraly * 0.20; break; Console.WriteLine(应缴收税额:t0:c2元, t); 第二次重构:方法只实现单一功能一个方法应该只实现一个单一的功能,观察重构后的PrintConent方法,其中包含了打印单据、计算缴税级别、计算缴税金额三种功能,应继续拆分,这样今后个人所得税计算方式改变后不会影响到打印功能,改变后的Report类代码如下:代码演示:第二次重构Reportusing System;namespace Com. QhIt.Verson2 public class Report public void Print(Tax tax) PrintHeader(); PrintContent(tax); public void PrintHeader() /打印收据头 Console.WriteLine(个人所得税申报专用收据); Console.Write(申报日期: + DateTime.Now.ToString(yyyy-MM-dd); Console.WriteLine(t操作人:张三); Console.WriteLine(-); public void PrintContent(Tax tax) /打印收据正文 Console.WriteLine(缴税人姓名:t0, tax.Name); Console.WriteLine(缴税人月收入:t0:c2元, tax.Saraly); int level = CountLevel(tax); Console.WriteLine(应缴税级别:t + level + 级); double t = CountTax(tax, level); Console.WriteLine(应缴收税额:t0:c2元, t); public int CountLevel(Tax tax) int level = 0; if (tax.Saraly 2000) level = 1; if (tax.Saraly 4000) level = 2; if (tax.Saraly 6000) level = 3; if (tax.Saraly 8000) level = 4; if (tax.Saraly 10000) level = 5; return level; public double CountTax(Tax tax, int level) double t = 0; switch (level) case 0: t = 0; break; case 1: t = tax.Saraly * 0.05; break; case 2: t = tax.Saraly * 0.08; break; case 3: t = tax.Saraly * 0.10; break; case 4: t = tax.Saraly * 0.15; break; case 5: t = tax.Saraly * 0.20; break; return t; 第三次重构:方法按逻辑归类刚才强调过一个方法只应实现一个功能,同一个类中方法应该实现一系列逻辑相关的功能。Report类现在具有打印报表相关的方法和税务计算两种不同的方法,实际上Report类中只应该包含打印相关的方法,税务相关的方法应该转移到Tax类中。观察下面更改后的Tax与Report类的代码,会发现将方法转移到应该归属的类后,方法的参数也变得更加简单:代码演示:第三次重构Taxusing System;namespace Com. XinZhanedu.Verson3 public class Tax private string name; public string Name get return name; set name = value; private double saraly; public double Saraly get return saraly; set saraly = value; public Tax() public Tax(string name, double saraly):base() = name; this.saraly = saraly; public int CountLevel() int level = 0; if (this.Saraly 2000) level = 1; if (this.Saraly 4000) level = 2; if (this.Saraly 6000) level = 3; if (this.Saraly 8000) level = 4; if (this.Saraly 10000) level = 5; return level; public double CountTax() double t = 0; /直接调用自身的countLevel方法获取缴税级别 switch (CountLevel() case 0: t = 0; break; case 1: t = this.Saraly * 0.05; break; case 2: t = this.Saraly * 0.08; break; case 3: t = this.Saraly * 0.10; break; case 4: t = this.Saraly * 0.15; break; case 5: t = this.Saraly * 0.20; break; return t; 代码演示:第三次重构Reportusing System;namespace Com.XinZhanedu. Verson3 public class Report public void Print(Tax tax) PrintHeader(); PrintContent(tax); public void PrintHeader() /打印收据头 Console.WriteLine(个人所得税申报专用收据); Console.Write(申报日期: + DateTime.Now.ToString(yyyy-MM-dd); Console.WriteLine(t操作人:张三); Console.WriteLine(-); public void PrintContent(Tax tax) /打印收据正文 Console.WriteLine(缴税人姓名:t0, tax.Name); Console.WriteLine(缴税人月收入:t0:c2元, tax.Saraly); Console.WriteLine(应缴税级别:t0级, tax.CountLevel(); Console.WriteLine(应缴收税额:t0:c2元, tax.CountTax(); 第四次重构:替换switch前三次重构都没有减少代码的数量,重构的目标也并不是减少代码量,而是合理化代码结构。但本次重构可以简化Tax类CountTax方法中冗长的switch代码,简化switch的方法有很多,本例中我们使用数组代替switch语句:代码演示:第四次重构Taxusing System;namespace Com.XinZhanedu.Verson4 public class Tax private string name; public string Name get return name; set name = value; private double saraly; public double Saraly get return saraly; set saraly = value; public Tax() public Tax(string name, double saraly):base() = name; this.saraly = saraly; /存放税率 private double rates = new double0, 0.05, 0.08, 0.1, 0.15, 0.2; public int CountLevel() int level = 0; if (this.Saraly 2000) level = 1; if (this.Saraly 4000) level = 2; if (this.Saraly 6000) level = 3; if (this.Saraly 8000) level = 4; if (this.Saraly 10000) level = 5; return level; public double CountTax() return this.Saraly * ratesCountLevel(); 4. 跟随Martin Fowler的步伐通过上面的小例子我们可以感受到重构的基本特点,下面在Martin Fowler的带领下更深入的了解重构的各种手段吧(在课件的“代码演示”目录中有重构一书的免费章节和代码)。类的设计原则1. 软件设计的终极目标有很多种评判软件设计优劣的指标,比如可复用性、安全性、稳定性、可操作型、可维护性等。根据软件应用范围的不同,开发者会侧重于不同的指标,有些软件追求体积的小巧,甚至有的开发者写出体积仅96K的三维动画程序,但对于商业应用程序而言,更多追求的则是可维护性。2002年曾经有人对美国的商业应用程序进行过统计,在软件开发上每投入1美元,就将花费2美元用于维护。也就是说软件维护的代价是开发的2倍,从企业收益角度考虑,较高的可维护性能够降低企业成本,也是商用软件设计最求的终极目标。如何实现软件的可维护性呢?一个常见的错误认识就是尽量提高软件的可重用性。虽然可重用性也是面向对象编程所追求的一个目标,但可重用性并不等于可维护性,如下例:A类与B类重用了R类的代码,A类为满足业务变化必须修改R类的代码。B类的业务并没有变化,但对R类所做的修改同样反映到了B类,反而使B类无法满足原有的业务需求,本例中系统拥有可重用性,却没有较高的可维护性。尽管不同业务中对可维护性的要求也不尽相同,想让软件系统能够应对各种业务变更也是几乎不可能完成的任务,但很多情况下,遵守类的设计原则能够很大程度上增强软件系统的可维护性。类的设计原则在面向对象设计中处于核心的地位,为面向对象的设计提供了理论上的指导,也为检测类之间设计是否合理提供了一套标准,后面我们要学习的设计模式也是建立在类的设计原则之上的。本章对重要的设计原则进行了简介。2. 开放封闭原则(OCP原则)开放封闭原则(Open Closed Principle)由Bertrand Meyer博士提出,是所有类的设计原则中最重要、最基础的原则,其他的设计原则均从OCP原则演化而来。Bertrand博士在著作面向对象软件结构中对OCP原则描述如下:software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.软件实体(类,模块,方法等)应该对扩展开放,对修改关闭。原文较难理解,一般来说,如果做到以下两点就能够符合OCP原则:1. 修改已有功能时不影响其他代码。2. 在不修改原有代码的基础上,能够为系统添加新的功能。符合OCP原则的软件系统具有两方面的优越性:1. 通过扩展已有的软件系统,可以提供新的功能和行为,以满足需求的变化,使软件具有一定的适应性和灵活性。2. 对已有的软件模块,特别是抽象层的代码不能再修改,使变化中的软件系统具有一定的稳定性和延续性。如何设计软件系统使其符合OCP原则呢?我们先来分析一个绘制各种形状的例子:代码演示:绘制各种形状的类 public class ShapeDemo public void PrintCircle() Console.WriteLine(); public void PrintSquare() Console.WriteLine(); ShapeDemo类并不符合OCP原则,无论是修改已有的画图功能还是扩展新的功能(比如增加画三角形),都需要改动已有的代码。下面是经过修改的代码:代码演示:修改后的代码 public abstract class AbstractShape public abstract void Print(); public class Circle : AbstractShape public override void Print() Console.WriteLine(); public class Square : AbstractShape public override void Print() Console.WriteLine(); 修改后的代码符合OCP原则,首先需要修改已有的画图功能时不会影响其他类,其次需要扩展新的画图功能也不会影响原有代码。通过例子可以看出,令软件系统符合OCP原则的关键是设计抽象化的结构。在C#语言中可以将抽象类或接口作为抽象层,定义所有具体类必须实现的方法。抽象层应该预见所有可能的扩展,这样就能够满足OCP原则中所要求的“对修改关闭”。同时根据C#的多态性原理,抽象层可以有不同的实现类,每个实现类都可以改变系统的行为(通过重写抽象层定义的方法),满足了OCP原则要求的“对扩展开放”。下面我们再通过一个生活中的实例来进一步说明这个问题,比如:阿聪家刚装完了房子,装完之后发现,没有为餐厅预留插座,这里,是修改原来的电路布线,在墙壁上重新布线,还是改用外部插座?如果修改,则需要对原来的线路很熟悉,并且很麻烦,用外部插座,很方便,当然可能不好看通过这个生活案例,我们知道了,为什么有的时候增加代码远要比修改代码的成本要低。.Net Framework中很多设计都符合OCP原则。比如IEnumerable接口作为抽象层定义了集合的外部特征,Hashtable、SortedList等作为实现类提供了不同的内在特性,如果需要增加一种集合,不需要修改已有的代码,只要增加集合类,实现IEnumerable接口即可。类似的例子还包括IO流、异常结构等设计。3. 依赖倒转原则(DIP原则)依赖倒转原则(Dependency Inversion Principle)是关于类之间耦合的原则。在面向对象系统中,类之间可以存在有三种不同的耦合关系:1. 零耦合:如果两个类没有耦合关系,就称之为零耦合。2. 抽象耦合:一个类引用另一个抽象类或接口,称之为抽象耦合。3. 具体耦合:一个类直接引用另一个具体类,称之为具体耦合。类之间的耦合度越高,越容易破坏OCP原则,一个类产生变化时往往会波及到相耦合的其他类。低耦合是理想的情况,但在实际开发中很难做到,我们只能退而求其次追求抽象耦合。如何实现抽象耦合?在DIP原则中有如下两段描述:High-level modules should not depend on low-level modules. Both should depend on abstractions.高等级模块不应依赖于低等级模块,而应依赖于抽象层。Program to an interface, not an implementation.针对接口(抽象层)编程,而不是实现类。针对接口编程指应当使用接口或抽象类进行变量类型声明、参数类型声明或方法返回类型声明,而不是使用具体实现类。比如定义集合时应使用IList list = new ArrayList ()这种形式而不是ArrayList v= new ArrayList (),前者提供了更高的灵活性。4. 接口分离原则(ISP原则)接口分离原则(Interface Segregation Principle)关注与接口的设计,要求使用多个专用接口,而不是单一的通用接口。通过对接口细分,可以符合ISP原则。符合ISP原则的接口设计有两大优势:1. 为抽象层提供更灵活的描述方式。2. 提供更细粒度的接口依赖。观察下面的例子,使用面向对象的方法描述Apple公司的iPod系列产品,第一版中我们让所有的iPod系列播放器都实现同一接口:图1 对iPod系列产品进行描述这种设计存在两个缺陷,首先并非所有的iPod都具有播放视频(shuffle不可以)和触控操作(只有iPod touch可以)功能,导致实现类只能对方法进行空实现;其次单一接口无法将各个产品区分开,如设计一个方法传入可播放视频的iPod对其进行操作,方法参数只能是“IPod接口”,所有的iPod都可以传入,扩大了操作的范围。下面是使用ISP原则重新划分接口后的示意图:图2 使用ISP原则后的设计分离后的接口精确描述了每一种播放器的特性,具体类可以有选择性的实现接口,解决了第一种设计中存在的两个缺陷,这是符合客观实际的。5. 最少知识原则(LKP原则)最少知识原则(Least Knowledge Principle)又称为迪米特法则(Law of Demeter),指两个类如果不需要直接联系,但一个类又需要调用另一个类的方法,可以通过第三者转发这个调用。最少知识原则常常用来封装比较复杂的对象,简化调用接口。比如A类提供了Exit(退出程序)方法,但A实例的获取比较麻烦,为简化操作,B类也提供了静态的Exit方法,使用者可以直接调用B中的方法从而避免直接操作A类。将最少知识原则推广到类的设计中,要求类应该尽可能的封装内部的工作细节,实现信息的隐藏,从而减少各个类之间的耦合,允许他们独立的被开发、优化。合理的封装能够促进软件的复用,也方便开发者对模块进行优化。6. 命名空间的设计原则之前所述均为类的设计原则,接下来介绍两个命名空间的设计原则: 通用闭命名空间原则(Common Closure Principle,CCP原则)通用闭命名空间原则强调了命名空间的内聚性,当对一个类的改变可能引起对另一个类的改变时,最好将这两个类放在同一个命名空间中。 无环依赖原则无环依赖原则指应避免命名空间之间的循环依赖,如命名空间A依赖于命名空间B,命名空间B依赖于命名空间C,命名空间C又依赖于命名空间A。当循环依赖产生时,会对修改带来非常大的麻烦,应该尽可能的避免,一般可以将被依赖的类提取出来放倒另一个命名空间中,取消循环依赖。设计模式 I1. 关于设计模式设计模式可以看作是在编程实践中开发者的一种经验性的总结,就像中国传统武术中的招式,它也是一种经验性的总结。比如在陈式太极拳中有一式叫“铁牛耕地”,是这样用的:A在攻击B的过程中,如果A有机会闪到B的后面,就可以从后面勾住对方的脖子,然后快速用力向后拖,目标是要用B的后脑勺去“耕地”(相当恐怖啊),这样最终达到一招制敌的效果。图1 铁牛耕地这里面的招式的应用是有条件的:1. A有机会闪到B的后面。2. A与B的身高应该差不多。符合这样的条件或不符合这样的条件而主动创造出了这样的条件,使用这一式要达到一招制敌是很容易的,我们看到这些经验都是我们的先辈们在古战场上总结出来的非常好的经验。每个设计模式,可以说是处理某一方面或某几方面问题的经验。为了便于标识这些设计模式,每个设计模式都有自己的名称,同时也往往都有自已的应用条件或者说应用环境。一般认为,GoF(Gang of Four,指Gamma, Helm, Johnson Vlissides与 Addison-Wesley四位大师级的人物)于1995年出版的设计模式可复用面向对象软件的基础一书将设计模式提升到理论高度,推动了设计模式的流行与发展。此书中提出了23种可谓经典的设计模式(现在每隔一段时间都会有开发者总结出新的模式),学习和了解这些经典的设计模式的使用方式、具体的应用环境以及使用这些设计模式会为我们的程序带来哪些效果,对于我们的程序设计而言,可以大大的提升应用程序的可扩展性、可重用性、可维护性。需要注意的是,设计模式作为经验的累积和编程思想的总结,所谓的可复用性并不是简单的调用API。实际上每一种设计模式都没有固定的实现方法,每一种模式都有自身的适用范围,学习设计模式时,不求死记硬背,而是要理解模式提出的背景以及所解决的问题和解决问题所采用的策略,还可以运用上一章介绍的关于类的设计原则来判断该模式符合哪些原则、违背了哪些原则。只有充分理解了模式,才能灵活的应用模式,而不是为了要给程序增加“卖点”去生搬硬套,那样往往会适得其反,就像要击倒一个对手,不顾武术招式
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025年武清数学中考试题及答案
- 智算中心计算任务调度与管理方案
- 水体景观设计与水质管理方案
- 机电设备安装过程风险评估与控制方案
- 汽车八级考试题目及答案
- 产后恶露考试试题及答案
- 广告制作安装合同
- 广东省2024年普通高中学业水平合格性考试思想政治考试题目及答案
- 互联网医疗平台员工劳动合同及医疗数据保密协议
- 知识产权竞业禁止协议赔偿金计算与执行细则
- 锚喷工入场安全教育试卷(含答案)
- DeepSeek+AI智能体医疗健康领域应用方案
- 2025至2030年中国玄武岩行业市场行情动态及发展前景展望报告
- 运输承运商管理制度
- 光伏支架系统培训
- CJ/T 233-2006建筑小区排水用塑料检查井
- 安全二级培训试题及答案
- (高清版)DB36∕T 2070-2024 疼痛综合评估规范
- 婚后老公赌博协议书
- 常见精神科药物的副作用及其处理
- 《公务员法解读》课件
评论
0/150
提交评论