设计原则与设计模式概述
设计原则
简介
设计原则是设计模式的基础,在开发中要根据实际情况做好设计原则粒度甚至原则本身的抉择平衡和取舍,不必刻意追求完美。
设计原则 | 概述 | 目的 |
---|---|---|
开闭原则 | 对扩展开放,对修改关闭 | 降低维护带来的新风险 |
单一职责原则 | 一个类只干一件事,实现类要单一 | 便于理解,提高代码的可读性 |
里氏替换原则 | 不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义 | 防止继承泛滥 |
依赖倒置原则 | 高层不应该依赖低层,要面向接口编程 | 更利于代码结构的升级扩展 |
接口分离原则 | 一个接口只干一件事,接口要精简单一 | 功能解耦,高聚合、低耦合 |
迪米特法则 | 不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度 | 只和朋友交流,不和陌生人说话,减少代码臃肿 |
合成复用原则 | 尽量使用组合或者聚合关系实现代码复用,少使用继承 | 降低代码耦合 |
实际上,这些原则的目的都是相同的:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。
开闭原则(Open Close Principle)
一个软件实体应该对拓展放,对修改关闭。
软件实体应该在不修改原代码的情况下进行拓展。或者说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码。
在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或者一个独立的类、接口或方法。
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。此外,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
最常见的满足开闭原则的方式是继承和实现。开闭原则常常可以视为面向对象设计的目标。
单一职责原则(Single Responsibility Principle)
一个类只负责一个功能领域中的相应职责
或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
在软件系统中,一个类承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
类在这里根据代码粒度会有不同的定义,大到模块小到方法。根据业务需求变动或者其他原因常常会发生职责扩散,也就是原来的职责会被分化为粒度更细的多个职责,此时之前的类就无法再遵循单一原则。对于一个已经写好的类,将其分解成两个类是很花时间的,因此我们可以退而求其次,不追求类的单一职责二追求方法的单一职责,也就是不改动原来的方法而是新建一个新方法来完成新职责。(当然,更好的是设计时就抽离出一个抽象父类,新增职责时就新增实现的子类)。
里氏替换原则(Liskov Substitution Principle)
所有引用父类的地方必须能透明地使用其子类的对象。
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。子类可以扩展父类的功能,但不能改变父类原有的功能。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
里氏替换原则可以视为开闭原则的补充,实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
具体运用里氏替换原则时:
- 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
- 尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。
依赖倒置原则(Dependency Inversion Principle)
抽象不应该依赖于细节,细节应当依赖于抽象。
换言之,要针对接口编程,而不是针对实现编程。
依赖倒置原则是面向对象设计的主要实现机制之一,是系统抽象化的具体实现。
-
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
-
抽象不应该依赖于具体,具体应该依赖于抽象。
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。(里氏替换原则)
在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
为了实现依赖倒置原则:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
开闭原则是目标,里氏替换是基础,依赖倒置是手段
接口分离原则(Interface Segregation Principle)
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。
迪米特法则(Law of Demeter)
一个软件实体应当尽可能少地与其他实体发生相互作用
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
在迪米特法则中,对于一个对象,其可通信的“朋友“包括以下几类:
- 对象本身;
- 以参数形式传入到当前对象方法中的对象;
- 当前对象的成员对象;
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
- 当前对象所创建的对象。
在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与“陌生人”发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
在现在流行的三层软件架构中的体现则是尽量少的跨层调用相关对象。
合成复用原则(Composite Reuse Principle)
软件复用时,要尽量先使用组合或者聚合等关联关系来实现。
如果必须要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但:破坏了类的封装性、耦合度过高、复用灵活性低,因此若非必要,尽量使用组合或聚合复用。
合成复用原则通常是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
设计模式
分类
根据目的来分
- 创建型模式:用于创建对象,主要特点是将对象的创建与使用分离。
- 结构型模式:用于将类或对象按照某种设计组成更大的结构。
- 行为型模式:用于分配任务来使类或对象之间协作完成其无法单独完成的任务。
根据作用范围来分
- 类模式:用于处理类和子类之间的关系。
- 对象模式:用于处理对象之间的关系,这些关系通过组合和聚合来实现(传参与成员对象)。
创建型模式 | 结构型模式 | 行为型模式 | |
---|---|---|---|
类模式 | 工厂方法模式 | 适配器模式(类) | 模板方法模式 解释器模式 |
对象模式 | 单例模式 原型模式 抽象工厂模式 建造者模式 | 代理模式 适配器模式(对象) 桥接模式 装饰模式 外观模式 享元模式 组合模式 | 策略模式 命令模式 责任链模式 状态模式 观察者模式 中介者模式 迭代器模式 访问者模式 备忘录模式 |
功能简介
设计模式 | 功能简介 |
---|---|
单例模式 | 某个类只能生成一个单例,该类提供了一个全局访问点供外部获取该实例,其拓展是优先多例模式。 |
原型模式 | 将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。 |
工厂方法模式 | 定义一个用于创建产品的接口,由子类决定生产什么产品。 |
抽象工厂模式 | 提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。 |
建造者模式 | 将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。 |
代理模式 | 为某对象提供一种代理以控制对该对象的访问。即客户端通过代理简洁的访问该对象,从而限制、增加或修改该对象的一些特性。 |
适配器模式 | 将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作 |
桥接模式 | 将抽象与实现分离,使它们可以独立变化。使用组合来代替继承关系,从而降低抽象和实现的耦合度。 |
装饰模式 | 动态的给对象增加一些职责,即增加其额外的功能。 |
外观模式 | 为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。 |
享元模式 | 运用共享技术来有效的支持大量细度对象的复用。 |
组合模式 | 将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。 |
模板方法模式 | 定义一个操作中的算法骨架,而将对算法的一些步骤延迟到子类中,使得子类在可以不改变该算法结构的情况下重定义该算法的特定步骤。 |
策略模式 | 定义了一系列算法,并将每个算法封装起来,使它们可以互相替换,而且算法的改变不会影响使用算法的客户 |
命令模式 | 将一个请求封装成一个对象,使发出请求的责任和执行请求的责任分隔开。 |
责任链模式 | 把请求从链中的一个对象传给下一个对象,直到请求被响应为止。通过这种方法去除对象间的耦合。 |
状态模式 | 允许一个对象在其内部状态发生改变时改变其行为能力。 |
观察者模式 | 多个对象之间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。 |
中介者模式 | 定义一个中介对象来简化对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必互相了解。 |
迭代器模式 | 提供一个方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。 |
访问者模式 | 在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。 |
备忘录模式 | 在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。 |
解释器模式 | 提供如何定义语言的文法,以及对语言句子的解释方法。 |
设计模式并不是孤立存在的,一个系统中往往会同时使用多个设计模式。