文章目录
本系列文章共分为六篇:
设计模式的分类与区别
创建型模式介绍及实例
结构型模式介绍及实例
行为型模式介绍及实例(上)
行为型模式介绍及实例(下)
设计模式,实质上是一套被反复使用的代码设计经验,它提供了在软件设计过程中重复性问题的解决方案。其目的是为了提高代码的可重用性、代码的可读性和代码的可靠性。
设计模式的本质是建立在对类的封装性、继承性和多态性以及类的关联关系和组合关系等充分理解的基础上,对面向对象设计原则的实际运用。
一、设计模式的分类
常规的分类方式是根据其作用来划分,总共有三类:创建型模式、结构型模式和行为型模式。
1.1 创建型模式
该模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象,它的主要特点是将对象的创建与使用分离。
该类别包括5种具体的模式:
模式 | 功能 |
---|---|
单例模式 | 某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例 |
工厂方法模式 | 定义一个用于创建产品的接口,由子类决定生产什么产品 |
抽象工厂模式 | 提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品 |
建造者模式 | 将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象 |
原型模式 | 将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例 |
1.2 结构型模式
该模式关注类和对象的组合,即如何将类或对象按某种布局组成更大的结构。
该类别包括7种具体的模式:
模式 | 功能 |
---|---|
适配器模式 | 将一个类的接口转换成另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作 |
桥接模式 | 将抽象部分与实现部分分离,使它们都可以独立的变化 |
组合模式 | 将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性 |
装饰模式 | 向一个现有的对象添加新的功能,同时又不改变其结构,即动态地给一个对象添加一些额外的职责 |
外观模式 | 隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口,使得这一子系统更加容易使用 |
享元模式 | 运用共享技术来有效地支持大量细粒度对象的复用 |
代理模式 | 为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性 |
1.3 行为型模式
该模式用于描述类或对象之间怎样通信、协作共同完成任务,以及怎样分配职责。
该类别包括11种具体的模式:
模式 | 功能 |
---|---|
访问者模式 | 在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问 |
模板模式 | 定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤 |
策略模式 | 定义了一系列算法,并将每个算法封装起来,使它们可以相互替换 |
状态模式 | 允许一个对象在其内部状态发生改变时改变其行为能力 |
观察者模式 | 多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为 |
备忘录模式 | 在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它 |
中介者模式 | 定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解 |
迭代器模式 | 提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示 |
解释器模式 | 提供如何定义语言的文法,以及对语言句子的解释方法,即解释器 |
命令模式 | 将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开 |
责任链模式 | 请求从链中的一个对象传到下一个对象,直到请求被处理为止 |
二、设计模式的六大原则
2.1 总原则(开闭原则)
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码。
可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
抽象的设计方式灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。同时软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需根据需求重新派生一个实现类来扩展功能即可。
2.2 里氏替换原则
该原则的定义是:任何基类可以出现的地方,子类一定可以出现。该原则是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用。
实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现。因此,里氏代换原则是对开闭原则的补充。
该原则简单来说,就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。其具体体现为:
1>子类必须实现父类的抽象方法,但不能重写父类的非抽象(已实现的)方法。
2>子类中可以增加自己特有的方法。
3>当子类覆盖或者实现父类的方法时,方法的前置条件(方法形参)要比父类输入参数更加宽松,否则会调用到父类的方法。
4>当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格,否则会调用到父类的方法。
如果程序违背了里氏替换原则,就需要取消原来的继承关系,重新设计它们之间的关系。
2.3 依赖倒置原则
该原则的定义是:程序要依赖于抽象接口,不要依赖于具体实现。其定义中包含了三层含义:
1>高层模块不应该依赖低层模块,两者都应该依赖其抽象。
2>抽象不应该依赖细节。
3>细节应该依赖抽象。
依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
该原则的核心思想是:要面向接口(接口或者抽象类)编程,不要面向实现(具体的实现类)编程。在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多(这里的抽象指的是接口或者抽象类,而细节是指具体的实现类)。
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,在实际开发中,该原则的具体体现为:
1>每个类尽量提供接口或抽象类,或者两者都具备。
2>变量的声明类型尽量是接口或者是抽象类。
3>任何类都不应该从具体类派生。
4>使用继承时尽量遵循里氏替换原则。
2.4 单一职责原则
该原则中的职责是指类变化的原因,该原则规定一个类应该有且仅有一个引起它变化的原因,否则类就应该被拆分。
如果一个类承担了太多的职责,至少存在以下两个缺点:
1>一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力。
2>当客户端需要使用该对象的某一个职责时,不得不同时实现其他职责,从而造成冗余代码或代码的浪费。
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点:
1>降低类的复杂度。
2>提高类的可读性。
3>提高系统的可维护性。
4>变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。
2.5 接口隔离原则
该原则的定义:一个类对另一个类的依赖应该建立在最小的接口上。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的,区别如下:
1>单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
2>单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
在具体应用接口隔离原则时,需要从以下几个方面考虑:
1>接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
2>为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
3>了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准也不同。
4>提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
2.6 迪米特法则
该原则的定义:只与你的直接朋友(前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等)交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。该原则的目的是降低类之间的耦合度,提高模块的相对独立性。
从迪米特法则的定义可知,它强调以下两点:
1>从依赖者的角度来说,只依赖应该依赖的对象。
2>从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下 6 点:
1>在类的划分上,应该创建弱耦合的类。
2>在类的结构设计上,尽量降低类成员的访问权限。
3>在类的设计上,优先考虑将一个类设置成不变类。
4>在对其他类的引用上,将引用其他对象的次数降到最低。
5>不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
6>谨慎使用序列化功能。
2.7 合成复用原则
该原则要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现(如果要使用继承关系,则必须严格遵循里氏替换原则)。
采用组合或聚合的方式复用类时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
1>维持了类的封装性。
2>新旧类之间的耦合度低。
3>复用的灵活性高。
三、设计模式的适用场景
这个章节,对23种设计模式的不同点做了一些简单总结,以便更直观地从全面把握,具体如下:
模式 | 适用场景 | 例子 |
---|---|---|
单例模式 | 只要求生成一个对象 需要频繁实例化,而创建的对象又频繁被销毁 |
线程池 Android应用中的全局Context |
工厂方法模式 | 父工厂类中只有创建产品的抽象接口,将产品对象的实际创建工作推迟到具体子工厂类当中 | 老师布置作业,要学生制作一个机甲玩具,小明做了奥特曼,小李做了变形金刚 |
抽象工厂模式 | 系统中有多个产品类,但每次只使用其中的某一类产品 | 依旧以老师布置作业为例,老师除了布置机甲作业外,还可能布置书法练习作业,小明写了楷书,小李写了行书 |
建造者模式 | 对象可以分模块初始化 | Android中的AlertDialog |
原型模式 | 不同对象之间相似度高 创建对象比较麻烦,但复制比较简单 |
clone |
适配器模式 | 不同模块之间接口不一致 | 插口转换器 |
桥接模式 | 不同类都有多个变化的维度,这些维度可以组合成不同的结果 | 如颜色(有黑色、灰色)与书包(有双肩包、挎包),可以组合成四种不同的包 |
组合模式 | 表示一个对象整体与部分的层次结构 | 文件夹与文件 |
装饰器模式 | 不影响原有模块功能,需要动态地添加、撤销一些附属功能 | 在原有模块中加入日志打印 |
外观模式 | 隐藏子系统的复杂操作,对外提供简单的接口 | 找代购者买商品,代购者会进行货物挑选、议价、买入等流程 |
享元模式 | 系统中存在大量相同或相似的对象,只保存一个就行 | 围棋中的黑白子 |
代理模式 | 控制访问权限 | 不同管理层可以看到公司不同程度的财务账目 |
模板模式 | 子类执行固定步骤下的不同具体步骤 | 优酷和爱奇艺都决定在6月推出悬疑剧集,爱奇艺推出了《十日游戏》,优酷推出了《失踪人口》 |
策略模式 | 不同算法互相替代使用 | 要进行排序时,可以使用快速排序,也可以使用插入排序 |
状态模式 | 状态在很大程度上决定了系统的运行 | 开关 |
观察者模式 | 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象 | 群发邮件 |
备忘录模式 | 需要保存与恢复数据的场景 | Ctrl + Z |
中介者模式 | 不同对象之间关系复杂,需要调整 | 买房者、卖房者与中介 |
迭代器模式 | 为遍历某种结构提供统一接口 | 遍历器Iterator |
解释器模式 | 语言的文法较为简单,格式固定 | XML、JSON解析 |
命令模式 | 请求调用者与请求接收者解耦 | 物理老师给物理课代表下了一个命令:找出办理物理成绩最好的人 |
责任链模式 | 有多个对象可以处理一个请求 | 不同级别的人可以批准不同时长的假期 |
访问者模式 | 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构 | 浏览不同的电商网站购买衣服 |
四、设计模式的区别
4.1 状态模式与策略模式
状态模式处理对象所处的状态,封装了依赖于状态的行为;策略模式处理对象如何执行特定的任务,它封装的是算法。
4.2 策略模式与模板方法模式
在模板方法中,算法是在编译时通过继承选择的。使用策略模式,算法在运行时通过组合选择。
4.3 代理模式与装饰模式
代理模式控制对象访问权限,装饰模式用于向对象添加职责。
4.4 工厂模式与创建者模式
创建者模式可以将零件对象组装成整体,而工厂模式直接给出完整的对象。
4.5 访问者模式与迭代器模式
访问者模式一般遍历复杂结构,如树结构或组合结构等,结构中每个结点可以同构也可以异构,前提是只要提供一个预定的统一访问接口即可。迭代器模式用于遍历元素类型一致的集合(多是线性结构,当然也可以是非线性结构),不用为每个元素定义统一的访问接口。
4.6 适配器模式与外观模式
适配器模式一般只包装一个对象,目标是为了改变接口来适应外部系统,但要完成的功能没变。外观模式 一般是将一个子系统进行包装,目的是简化接口。
4.7 装饰器模式与适配器模式
装饰器模式是为了给现有对象增加功能,一般接口不变或接口增加。适配器模式是为了改变其接口,功能保持不变。
4.8 工厂模式与原型模式
工厂模式是重新实例一个对象,原型模式是从已有的实例直接拷贝生成一个实例,从而省去了初始化的过程。
4.9 备忘录模式与命令模式
两种模式都可以提供回到先前某点的功能,但前者是针对对象状态,后者针对对象行为。
参考资料:开闭原则——面向对象设计原则
参考资料:那些相似的设计模式的区别