




已阅读5页,还剩17页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
面向对象设计六大原则面向对象设计的原则是面向对象思想的提炼,它比面向对象思想的核心要素更具可操作性,但与设计模式相比,却又更加的抽象,是设计精神要义的抽象概括。形象地将,面向对象思想像法理的精神,设计原则则相对于基本宪法,而设计模式就好比各式各样的具体法律条文了。面向对象设计原则有6个:开放封闭原则,单一职责原则,依赖倒置原则,Liskov替换原则,迪米特法则和接口隔离原则或合成/聚合复用原则(不同资料略有不同,这里对7个都做了整理)。1单一职责原则(Single Responsibility Principle SRP) There should never be more than one reason for a class to change. 什么意思呢? 所谓单一职责原则就是一个类只负责一个职责,只有一个引起变化的原因。 如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化会削弱或抑制这个类完成其他职责的能力,这个耦合会导致脆弱的设计。软件设计真正要做的许多内容,就是发现职责并把这些职责相互分离;如果能够想到多于一个动机去改变一个类,那么这个类就具有多于一个职责,就应该考虑类的分离。以调制解调器为例如下图:从上述类图里面我们发现有四个方法Dial(拨通电话),Hangup(挂电话),Receive(收到信息),Send(发送信息),经过分析不难判断出,实际上Dial(拨通电话)和Hangup(挂电话)是属于连接的范畴,而Receive(收到信息)和Send(发送信息)是属于数据传送的范畴。这里类包括两个职责,显然违反了SRP。这样做有潜在的隐患,如果要改变连接的方式,势必要修改Modem,而修改Modem类的结果导致凡事依赖Modem类可能都需要修改,这样就需要重新编译和部署,不管数据传输这部分是否需要修改。因此要重构Modem类,从中抽象出两个接口,一个专门负责连接,另一个专门负责数据传送。依赖Modem类的元素要做相应的细化,根据职责的不同分别依赖不同的接口。如下图:这样以来,无论单独修改连接部分还是单独修改数据传送部分,都彼此互不影响。总结单一职责优点:降低类的复杂性,提高可维护性提高可读性。降低需求变化带来的风险。需求变化是不可避免的,如果单一职责做的好,一个接口修改只对相应的实现类有影响,对其它的接口无影响,这对系统的扩展性和维护性都有很大的帮助。2里氏替换原则(Liskov Substitution Principle LSP)里氏替换原则是面向对象设计的基本原则之一。任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当子类可以替换基类,软件单位的功能不受影响时,基类才能真正的被复用,而子类也可以在基类的基础上增加新的行为。Liskov提出了关于继承的原则:Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.-继承必须确保超类中所拥有的性质在子类中仍然成立。2002年,软件工程大师Robert C. Martin出版了一本Agile Software DevelopmentPrinciples Patterns and Practices,在文中他把里氏代换原则最终简化为一句话:“Subtypes must be substitutable for their base types”也就是说子类必须能够替换成他们的基类。里氏替换原则讲的是基类和子类的关系,只有这种关系存在的时候里氏替换原则才能成立。里氏替换原则是实现开放封闭原则的具体规范。这是因为:实现开放封闭原则的关键是抽象,而继承关系又是抽象的一种具体实现。我们大家都打过CS的游戏,用枪射击杀人,如下类图:枪的主要职责是射击,如何射击在各个具体的子类中定义。注意在类中调用其他类时务必调用父类或接口,如果不能掉话父类或接口,说明类的射击已经违反了LSP原则。如果我们有一个玩具手 枪,该如何定义呢?我们先在类图2-1上增加一个类ToyGun,然后继承于AbstractGun类,修改后的类图如下:玩具枪是不能用来射击的,杀不死人的,这个不应该写shoot方法,在这种情况下业务的调用类就会出现问题。为了解决这个问题,ToyGun可以脱离继承,建立一个独立的父类,为了做到代码可以服用,可以与AbstractGun建立关联委托关系,如下图:因此,如果子类不能完整地实现父类的方法,那么建议断开父子继承关系,采用依赖,聚合,组合等关系代替继承。子类可以有自己的属性或方法。覆盖或实现父类的方法时输入的参数可以放大。覆盖或实现父类的方法时输出结果可以被缩小。这是什么意思呢,父类的方法返回值是一个类型T,子类相同的方法(覆写)的返回值为类型S,那么根据里氏替换原则就要求S必须小于等于T,也就是说要么S和T是同一个类型,要么S是T的子类型。采用里氏替换原则的目的就是增加程序的健壮性,需求变更时也可以保持良好的兼容性和稳定性,即使增加子类,原有的子类可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同业务逻辑。3依赖倒置原则(Dependence Inversion Principle DIP ) 所谓依赖倒置原则就是要依赖于抽象,不要依赖于具体。简单的说就是对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变化时,上层也要跟着变化,这就会导致模块的复用性降低而且大大提高了开发的成本。面向对象的开发很好的解决了这个问题,一般的情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变化,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序域实现细节的耦合度。比如一个合资汽车公司现在要求开发一个自动驾驶系统,只要汽车上安装上这个系统,就可以实现无人驾驶,该系统可以在福特车系列和本田车系列上使用。面向过程的结构图:实现代码如下:public class HondaCar public void Run() Console.WriteLine(本田车启动了!); public void Turn() Console.WriteLine(本田车拐弯了!); public void Stop() Console.WriteLine(本田车停止了!); public class FordCar public void Run() Console.WriteLine(福特车启动了!); public void Turn() Console.WriteLine(福特车拐弯了!); public void Stop() Console.WriteLine(福特车停止了!); public class AutoSystem public enum CarType Ford,Fonda private HondaCar hondcar=new HondaCar(); private FordCar fordcar=new FordCar(); private CarType type; public AutoSystem(CarType carType) this.type = carType; public void RunCar() if (this.type = CarType.Fonda) hondcar.Run(); else if (this.type = CarType.Ford) fordcar.Run(); public void StopCar() if (this.type = CarType.Fonda) hondcar.Stop(); else if (this.type = CarType.Ford) fordcar.Stop(); public void TurnCar() if (this.type = CarType.Fonda) hondcar.Turn(); else if (this.type = CarType.Ford) fordcar.Turn(); 显然这个实现代码也可满足现在的需求。但是如何现在公司业务规模扩大了,该自动驾驶系统还要把吉普车也兼容了。这些就需要修改AutoSystem类如下:public class AutoSystem public enum CarType Ford,Fonda,Jeep private HondaCar hondcar=new HondaCar(); private FordCar fordcar=new FordCar(); private Jeep jeep = new Jeep(); private CarType type; public AutoSystem(CarType carType) this.type = carType; public void RunCar() if (this.type = CarType.Fonda) hondcar.Run(); else if (this.type = CarType.Ford) fordcar.Run(); else if (this.type = CarType.Jeep) jeep.Run(); public void StopCar() if (this.type = CarType.Fonda) hondcar.Stop(); else if (this.type = CarType.Ford) fordcar.Stop(); else if (this.type = CarType.Jeep) jeep.Stop(); public void TurnCar() if (this.type = CarType.Fonda) hondcar.Turn(); else if (this.type = CarType.Ford) fordcar.Turn(); else if (this.type = CarType.Jeep) jeep.Turn(); 通过代码分析得知,上述代码也确实满足了需求,但是软件是不断变化的,软件的需求也是变化的,如果将来业务又扩大了,该自动驾驶系统还有能实现通用、三菱、大众汽车,这样我们不得不又要修改AutoSystem类了。这样会导致系统越来越臃肿,越来越大,而且依赖越来越多低层模块,只有低层模块变动,AutoSystem类就不得不跟着变动,导致系统设计变得非常脆弱和僵硬。导致上面所述问题一个原因是,含有高层策略的模块,如AutoSystem模块,依赖于它所控制的低层的具体细节的模块(如FordCar和HondaCar)。如果能使AutoSystem模块独立于它所控制的具体细节,而是依赖抽象,那么我们就可以服用它了。这就是面向对象中的“依赖倒置”机制。如下类图:实现代码如下:public interface ICar void Run(); void Stop(); void Turn(); public class HondaCar:ICar public void Run() Console.WriteLine(本田车启动了!); public void Turn() Console.WriteLine(本田车拐弯了!); public void Stop() Console.WriteLine(本田车停止了!); public class FordCar :ICar public void Run() Console.WriteLine(福特车启动了!); public void Turn() Console.WriteLine(福特车拐弯了!); public void Stop() Console.WriteLine(福特车停止了!); public class Jeep:ICar public void Run() Console.WriteLine(福特车启动了!); public void Turn() Console.WriteLine(福特车拐弯了!); public void Stop() Console.WriteLine(福特车停止了!); public class AutoSystem private ICar car; public AutoSystem(ICar car) this.car = car; public void RunCar() this.car.Run(); public void StopCar() this.car.Stop(); public void TurnCar() this.car.Turn(); 现在Autosystem系统依赖于ICar这个抽象,而与具体的实现细节HondaCar:和FordCar无关,所以实现细节的变化不会影响AutoSystem.对于实现细节只要实现ICar即可。即实现细节依赖于ICar抽象。综上所述:一个应用中的重要策略决定及业务 正是在这些高层的模块中。也正是这些模块包含这应用的特性。但是,当这些模块依赖于低层模块时,低层模块的修改比较将直接影响到他们,迫使它们也改变。这种情况是荒谬的。 应该是处于高层的模块去迫使那些低层的模块发生改变。处于高层的模块应优先于低层的模块。无论如何高层模块也不应该依赖于低层模块。而且我们想能够复用的是高层的模块,只有高层模块独立于低层模块时,复用才有可能。 总之,高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象。抽象不应该依赖于具体,具体应该依赖于抽象。4迪米特法则迪米特法则(Law of Demeter)又叫最少知识原则(Least Knowledge Principle LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。 对面向对象来说,一个软件实体应当尽可能的少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而其局限于那些与本单位密切相关的软件单位。迪米特法则的目的在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块相互独立,相互之间不存在依赖关系。应用迪米特法则有可能造成的一个后果就是,系统中存在的大量的中介类,这些类只所以存在完全是为了传递类之间的相互调用关系-这在一定程度上增加系统的复杂度。设计模式中的门面模式(Facade)和中介模式(Mediator)都是迪米特法则的应用的例子。狭义的迪米特法则的缺点:在系统里面造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商业逻辑无关。遵循类之间的迪米特法则会使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有之间的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。广义的迪米特法则在类的设计上的体现:优先考虑将一个类设置成不变类.尽量降低一个类的访问权限。尽量降低成员的访问权限。下面的代码在方法体内部依赖了其他类,这严重违反迪米特法则12345678910111213public class Teacher public void commond(GroupLeader groupLeader) List listGirls = new ArrayList(); for (int i = 0; i 20; i+) listGirls.add(new Girl(); groupLeader.countGirls(listGirls); 方法是类的一个行为,类竟然不知道自己的行为与其他类产生了依赖关系,这是不允许的。正确的做法是:1234567public class Teacher public void commond(GroupLeader groupLeader) groupLeader.countGirls(); 12345678910111213public class GroupLeader private List listGirls; public GroupLeader(List _listGirls) this.listGirls = _listGirls; public void countGirls() System.out.println(女生数量是: + listGirls.size(); 5开放封闭原则(Open-Closed Principle OCP)Software entities(classes,modules,functions etc) should open for extension ,but close for modification. 什么意思呢? 所谓开放封闭原则就是软件实体应该对扩展开发,而对修改封闭。开放封闭原则是所有面向对象原则的核心。软件设计本身所追求的目标就是封装变化,降低耦合,而开放封闭原则正是对这一目标的最直接体现。 开放封闭原则主要体现在两个方面: 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。 对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。为什么要用到开放封闭原则呢?软件需求总是变化的,世界上没有一个软件的是不变的,因此对软件设计人员来说,必须在不需要对原有系统进行修改的情况下,实现灵活的系统扩展。如何做到对扩展开放,对修改封闭呢?实现开放封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以对修改就是封闭的;而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。对于违反这一原则的类,必须通过重构来进行改善。常用于实现的设计模式主要有Template Method模式和Strategy 模式。而封装变化,是实现这一原则的重要手段,将经常变化的状态封装为一个类。以银行业务员为例没有实现OCP的设计:public class BankProcess /存款 public void Deposite() /取款 public void Withdraw() /转账 public void Transfer() public class BankStaff private BankProcess bankpro = new BankProcess(); public void BankHandle(Client client) switch (client.Type) /存款 case deposite: bankpro.Deposite(); break; /取款 case withdraw: bankpro.Withdraw(); break; /转账 case transfer: bankpro.Transfer(); break; 这种设计显然是存在问题的,目前设计中就只有存款,取款和转账三个功能,将来如果业务增加了,比如增加申购基金功能,理财功能等,就必须要修改BankProcess业务类。我们分析上述设计就不能发现把不能业务封装在一个类里面,违反单一职责原则,而有新的需求发生,必须修改现有代码则违反了开放封闭原则。从开放封闭的角度来分析,在银行系统中最可能扩展的就是业务功能的增加或变更。对业务流程应该作为扩展的部分来实现。当有新的功能时,不需要再对现有业务进行重新梳理,然后再对系统做大的修改。如何才能实现耦合度和灵活性兼得呢?那就是抽象,将业务功能抽象为接口,当业务员依赖于固定的抽象时,对修改就是封闭的,而通过继承和多态继承,从抽象体中扩展出新的实现,就是对扩展的开放。以下是符合OCP的设计:首先声明一个业务处理接口public interface IBankProcess void Process(); public class DepositProcess : IBankProcess public void Process() /办理存款业务 Console.WriteLine(Process Deposit); public class WithDrawProcess : IBankProcess public void Process() /办理取款业务 Console.WriteLine(Process WithDraw); public class TransferProcess : IBankProcess public void Process() /办理转账业务 Console.WriteLine(Process Transfer); public class BankStaff private IBankProcess bankpro = null; public void BankHandle(Client client) switch (client.Type) /存款 case Deposit: bankpro = new DepositUser(); break; /转账 case Transfer: bankpro = new TransferUser(); break; /取款 case WithDraw: bankpro = new WithDrawUser(); break; bankpro.Process(); 这样当业务变更时,只需要修改对应的业务实现类就可以,其他不相干的业务就不必修改。当业务增加,只需要增加业务的实现就可以了。设计建议:开放封闭原则,是最为重要的设计原则,Liskov替换原则和合成/聚合复用原则为开放封闭原则提供保证。可以通过Template Method模式和Strategy模式进行重构,实现对修改封闭,对扩展开放的设计思路。封装变化,是实现开放封闭原则的重要手段,对于经常发生变化的状态,一般将其封装为一个抽象,例如银行业务中IBankProcess接口。拒绝滥用抽象,只将经常变化的部分进行抽象。6接口隔离原则(ISP)接口隔离原则 认为:使用多个专门的接口比使用单一的总接口要好。因为接口如果能够保持粒度够小,就能保证它足够稳定,正如单一职责原则所标榜的那样。多个专门的接口就好比采用活字制版,可以随时拼版拆版,既利于修改,又利于文字的重用。而单一的总接口就是雕版印刷,显得笨重,实现殊为不易;一旦发现错字别字,就很难修改,往往需要整块雕版重新雕刻。例一:参考下图的设计,在这个设计里,取款、存款、转帐都使用一个通用界面接口,也就是说,每一个类都被强迫依赖了另两个类的接口方法,那么每个类有可能因为另外两个类的方法(跟自己无关)而被影响。拿取款来说,它根本不关心“存款操作”和“转帐操作”,可是它却要受到这两个方法的变化的影响。那么我们该如何解决这个问题呢?参考下图的设计,为每个类都单独设计专门的操作接口,使得它们只依赖于它们关系的方法,这样就不会互相影了!例二:使用多个专门的接口还能够体现对象的层次,因为我们可以通过接口的继承,实现对总接口的定义。例如,.NET框架中IList接口的定义。1. publicinterfaceIEnumerable 2. 3. IEnumeratorGetEnumerator(); 4. 5. publicinterfaceICollection:IEnumerable 6. 7. voidCopyTo(Arrayarray,intindex); 8. 9. /其余成员略 10. 11. publicinterfaceIList:ICollection,IEnumerable 12. 13. intAdd(objectvalue); 14. voidClear(); 15. boolContains(objectvalue); 16. intIndexOf(objectvalue); 17. voidInsert(intindex,objectvalue); 18. voidRemove(objectvalue); 19. voidRemoveAt(intindex); 20. 21. /其余成员略 22. 如果不采用这样的接口继承方式,而是定义一个总的接口包含上述成员,就无法实现IEnumerable接口、ICollection接口与IList接口成员之间的隔离。假如这个总接口名为IGeneralList,它抹平了IEnumerable接口、ICollection接口与IList接口之间的差别,包含了它们的所有方法。现在,如果我们需要定义一个Hashtable类。根据数据结构的特性,它将无法实现IGeneralList接口。因为Hashtable包含的Add()方法,需要提供键与值,而之前针对ArrayList的Add()方法,则只需要值即可。这意味着两者的接口存在差异。我们需要专门为Hashtable定义一个接口,例如IDictionary,但它却与IGeneralList接口不存在任何关系。正是因为一个总接口的引入,使得我们在可枚举与集合层面上丢失了共同的抽象意义。虽然Hashtable与ArrayList都是可枚举的,也都具备集合特征,它们却不可互换。如果遵循接口隔离原则,将各自的集合操作功能分解为不同的接口,那么站在ICollection以及IEnumerable的抽象层面上,可以认为ArrayList和Hashtable是相同的对象。在这一抽象层面上,二者是可替换的,如图2-9所示。这样的设计保证了一定程度的重用性与可扩展性。从某种程度来讲,接口隔离原则可以看做是接口层的单一职责
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 低压电器 课件 单元二 项目二 任务四 主令电器的使用
- 山西应用科技学院《金融信托与租赁》2023-2024学年第二学期期末试卷
- 西安航空职业技术学院《动物微生物与免疫学》2023-2024学年第二学期期末试卷
- 云南省师宗县2025年中考最后冲刺模拟(一)语文试题文试题含解析
- 浙江省台州市坦头中学2024-2025学年初三下学期中考模拟训练(五)英语试题试卷含答案
- 苏州卫生职业技术学院《医学图像处理B》2023-2024学年第二学期期末试卷
- 浙江省杭州市杭州二中2024-2025学年高三下学期返校数学试题含解析
- 上海市嘉定区封浜高中2024-2025学年高三入学检测试题生物试题含解析
- 宁波财经学院《教师职业理念与师德修养》2023-2024学年第二学期期末试卷
- “经营”英语竞争力讲座
- -电子公章-模板
- 语音信号处理第4讲剖析
- 锤击钢筋混凝土预制桩综合施工记录
- 初中化学人教九年级下册(2023年新编) 酸和碱黄琳娜微项目皮蛋制作中的化学教学设计
- Q∕SY 02098-2018 施工作业用野营房
- DB62∕T 3176-2019 建筑节能与结构一体化墙体保温系统应用技术规程
- 《博物馆馆藏文物管理库房工作日志》示例
- 施工现场防洪防汛应急预案45173
- 大猫英语分级阅读 八级1 Unusual Traditions课件
- 提高初中数学课堂教学有效性--教学论文
- 固定资产及累计折旧审计程序表
评论
0/150
提交评论