【设计模式】面向对象设计七大原则
【设计模式】设计模式概念和分类
【设计模式】创建型设计模式:单例模式
面向对象设计七大原则
- 单一职责原则
- 开放封闭原则
- 里氏代换原则
- 依赖倒置原则
- 接口隔离原则
- 合成复用原则
- 迪米特原则
- 总结
单一职责原则
Single-Responsibilitiy Principle(SRP):最简单的设计原则,用于控制类的粒度大小。一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
开放封闭原则
Open-Close Principle(OCP):一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。目的就是保证程序的扩展性好,易于维护和升级。开放封闭原则简称为开闭原则。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
开闭原则还可以通过一个更加具体的对可变性封装原则来描述,对可变性封装原则(EVP)要求找到系统的可变因素并将其封装起来。
里氏代换原则
Liskov Substitution Principle(LSP):子类可以扩展基类的功能,但是不能改变基类原有的功能。所有引用基类的地方必须能透明的使用其子类的对象。里氏替换原则克服继承的缺点。
子类可以实现基类的抽象方法,但不能覆盖基类的非抽象方法
子类中可以增加自己特有的方法
当子类的方法重载基类的方法时,方法的前置条件(即方法的形参)要比基类方法的输入参数更宽松。
当子类的方法实现基类的抽象方法时,方法的后置条件(即方法的返回值)要比基类更严格。
继承优点:代码共享,减少创建类的工作量,每个子类都拥有基类的方法和属性;提高代码的重用性;子类可以形似基类,但又异于基类;提高代码的可扩展性,实现基类的方法就可以“为所欲为”了;提高产品或项目的开放性。
继承缺点:继承是侵入性的。只要继承,就必须拥有基类的所有属性和方法;降低代码的灵活性。子类必须拥有基类的属性和方法;增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果;大片的代码需要重构。
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
尽量把基类设计成抽象类或接口,让子类继承基类或实现基类接口。增加一个新功能时,通过增加一个新的子类来实现。
依赖倒置原则
Dependence Inversion Principle(DIP):针对抽象编程,不要针对实现编程。是一个类与类之间的调用规则,依赖是指代码中的耦合。代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性
(1)每个类尽量提供接口或抽象类,或者两者都具备。
(2)变量的声明类型尽量是接口或者是抽象类。
(3)任何类都不应该从具体类派生。
(4)使用继承时尽量遵循里氏替换原则
依赖倒置原则的作用:降低类间的耦合性、提高系统的稳定性、减少并行开发引起的风险、提高代码的可读性和可维护性。
接口隔离原则
Interface Segregation Principle(ISP):恰当的划分角色和接口(分离接口)从而实现解耦。客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。
(1)将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
(2)接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
(3)如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
(4)使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
(5)能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
在具体应用接口隔离原则时,应该根据以下几个规则来衡量。
(1)接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
(2)为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
(3)了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
(4)提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
接口隔离原则和单一职责原则都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想。
但两者是不同的:
(1)单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
(2)单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
合成复用原则
Composite Reuse Principle(CRP):又称为组合/聚合复用原则。尽量使用对象组合,而不是继承来达到复用的目的。子类与其父类关系密切,父类的修改必然会对子类造成影响,而如果不小心对父类方法进行修改,就会造成子类错误的发生,这种依赖关系增加了类间的依赖关系,提高了耦合度。
在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。
继承复用:实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。(“白箱”复用 )
组合/聚合复用:耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。(“黑箱”复用 )
组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
迪米特原则
Law of Demeter(LoD):也叫最小知识原则。一个对象应该对其他对象有最少的了解,只和直接的朋友通信;类间解耦,弱耦合。简单地说,迪米特法则就是指一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。类间松耦合,类间耦合度越低,其复用性越强。
迪米特法则可分为狭义法则和广义法则:
狭义的迪米特法则:可以降低类之间的耦合,但是会在系统中增加大量的小方法并散落在系统的各个角落,它可以使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联,但是也会造成系统的不同模块之间的通信效率降低,使得系统的不同模块之间不容易协调。
广义的迪米特法则:指对对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制。信息的隐藏可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化、使用和修改,同时可以促进软件的复用,由于每一个模块都不依赖于其他模块而存在,因此每一个模块都可以独立地在其他的地方使用。一个系统的规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显。
在具体应用迪米特原则时,应该根据以下几个规则来衡量。
(1)在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
(2)在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
(3)在类的设计上,只要有可能,一个类型应当设计成不变类;
(4)在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
总结
七大原则是提高面向对象编程代码质量的必备原则,另外还是理解设计模式的必备前提。只有满足了这七大原则,才能设计出稳定的软件架构,但它们只是原则,有时还是需要学会灵活应变,千万不要生搬硬套,否则只会把简单问题复杂化。