设计模式是一套被反复使用的、多数人知晓的、经过分类编目的代码设计经验的总结,使用设计模式是为了可以重用代码,让代码更容易被他人理解并且提高代码的可靠性。
1 设计模式概述
GoF(Gang Of Four 四人组,指4位著名软件工程学者),在1994年归纳发表了23中在软件开发中使用频率较高的设计模式。
类型 | 数量 | 设计模式 | 职责 |
创建型 | 5 | 单例模式 | 描述如何创建对象。 |
工厂方法模式 | |||
抽象工厂模式 | |||
原型模式 | |||
建造者模式 | |||
结构型 | 7 | 适配器模式 | 描述如何实现类或对象的组合。 |
桥接模式 | |||
组合模式 | |||
装饰模式 | |||
外观模式 | |||
享元模式 | |||
代理模式 | |||
行为型 | 11 | 职责链模式 | 描述类或对象怎样交互以及怎样分配职责 |
命令模式 | |||
解释器模式 | |||
迭代器模式 | |||
中介者模式 | |||
备忘录模式 | |||
观察者模式 | |||
状态模式 | |||
策略模式 | |||
模板方法模式 | |||
访问者模式 |
表 GoF 23中设计模式
1.1 设计模式掌握点
1)设计模式的意图,解决的问题,使用场景;
2)如何解决问题,其结构图,关键代码;
3)至少两个应用实例,1个生活中,1个软件中;
4)优缺点,使用时注意事项;
2 UML
UML(Unified Modeling Language,统一建模语言)是最常用、使用最广泛的图形描述技术。
2.1 关联
是类与类之间最常用的一种关系,用于表示一类对象与另一类对象之间有联系。在UML中,关联关系通常又包含如下几种形式:
1) 双向关联
默认情况下,关联是双向的,用实线连接两个类,表示它们的双向关联。例如,用户创建订单,得到一个新订单,而订单则与这个用户相关联。
图 用户与订单双向关联
2) 单向关联
用带箭头的实线表示单向关联,其中箭头指向被关联的元素。例如,用户拥有手机。
图 用户单向关联手机
3)自关联
类的属性对象类型是该类的本身。例如,节点Node类包含属性:nextNode表示下一节点。
图 Node的自关联
4) 多重性关联
又称为重数性关联关系,表示两个关联对象在数量上的对于关系,比如用户与订单 1对多的关联关系。
图 用户与订单1对N的关联关系
5) 聚合关系
表示整体与部分的关系。在聚合关系中,成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。在UML中,用空心菱形及带箭头(也可以不带箭头)的直线表示。空心菱形指向整体,箭头指向成员对象。例如,图书馆和书籍是聚合关系。
图 图书馆与书籍的聚合关系
组合关系
表示整体与部分的关系,在组合关系中,整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在。在UML中,用实心菱形及带箭头(也可以不带箭头)的直线表示。实心菱形指向整体,箭头指向成员对象。例如,人与嘴巴是组合关系。
图 人和嘴巴的组合关系
2.2 依赖关系
是一种使用关系,特定事务的改变有可能会影响到使用该事务的其他事务。在大多数情况下,依赖关系体现在某个类的方法使用另一个类的对象作为参数。在UML中,依赖关系用带箭头的虚线表示,箭头指向被依赖的元素。
例如,人看书。People有个read方法,参数为Book类型,因此,People依赖Book。
图 人与书的依赖关系
3 设计原则
面向对象设计原则是为支持可维护性复用而诞生,是从许多设计方案中总结出的指导性原则。也是用于评价一个设计模式的的使用效果的重要指标之一。
最常用的有7种设计原则。
单一职责原则 | 一个类只负责一个功能领域中的相应职责。 |
开闭原则 | 软件实体应对扩展开放,而对修改关闭。 |
里氏代换原则 | 所有引用基类对象的地方能够透明地使用其子类的对象。 |
依赖倒转原则 | 抽象不应该依赖于细节,细节应该依赖于抽象。 |
接口隔离原则 | 使用多个专门的接口,而不使用单一的总接口。 |
合成复用原则 | 尽量使用对象组合,而不是继承来达到复用的目的。 |
迪米特法则 | 一个软件实体应当尽可能少地与其他实体发生相互左右。 |
表 7种常用的面向对象设计原则
3.1 单一职责原则
一个类只负责一个功能领域中的相应职责。或者说,就一个类而言,应该只有一个引起它变化的原因。
例如,创业初期,老板要身兼多职,既要去跑业务、也要管理财务,生意有起色的话,还要去招聘。
图 创业初期的公司
在付出了99%的努力后,生意没有起色,快要倒闭的时候,靠着1%的机遇,让公司存活了下去,并且开始做大做强。这个时候,Boss就不能身兼多职了,要放权,让员工各司其职,专业的人做专业的事。否则,公司是绝对做不大的。
图 做大做强的公司
3.2 开闭原则
是最重要的对象设计原则。一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
对于软件项目,会随着时间的推移,对应的需求也会发生变化,当面临新的需求时,应尽量保证系统的设计框架是稳定的。
图 历经千年的“镇海吼”
历经千年不倒的“镇海吼”,在经过专家们的多次摧残式的修复后,变得更加伤痕累累,加大了倒塌的可能性。
为了满足开闭原则,需要对系统进行抽象化设计,这是开闭原则的关键。
3.3 里氏代换原则
所有引用基类的地方必须能透明地使用其子类的对象。是实现开闭原则的重要方式之一。
在运用里氏代换原则时,应该将父类设计为抽象类或者接口,在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型。
例如,有一家汽车销售店,其出售奥迪及保时捷品牌的车。后续可能会销售其他品牌车。
图 汽车销售店UML图
上图设计氏不符合开闭原则的。当增加新的品牌时,需要新增一个方法。需要修改CarShop的代码。根据里氏代换原则,重新设计为:
图 优化后的汽车销售店UML图
3.4 依赖倒转原则
抽象不应该依赖于细节,细节应该依赖于抽象。要针对接口编程,而不是针对实现编程。
面向对象 | 以对象为单位,考虑它的属性及方法。 |
面向过程 | 以一个具体的流程(事务过程)为单位,考虑它的实现。 |
面前接口 | 是针对复用技术而言,与上面两个面向不是同一个问题。 |
表 面向接口与面向对象、面向过程的区别
例如,将大象放入冰箱,我们针对的是大象这个种类,考虑的是怎么把大象怎么放进冰箱。而面对接口编程,则考虑的是把物品(包括动物)放入冰箱。
图 面向接口的“将大象放入冰箱”问题
3.5 接口隔离原则
使用多个专门的接口,而不使用单一的总接口。即客户端不应该依赖那些它不需要的接口。
在使用接口时,要注意颗粒度问题,颗粒度太细的话,容易造成接口的泛滥,太粗则可能会不符合接口隔离原则。
3.6 合成复用原则
尽量使用对象组合,而不是继承来达到复用的目的。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性。因为继承会将基类的实现细节暴露给子类,如果基类发生改变,那么子类的实现也不得不发生改变,没有足够的灵活性。
例如,手机由屏幕、芯片及电池组成,苹果公司几乎每一年都会推出新的一款手机。
图 通过继承来实现手机生产
假如Foxconn的makeIPhone方法在添加个参数camera,那么其子类也同时需要修改。现在我们根据合成复用原则,做优化:
图 通过组合来实现手机生产
3.7 迪米特法则
一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则还要另一种定义形式:不要和“陌生人”说话,只与你的之间朋友通信等。“朋友”包括以下几类:
- 当前对象本身;
- 以参数形式传入到当前对象方法中的对象;
- 当前对象的成员对象,如果当前对象成员对象是一个集合,那么集合中的元素也是朋友;
- 当前对象所创建的对象。
迪米特法则要求在设计系统时,应该尽量减少对象之间的交互。如果两个对象之间不必彼此通信,那么这两个对象就不应当发生任何直接的相互作用;如果其中一个对象需要调用另一个对象的方法,可以通过第三者转发这个调用(引入一个合理的第三者来降低现有对象之间的耦合度)。
例如,每个服装公司都有自己的网站用来给客户下单。
图 客户在服装公司官网上下单
客户与服装公司的耦合度是很高的,受限于公司网站是否有效、付款规则等。假如某些服装公司下单规则改变时,Customer需要做相应的修改。
我们引入第三方交易平台来降低客户与服装公司的耦合度。
图 引入第三方平台后,客户之间在第三方平台下单