设计模式原理及应用场景
| 本文总阅读量次设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
1. 深入浅出设计模式原则:
目标就是:高内聚,低耦合。
在设计模式中,所谓的“实现一个接口”,并不一定标识写一个类,实现一个interface。实现一个接口泛指实现某个超类型(可以是类或接口)
1.1 单一职责原则(SRP,Single Responsibility Principle)
系统中的每一个对象都应该只有一个单独的原则。
1.2 开闭原则(OCP,Open For Extension, Close For Modification Principle)
类应该对扩展开放,对修改关闭。对类的改动通过增加代码来进行,而不是修改现有的代码。
1.3 依赖注入原则(DIP,Dependence Inversion Principle)
针对接口编程,不针对实现编程,程序利用多态针对超类型(supertype)编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上。依赖抽象,不要依赖具体类。
1.4 里式代换原则(LSP,Liskov Substitution Principle)
任何基类可以出现的地方,子类一定可以出现。反之则不行。
1.5 迪米特法则(LoD,Law of Demeter)
最少知道原则:只和朋友交谈。 就是只调用接口方法,没必要去了解接口内部的实现。
1.6 接口隔离原则(ISP,Interface Segregation Principle)
为交互对象之间的松耦合设计而努力。接口尽量小,但是要有限度。
1.7 多用组合,少用继承(CARP,Composite/Aggregate Reuse Principle)
组合使系统具有弹性,不仅可以将算法封装成类,还可以在运行时动态的改变行为。继承时,如果父类修改,则子类都要进行修改。所以在最初设计时,应尽量降低这种依赖关系。
2. 设计模式
2.1 创建型模式
2.1.1 单例模式(Singleton)
单例模式就是:确保一个类只有一个实例,并提供全局访问点。
单例模式的实现有多种方式,如下:
- 懒汉式,线程不安全
是否 Lazy 初始化:是
是否多线程安全:否
实现难度:易
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
代码实例:
1 | public class Singleton { |
接下来介绍的几种实现方式都支持多线程,但是在性能上有所差异。
- 懒汉式,线程安全
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:易
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
代码实例:
1 | public class Singleton { |
- 饿汉式
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
代码实例:
1 | public class Singleton { |
- 双检锁/双重校验锁(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
代码实例:
1 | public class Singleton { |
- 登记式/静态内部类
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloder 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
代码实例:
1 | public class Singleton { |
- 枚举
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
代码实例:
1 | public enum EnumTest { |
了解枚举
经验之谈:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
2.1.2 工厂模式(Factory)
在技术上,new一个对象总是没错的,但是如果你new的这个对象时不确定的,可变的,那就需要我们将变化的部分从不变的部分抽离出来。
工厂处理创建对象的细节。所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合。
关于工厂模式,我觉得最好还是看下《Head First设计模式》这本书比较好,其他人讲的真不咋的。
2.1.2.1 简单工厂方法模式(Factory Method)
简单工厂其实不是一个设计模式,而是比较像是一种编程习惯。屏蔽创建对象的细节。
2.1.2.2 工厂方法模式(Factory Method)
定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
2.1.2.3 抽象工厂模式(Abstract Factory)
抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。对象创建被实现在工厂接口所暴露出来的方法中。
2.1.3 原型模式(Prototype)
原型模式是创建模式的一种,顾名思义,就是创建一个和原来对象一模一样的新对象。
当创建给定类的实例的过程很昂贵或很复杂是,就是用原型模式。
Clone接口。
优点:
- 向客户隐藏制造新实例的复杂性。
- 提供让客户能够产生位置类型对象的选项。
- 在某些环境下,复制对象比创建新对象更有效。
缺点:
- 在一个复杂的类层次中,当系统必须从其中的许多类型创建新对象时,可以考虑原型。
- 对象有时复制相当复杂。
2.1.4 创建者模式(Builder)
创建者(生成器)模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示,而且客户端不知道对象的构建细节。
优点:
- 将一个复杂对象的创建过程封装起来。
- 允许对象通过多个步骤来创建,并且可以改变过程。
- 向客户隐藏产品内部的表现。
- 产品的实现可以被替换,因为客户只看到一个抽象接口。
缺点:
- 经常被用来创建组合结构。
- 与工厂模式相比,采用生成器模式创建对象的客户,需要具备更多的领域知识。
2.2 结构型模式
2.2.1 适配器模式(Adapter)
将一个类的接口,转换成客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。
2.2.2 门面(外观)模式 (Facade)
提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
优点
松散耦合
使得客户端和子系统之间解耦,让子系统内部的模块功能更容易扩展和维护;
简单易用
客户端根本不需要知道子系统内部的实现,或者根本不需要知道子系统内部的构成,它只需要跟Facade类交互即可。
更好的划分访问层次
有些方法是对系统外的,有些方法是系统内部相互交互的使用的。子系统把那些暴露给外部的功能集中到门面中,这样就可以实现客户端的使用,很好的隐藏了子系统内部的细节。
2.2.3 代理模式 (Proxy)
为另一个对象提供一个替身或者占位符以控制对这个对象的访问。
优点:
- 只有真正去调用的时候才会创建实例。有些情况下,程序不会真正的调用被调用对象的某个方法时,这种情况无需去创建被调用这对象的实例。在这种情况下,代理模式可以调程序的性能。宏观上减少了系统开销。
应用:hibernate延时加载
2.2.4 合成(组合)模式 (Composite)
允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象及对象组合。
应用实例:
算术表达式包括操作数、操作符和另一个操作数,其中,另一个操作符也可以是操作树、操作符和另一个操作数。
在 JAVA AWT 和 SWING 中,对于 Button 和 Checkbox 是树叶,Container 是树枝。
优点:
高层模块调用简单。
节点自由增加。
缺点:
- 在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
使用场景:部分、整体场景,如树形菜单,文件、文件夹的管理。
注意事项:定义时为具体类。
2.2.5 享元(蝇量)模式 (FlyWeight)
优点:
- 减少运行时对象实例的个数,节省内存。
- 将许多“虚拟”对象的状态集中管理。
缺点:
- 当一个类有许多的实例,而这些实例能被同一方法控制的时候,我们就可以使用蝇量模式。
- 蝇量模式的缺点在于,一旦你实现了它,那么单个的逻辑实例将无法拥有独立而不同的行为。
2.2.6 装饰模式 (Decorator)
动态的将责任附加到对象上,若要扩展功能,装饰者提供了比集成更具有弹性的替代方案。
2.2.7 桥接模式 (Bridge)
抽象变化,封装各自变化。
- 将实现予以解耦,让它和界面之间不再永久绑定。
- 抽象和实现可以独立扩展。
- 对于具体的抽象类所做的改变,不会影响到客户。
缺点:
- 适合使用在需要跨越多个平台的图形和窗口系统上。
- 当需要用不同的方式改变接口和实现时,你会发现桥接模式很好用。
2.3 行为模式
2.3.1 策略模式(Stategy)
定义了算法簇,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。体现封装变化、多用组合少用继承、针对接口编程,不针对实现编程。
2.3.2 迭代器模式(Iterator)
提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
应用实例:JAVA 中的 iterator。
优点:
它支持以不同的方式遍历一个聚合对象。
迭代器简化了聚合类。
在同一个聚合上可以有多个遍历。
在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:
- 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
使用场景:
访问一个聚合对象的内容而无须暴露它的内部表示。
需要为聚合对象提供多种遍历方式。
为遍历不同的聚合结构提供一个统一的接口。
注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。
2.3.3 模板方法模式(Template Method)
在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使用子类可以在不改变算法结构的情况下,重新定义算法中的步骤。
优点
封装不变部分,扩展可变部分。把认为不变部分的算法封装到父类中实现,而可变部分的则可以通过继承来继续扩展。
提取公共部分代码,便于维护。
行为由父类控制,子类实现。
缺点
- 按照设计习惯,抽象类负责声明最抽象、最一般的事物属性和方法,实现类负责完成具体的事务属性和方法,但是模板方式正好相反,子类执行的结果影响了父类的结果,会增加代码阅读的难度。
模板方法模式是通过父类建立框架,子类在重写了父类部分方法之后,在调用从父类继承的方法,产生不同的效果,通过修改子类,影响父类行为的结果,模板方法在一些开源框架中应用非常多,它提供了一个抽象类,然后开源框架写了一堆子类,如果需要扩展功能,可以继承此抽象类,然后覆写protected基本方法,然后在调用一个类似TemplateMethod()的模板方法,完成扩展开发。
2.3.4 中介者模式(Mediator)
使用中介者模式来集中相关对象之间复杂的沟通和控制方式。
优点:
- 通过将对象彼此解耦,可以增加对象的复用。
- 通过将控制逻辑集中,可以简化系统的维护。
- 可以让对象之间所传递的消息变得简单而且大幅减少。
缺点:
中介者常常被用来协调相关的GUI组件。
中介者模式的缺点是,如果设计不当,中介者对象本身会变得过于复杂。
2.3.5 访问者模式(Visitor)
当你想要为一个对象的组合增加新的能力,且封装并不重要时,就使用访问者模式。
优点:
- 允许你对组合结构加入新的操作,而无需改变结构本身。
- 想要加入新的操作,相对容易。
- 访问者所进行的操作,其代码是集中在一起的。
缺点:
- 当采用访问者模式的时候,就会打破组合类的封装。
- 因为游走的功能牵涉其中,所以对组合结构的改变就更加困难。
2.3.6 职责链模式(Chain of Responsibility)
当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链。
优点:
- 将请求的发送者和接收者解耦。
- 可以简化你的对象,因为它不需要知道链的结构。
- 通过改变链内的成员或调动它们的次序,允许你动态的新增或者删除责任。
缺点:
- 经常被使用的窗口系统中,处理鼠标和键盘之类的事件。
- 并不保证请求一定会被执行;如果没有任何对象处理它的话,他可能回落到链尾之外。
- 可能不容易观察运行时的特征,有碍于除错。
2.3.7 状态模式(State)
允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
State模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某一个State子类中, 所以通过定义新的子类可以很容易的增加新的状态和转换。另一个方法是使用数据值定义内部状态并且让 Context操作来显式地检查这些数据。但这样将会使整个Context的实现中遍布看起来很相似的条件if else语句或switch case语句。增加一个新的状态可能需要改变若干个操作, 这就使得维护变得复杂了。State模式避免了这个问题, 但可能会引入另一个问题, 因为该模式将不同状态的行为分布在多个State子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些, 否则需要使用巨大的条件语句。正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大整块并且使得代码不够清晰,这又使得它们难以修改和扩展。 State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的 i f或s w i t c h语句中, 而是分布在State子类之间。将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。这将使代码结构化并使其意图更加清晰。
优点:
它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。
它使得状态转换显式化: 当一个对象仅以内部数据值来定义当前状态时 , 其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。而且, State对象可保证Context不会发生内部状态不一致的情况,因为从 Context的角度看,状态转换是原子的—只需重新绑定一个变量(即Context的State对象变量),而无需为多个变量赋值
State对象可被共享 如果State对象没有实例变量—即它们表示的状态完全以它们的类型来编码—那么各Context对象可以共享一个State对象。当状态以这种方式被共享时, 它们必然是没有内部状态, 只有行为的轻量级对象。
缺点:
状态模式的使用必然会增加系统类和对象的个数。
状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
2.3.8 解释器模式(Interpreter)
使用解释器模式为语言创建解释器。
优点:
- 将每一个语法规则表示成一个类,方便与实现语言。
- 因为语法有许多类表示,所以你可以轻易地改变或扩展语言。
- 通过在类结构中加入新的方法,可以在解释的同时增加新的行为,例如打印格式的美化或者进行复杂的程序验证。
用途和缺点:
- 当你需要实现一个简单的语言时,使用解释器。
- 当你有一个简单的语法,而且简单比效率更重要时,使用解释器。
- 可以处理脚本语言和编程语言。
- 当语法规则的数目太大时,这个模式可能会变得非常复杂。
2.3.9 观察者模式(Observer)
在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖他的对象都会收到通知并自动更新。体现封装变化、针对接口编程、多用组合,少用继承、交互对象之间的松耦合设计。
2.3.10 命令模式(Command)
将请求封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
优点
- 类间解耦:调用者角色与接收者角色之间没有任何依赖关系,调用者实现功能时只需调用Command 抽象类的execute方法就可以,不需要了解到底是哪个接收者执行。
- 可扩展性:Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client不产生严 重的代码耦合。
- 命令模式结合其他模式会更优秀:命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,则可以减少 Command子类的膨胀问题。
缺点
- 命令模式也是有缺点的,请看Command的子类:如果有N个命令,问题就出来 了,Command的子类就可不是几个,而是N个,这个类膨胀得非常大,这个就需要读者在项 目中慎重考虑使用。
2.3.11 备忘录模式(Memento)
备忘录有两个目标:
- 存储系统关键对象的重要状态。
- 维护关键对象的封装。
优点:
- 将被存储的状态放在外面,不要和关键对象混在一起,这可以帮助维护内聚。
- 保持关键对象的数据封装。
- 提供了容易实现的恢复能力。
缺点:
- 备忘录用于存储状态。
- 使用备忘录的缺点:存储和回复状态的过程可能相当耗时。
- 在Java系统中,其实可以考虑使用序列化机制存储系统的状态。
参考资料
- 本文链接: http://blog.programer.group/java/2018-01-10-pattern/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!