面向对象设计的五大原则
单一职责原则(SRP)
一个类应该仅有一个职责。开放封闭原则(OCP)
对扩展开放,对修改封闭。Liskov 替换原则(LSP)
子类必须能够替换其基类。这是面向接口编程的基础。
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
子类具有扩展父类的责任,而不是重写的责任。
在使用里氏代换原则时需要注意如下几个问题:
1.子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
2.我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
依赖倒置原则(DIP)
依赖抽象(接口或抽象类),不要依赖具体。接口隔离原则(ISP)
多个和客户相关的接口要好于一个通用的接口。
设计模式的六大原则
开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。里氏代换原则(Liskov Substitution Principle)
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。依赖倒转原则(Dependence Inversion Principle)
这个原则是开闭原则的基础,具体内容:针对对接口编程,依赖于抽象而不依赖于具体。接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。迪米特法则,又称最少知道原则(Demeter Principle)
最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。合成复用原则(Composite Reuse Principle)
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
创建型模式
1. 简单工厂
意图
在运行时以灵活的方法创建对象,这些对象是构造方法不能单独提供的。
小结
- 当单独使用构造方法不适于对象的创建时,那么就使用一个返回对象的方法。
- 客户类和工厂类分开。
- Factory 模式是设计模式的委托的一个例子,因为每个工厂都是将创建责任委托给构造方法。
- 增加新的需求对象需修改工厂类。
- 简单工厂没有在 GoF 模式中,经常与反射技术一起用。
2. 工厂模式
是类的创建模式
意图
定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类。
参与者
- 抽象工厂(Creator)角色: 工厂方法模式的核心, 与应用程序无关。任何在模式中创建的对象的工厂类必须实现这个接口。
- 具体工厂(Concrete Creator)角色: 这个是实现抽象工厂接口的具体工厂类, 包含与应用程序密切相关的逻辑, 并且受到应用程序调用以创建产品对象。
- 抽象产品(Product)角色: 工厂方法模式所创建的对象的父类或接口。
- 具体产品(Concrete Product)角色: 这个角色实现了抽象产品角色所定义的接口,由具体工厂创建。
类图
小结
- 使用工厂接口。
- 具体的创建由子类完成,相当于简单工厂。
- 增加新的需求对象不需要修改工厂接口。相比简单工厂扩展性好。
3. 单例模式
意图
确保一个类只有一个确切的实例,在整个应用程序中都是可访问的。
小结
- 当一个类必须有且只有一个实例时,使构造方法私有化,并且使用共有的存取方法来实例化一个静态的,私有的变量。
- 尽量减少单态对象: 只有在存取方法第一次调用时才创建它。Lazy
- 需要单态对象的属性和方法时,不能都定义为静态 static。
- 单态能够扩展为一个类具有两个或更多实例。
- 只有一个单态对象与这个对象能在应用程序中的任何地方被访问是同等重要。
4. 抽象工厂模式
设计目标
提供一个创建相关族或者相互依赖对象的接口,而不指明它们的具体类。别名: Kit
意图
在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时由于需求的变化,往往存在更多系列对象的创建工作。
产品等级结构和产品族
工厂方法针对一个产品等级结构
抽象工厂针对多个产品等级结构
产品等级结构:产品的继承结构,如一个抽象类是手机,其子类有苹果手机、三星手机、华为手机,抽象的手机和具体品牌的手机构成一个产品结构。
产品族:在抽象工厂模式中,产品族是指同一个工厂生产的,位于不同产品结构中的一组产品,如苹果公司的苹果手机、苹果平板电脑、苹果笔记本。
抽象工厂参与者 Actor
- 抽象工厂(AbstractFactory):声明生成抽象产品的方法。
- 具体工厂(ConcreteFactory):实现生成抽象产品的方法,生成一个具体的产品。
- 抽象产品(AbstractProduct):为一种产品声明接口。
- 具体产品(Product):定义具体工厂生成具体产品的对象,实现抽象产品接口。
- 客户(Client):我们的应用程序,使用抽象产品和抽象工厂生成对象。
工厂方法是不是潜伏在抽象工厂里面?
抽象工厂:任务是定义一个负责创建一组产品的接口。
工厂方法:抽象工厂的方法经常以工厂方法的方式实现。
抽象工厂:使用对象组合。
工厂方法:使用类继承。
适用性
- 一个系统要独立于他的产品的创建、组合和表示时。
- 一个系统要有多个产品系列中的一个来配置时。
- 当你要强调一系列相关的产品对象的设计以便进行联合使用时。
- 当你提供一个产品类库,而只是想显示它们的接口而不是实现时。
优缺点
- 它分离了具体的类
- 它使得易于交换产品系列
- 它有利于产品的一致性
- 难以支持新种类的产品
5. 建造者模式
意图
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
复杂对象:由很多其他的对象(part)组合而成。
结构图
Builder模式中的角色:
- Builder
- ConcreteBuilder(具体工厂)
- Product(产品)
- Part(部件)
- Director(导向器)
效果
- 它使你可以改变一个产品的内部表示。
- 它将构造代码和表示代码分开。
- 它使你可对构造过程更精细的控制。
结构型模式
讨论的是类和对象的结构,它采用继承机制来组合接口或实现(类结构型模式),或者通过聚合或依赖对象,从而实现新的功能(对象结构型模式)。
1. 适配器模式
意图
将一个类的接口转化成客户希望的另外一个接口。Adapter模式使得原来由于接口不兼容而不能使其工作的那些类可以一起工作。
别名:包装器 Wrapper
结构图
类适配器
对象适配器
适用性
- 你想使用一个已经存在的类,而它的接口不符合你的需要。
- 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作。
- 你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口对象适配器可以适配他的父类接口。
两种适配器效果
类适配器
- 用一个具体的 Adapter 类对 Adaptee 和 Target 进行匹配。(结果是当我们想要匹配一个类以及所有的子类时,类 Adapter 将不能胜任工作)
- 使用 Adapter 可以重定义 Adaptee 的部分行为,因为 Adapter 是 Adaptee 的一个子类。Overriding
- 仅仅引入了一个对象,并不需要额外的指针间接得到 Adaptee。
对象适配器
- 允许一个Adapter与多个Adaptee同时工作。
- 使得重定义Adaptee的行为比较困难。
2. 桥接模式
意图
将抽象部分与实现部分分离,使它们都可以独立地变化。(抽象类)
abstract class XXX 与 interface XXX 的区别
- is a(extends) ----- like a(implements)
- 可以有具体行为 ----- 不允许
- 可以带属性 ----- final 常量
动机
当一个抽象可能有多个实现时,通常用继承来协调它们。抽象类定义
对该抽象的接口,而具体的子类则用不同的方式加以实现。但是此方
法有时不够灵活。继承机制将抽象部分与它的实现部分固定在一起,
使得难以对抽象部分和实现部分独立地进行修改、扩充和重用。
继承机制的不足
- 扩展 Window 抽象使之适用于不同的种类的窗口或新的系统平台很不方便
- 继承机制使得客户代码与平台相关
结构图
适用性
- 你不希望在抽象和他的实现部分之间有一个固定的绑定关系。
- 类的抽象以及它的实现都应该通过生成子类的方法加以扩充。通过 Bridge 模式使你可以对不同的抽象接口和实现进行组合,并分别对它们进行扩充。
- 对一个抽象的实现部分的修改应对客户不产生影响,客户端代码不必重新编译。
- 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
3. 组合模式
意图
将对象组合成树型表示“部分—整体”的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。(具有共同接口)
适用性
- 你想表示的对象的部分—整体层次结构。
- 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
结构图
效果
- 定义了包含基本对象和组合对象的类层次结构。客户代码中,任何用到基本对象的地方都可以使用组合对象。
- 简化客户代码,客户可以一致地使用组合结构和单个对象。
- 使得更容易增加新类型的组件,新定义的 CompositeComponent 或 LeafComponent 自动地与已有的结构和客户代码一起工作。
- 使你的设计变得更加一般化,也会产生一些问题,那就是很难限制组合中的组件。
缺点
- 控制Component类型不太容易
- 增加新的行为很困难//Decorator
4. 装饰者模式
意图
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类(继承)更为灵活。
装饰模式满足开放-封闭原则!
适用性
- 在不影响其他对象的情况下,给单个对象添加职责。
- 处理那些可以撤销的职责,简化原有的类。
- 当不能采用生成子类的方法进行扩充时。
结构图
效果
- 比静态继承更加灵活。
- 避免在层次结构高层的类有太多的特征。
- 有许多小对象(装饰类)。
- 一个被装饰了的组件和原来组件是有差别的。
5. 外观模式
意图
为子系统中的一组接口提供一个一致的界面,Façade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。解耦!
结构图
适用性
- 当你要为一个复杂子系统提供一个简单接口时。(往往是具体类)
- 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入 Façade 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
- 如果子系统之间是相互依赖的,你可以让它们仅通过Façade进行通讯,从而简化了它们之间的依赖关系。
- 子系统中任何类对其方法进行修改,不影响外观类的代码。
效果
- 它对客户屏蔽子系统组建,因而减少了客户处理的对象的数目并使得子系统使用起来更加方便。
- 它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组建往往是紧耦合的。松耦合关系使得子系统的组建变化不会影响到它的客户。// 高内聚,低耦合。
- 如果应用需要,它并不限制客户使用子系统。因此你可以在系统易用性和通用性之间加以选择。也可以创建不同需求的外观类。
6. 代理模式
意图
为其他对象提供一种代理以控制对这个对象的访问。
别名:Surrogate/Broker
结构图
种类
- 远程(Remote)代理:为一个位于不同地址空间的对象提供一个局域代表对象。本地
- 虚拟(Virtual)代理:根据需要创建一个资源消耗较大的对象,使得此对象只在需要时才会被真正创建。
7. 享元模式
行为型模式
- 行为模式涉及到算法和对象间职责的分配。
- 行为模式不仅描述类和对象的模式,还描述它们之间的通信模式。
- 使用继承机制在类间分派行为
- 使用对象聚合或依赖
- 行为型模式涉及怎样合理地设计对象之间的交互通信,以及怎样合理为对象分配职责,让设计富有弹性,易维护,易复用。
1. 迭代器模式
意图
提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
别名: 游标(Cursor)
适用性
- 访问一个聚合对象的内容而无需暴露它的内部表示。
- 支持聚合对象的多种遍历。
- 为遍历不同的聚合结构提供统一的接口。
结构图
效果(作用)
- 它支持以不同的方式遍历一个聚合,复杂的聚合可以多种方式遍历。例如,代码生成和语义检查要遍历语法分析树。中序或前序(看图示) next() previous()
- 迭代器简化了聚合的接口,有了迭代器的遍历接口,聚合本身就不需要类似的遍历接口。
- 在同一个聚合上可以有多个遍历,每个迭代器保持它自己的遍历状态。
2. 模板模式
意图
定义一个操作中的算法骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
Template 设计模式的目标是处理算法的多变性。
结构图
适用性
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中的公共地行为应被提取出来并集中到父类中避免代码重复。
实现要点
- TemplateMethod 模式是一种非常基础性的设计模式,在面向对象中有着大量的应用。为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。Spring 框架 IOC
- 除了可以灵活应对子步骤地变化外,“Don’t call me, Let me call you”的反向控制结构是TemplateMethod的典型应用。 Inverse of Control
- 在具体实现方面,被 TemplateMethod 调用的虚方法可以具体实现,也可以空实现,空实现的方法或默认的实现叫“hook operation”钩子操作。
- 尽量减少 primitive operation,需要重新定义的操作越多,子类程序越复杂。
3. 观察者模式
意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
别名:发布-订阅(Publish-Subscribe)
适用性
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变。
- 当一个对象必须通知其他对象,你不希望这些对象是紧密耦合的。
结构图
4. 责任链模式
意图
使多个对象都有机会处理请求,从而避免请求的发送者和接受收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
该模式构造一系列分别担当不同的职责的类的对象来共同完成一个任务,
这些类的对象之间像链条一样紧密相连,所以被称作职责链模式。
实现要点
- 对象链的组织:需要将某任务的所有职责执行对象以链Chain的形式加以组织。
- 消息或请求的传递:将消息或请求沿着对象链传递,以让处于对象链中的对象得到处理机会。
- 处于对象链中的对象的职责分配:不同的对象完成不同的职责。
- 任务的完成:处于对象链的末尾的对象结束任务并停止消息或请求的继续传递。
适用性
- 有多个对象可以处理一个请求,哪个对象处理该请求运行时刻确定。
- 你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可处理一个请求的对象集合应被动态指定。(改变链内的成员或它们的次序)
结构图
效果
- 降低耦合。该模式使得一个对象无需知道是其它哪一个对象处理其请求。对象仅需知道该请求会被处理。
- 增强了给对象指派职责 Responsibility 的灵活性。在运行时刻对该链进行动态的增加或修改处理一个请求的那些职责
- 职责的分担。每个类只需要处理自己该处理的工作(不该处理的传递给下一个对象完成),明确各类的责任范围,符合类的最小封装原则。SRP
- 请求不保证被接受。一个请求可能因该链没有被正确配置而得不到处理。
5. 中介模式
意图
用一个中介对象来封装一系列对象交互。中介者使各对象不需要相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
迪米特法则
迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解。
迪米特法则不希望类之间建立直接的联系。
迪米特法则应用:外观模式 facade 和中介者模式 mediator
迪米特法则中,对于一个对象,其朋友包括以下几类:
- 当前对象本身;
- 以参数形式传入到当前对象方法中的对象;
- 当前对象的成员对象;
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
- 当前对象所创建的对象。
结构图
和外观模式的区别
-
中介者模式解决的是多个对象之间的通信问题,减少类之间的关联。
外观模式解决的是子系统的接口复杂度问题。 -
中介者模式中对象可以向中介者请求。
外观模式中对象不会对外观有任何协作请求。
特点
- 减少子类的生成。Mediator 将原本分布于多个对象间的行为集中在一起。改变这些行为只需生成 Mediator 的子类即可。
- 将各 Colleague 解耦。Mediator 有利于各 Colleague 类间的松耦合。你可以独立的改变和复用各 Colleague 类和 Mediator 类。
- 简化了对象协议。用 Mediator 和各 Colleague 间的一对多的交互来替代多对多的交互。一对多的关系更易于理解、维护和扩展。
- 使控制集中化。中介者模式将交互的复杂性变为中介者的复杂性。中介者封装了协议,它可能比任何一个 Colleague 都复杂。
6. 状态模式
意图
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
为什么说 Thermometer 类的实例在应对需求变化时缺乏弹性?
因为 showMessage( ) 的行为依赖于 temperature 大小
重新设计Thermometer类:将对象的状态从当前的对象中分离出去,即将一个对象的状态封装在另外一个类中。
结构图
优缺点
优点:
- 使用类封装对象的一种状态,容易扩增新的状态。
- 在 Context 中不必出现大量的条件判断语句。
- 用户很方便地切换 Context 实例的状态。状态切换
- Context 的各个对象可以共享一个状态对象。
缺点:
- 较多的 ConcreteState 子类。
适用性
- 对象的行为依赖于它的状态,并且它必须在运行时根据状态改变它的行为。
- 需要编写大量的条件语句来决定一个操作的行为,而且这些条件恰好表示对象的一种状态。