java常用设计模式概览

基本概念

  • 设计模式概念:
    一套被反复使用、多数人知晓、经过分类编目的优秀代码设计经验的总结。

  • 设计模式要素:
    模式名称、问题、举例、末态环境、推理、其他有关模式、已知的应用。

  • 设计模式的六大原则:
    开闭原则(Open Close Principle)
    里氏代换原则(Liskov Substitution Principle)
    依赖倒转原则(Dependence Inversion Principle)
    接口隔离原则(Interface Segregation Principle)
    迪米特法则(Demeter Principle)
    合成复用原则(Composite Reuse Principle)

  • 设计模式分类:
    创建型、结构型、行为型。


设计模式的六大原则

  • 单一职责原则:一个类应该只有一个职责。
    优点:降低类的复杂性;提高类的可读性;提高代码的可维护性和复用性;降低因变更引起的风险。

  • 里氏替换原则:
    优点:
    代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;提高代码的可重用性;提高代码的可扩展性;提高产品或项目的开放性。
    缺点:

    1. 继承是入侵式的。只要继承,就必须拥有父类所有属性和方法。
    2. 降低代码的灵活性。子类必须拥有父类的属性和方法,使子类收到限制。
    3. 增强了耦合性。当父类的常量、变量和方法修改时,必须考虑子类的修改,这种修改可能造成大片的代码需要重构。
  • 依赖倒置原则:高层模块不应该依赖低层模块,两者都依赖其抽象;抽象不依赖细节;细节应该依赖于抽象。
    在Java中的表现:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;接口或抽象类不依赖于是实现类;实现类依赖于接口或抽象类。

  • 接口隔离原则:

    1. 一个类对另外一个类的依赖性应当是建立在最小的接口上的
    2. 一个接口代表一个角色,不应当将不同的角色交给一个口。
    3. 不应该强迫客户使用它们的不同方法。
  • 迪米特法则:
    只与你直接的朋友们通信;不要跟“陌生人”说话;每一个软件单位对其他的单位都只有最少的了解,这些了解仅局限于那些与本单位密切相关的软件单位。
    对迪米特法则进行模式设计有两个:外观模式、中介者模式。

  • 开闭原则:一个软件实体应当对扩展开放,对修改关闭。
    重要性体现:提高复用性;提高维护性;提高灵活性;易于测试


设计模式分类

分类表:

创建型模式 结构型模式 行为型模式
简单工厂模式 适配器模式 不变模式
工厂方法模式 缺省适配模式 策略模式
抽象工厂模式 合成模式 模板方法模式
单例模式 装饰模式 观察者模式
多例模式 代理模式 迭代器模式
建造模式 享元模式 责任链模式
原始模型模式 门面模式 命令模式
桥梁模式 备忘录模式
状态模式
访问者模式
解释器模式
调停者模式

PS:

  • 创建型模式即用于创建对象
  • 结构型模式用来表示对象之间的关系及协作
  • 行为型模式即表示对象的行为

附关系图:


对应设计模式基本介绍

单例模式

单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。

优点:

  1. 在内存中只有一个实例,减少了内存的开支。
  2. 只生成一个实例,减少了系统的性能开销。
  3. 避免对资源的多重占用。
  4. 可以在系统设置全局的访问点,优化和共享资源访问。

缺点:

  1. 无法创建子类,扩展困难,若要扩展,除修改代码基本上没有第二种途径可以实现。
  2. 对测试不利。
  3. 与单一职责原则有冲突。

饿汉式单例类与懒汉式单例类之间的区别:

  1. 饿汉式单例类在被加载时实例化,而懒汉式单例类在第一次引用时实例化。
  2. 从资源利用效率上说,饿汉式单例类要差一点,但从速度和反应时间的角度来讲,饿汉式单例类则比懒汉式单例类稍好些。
  3. 饿汉式单例类可以在Java中实现,但不易在C++中实现。

单例模式的使用场景:

  1. 要求生成唯一的序列号环境。
  2. 在整个项目中需要一个共享访问点或共享数据。
  3. 创建一个对象需要消耗的资源过多。
  4. 需要定义大量的静态方法的环境。

使用单例模式的注意事项:

  1. 在使用任何EJB、RMI和JINI的分布式系统中,应当避免使用有状态的单例类。
  2. 同一个JVM中会有多个类加载器,当两个类加载器同时加载一个类时,会出现两个实例,此时也应当尽量避免使用有状态的单例类。

工厂方法模式

定义一个用于创建对象的接口,让子类决定实例化那个类。

优点:

  1. 良好的封装性,代码结构清晰;优秀的可扩展性。
  2. 屏蔽产品类;典型的解耦框架。

使用场景:

  1. 工厂方法模式是new一个对象的替代品,故在所有需要生成对象的地方都可以使用,但是需要慎重考虑是否需要增加一个工厂类进行管理,增加代码的复杂度。
  2. 需要灵活的、可扩展的框架时。
  3. 可以用在异构项目中。
  4. 可以使用在测试驱动开发的框架下。

抽象工厂模式

为创建一组相关或相互依赖的对象提供一个接口,且无需指定它们的具体类。

优点:

  1. 产品族内的约束为非公开状态,在不同的工厂中,各种产品可能具有不同的相互依赖关系,这些依赖关系由工厂封装在其内部,对于工厂的使用者是不可见的。
  2. 生产线的扩展非常容易,如果要针对同一产品族建立新的生产线,只需要实现产品族中的所有产品接口并建立新的工厂类即可。

缺点:
产品族本身的扩展非常困难,如果需要在产品族中增加一个新的产品类型,则需要修改多个接口,并且会影响已有的工厂类。

使用场景:

使用场景:当一个对象族(或是一组没有任何关系的对象)都有相同的约束。


建造者模式

建造者将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

优点:

  1. 封装性,可以使客户端不必知道产品内部组成的细节。
  2. 建造者独立,容易扩展。
  3. 便于控制细节风险。

使用场景:

  1. 相同的方法,不同的执行顺序,产生不同的结果。
  2. 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时。
  3. 产品类非常复杂,或者产品类中的方法调用顺序不同产生了不同的效能。
  4. 在对象创建过程中会使用到系统的一些其他对象,这些对象在产品对象的创建过程中不易得到时。

原型模式

用原型实例制定创建对象的种类,并且通过复制这些原型创建新的对象。

优点:
性能优良;逃避构造函数的约束。

使用场景:
资源优化场景;性能和安全要求场景;一个对象多个修改者的场景。


代理模式

为其他对象提供一种代理,并以控制对这个对象的访问。

优点:

  1. 职责清晰:真实的角色实现实际的业务逻辑,不用关心其他非本职的事务,通过后期的代理完成附加的事务,附带的结果就是编程简洁清晰。
  2. 高扩展性:具体主题角色随需求不同可能有很多种,但只要实现了接口,代理类就完全可以在不做任何修改的情况下代理各种真实主题角色。
  3. 智能化:代理类可以在运行时才确定要去代理的真实主题,这是一种强大的功能。

使用场景:
代理模式的应用非常广泛,大到一个系统框架、企业平台,小到事务处理、代码片段,随处可见代理模式的使用,例如,JavaRMI的远程调用和AOP。


装饰模式

动态的给一个对象添加一些额外的职责。

优点:

  1. 装饰类和被装饰类都可以独立发展,而不会相互耦合;
  2. 装饰模式是继承关系的一个替代方案.
  3. 装饰模式可以动态地扩展一个实现类的功能。

使用场景:

  1. 需要扩展一个类的功能,或给一个类增加附加功能。
  2. 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
  3. 需要为一批类进行改装或加装功能。

适配器模式

将一个类的接口变换成客户端所期待的的另一种接口,从而使原本因接口不匹 配而无法在一起工作的两个类能够在一起工作。

优点:
可以让两个没有任何关系的类在一起运行;增加了类的透明度;提高类的复用度;增 强代码的灵活性。

使用场景:
修改一个已经投产中的系统时,需要对系统进行扩展。此时使用一个已有类,但 这个类不符合系统中的接口,这是使用适配器模式是最合适的,它可以将不符合系统接口的类进行转换,转换成符合系统接口的、可以使用的类。


桥梁模式

将抽象和现实解耦,使得两者可以独立地变化。

优点:

  1. 抽象和现实的分离是桥梁模式的主要特点,是为了解决继承的缺点而提出的设计模式。在该模式下,实现可以不受抽象的约束,不用绑定在一个固定的抽象层次上。
  2. 实现对客户的透明,客户端不用关心细节的实现,它已经由抽象层通过聚合关系完成了封装。

使用场合:

  1. 如果一个系统需要在构件的抽象化角色和具体角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。
  2. 设计要求实现化角色的任何改变不应当影响客户端,或者说实现化及角色的改变对客户端是完全透明的。
  3. 一个构件有多于一个抽象化角色和实现化角色,系统需要它们之间进行动态耦合。
  4. 不希望或不适合使用继承的场合。

组合模式

将组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组 合对象的使用具有一致性。

优点:
高层模块调用简单;节点自由增加。

缺点:
不易控制树枝构件的类型;不易使用继承的方法来增加新的行为。

使用场景:

  1. 需要描述对象的部分和整体的等级结构,如树形菜单、文件和文件夹管理。
  2. 需要客户端忽略个体构件和组合构件的区别,平等对待所有的构件。

外观模式

要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。

优点:

  1. 减少系统的相互依赖,所有的依赖都是对Façade对象的依赖,与子系统无关。
  2. 提高灵活性,不管子系统系统内部如何变化,只要不影响Façade对象,任何活动都是自由的。
  3. 提高安全性,Façade中未提供的方法,外界就无法访问,提高系统的安全性。

使用场景:

  1. 为一个复杂的模块或子系统提供一个供外界访问的接口。
  2. 子系统相对独立,外界子系统的访问只要黑箱操作即可。
  3. 预防风险扩散,使用Façade进行访问操作控制。

享元模式

使用共享对象可有效地支持大量的细粒度的对象。

优点:
大幅减少内存中对象的数量,降低程序内存的占用,提高性能。

缺点:

  1. 增加了系统的复杂性,需要分出外部状态和内部状态,而且内部状态具有固化特性,不应该随外部状态改变而改变,这使得程序的逻辑复杂化。
  2. 将享元对象的状态外部化,而读取外部状态使得运行时间变长。

使用场景:

  1. 系统中有大量的相似对象,这些对象耗费大量的内存。
  2. 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,即对象没有特定身份。
  3. 需要缓冲池的场景。

模板方法模式

定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

优点:
封装不变的部分,扩展可变的部分;提取公共部分代码,便于维护;行为由父类控制,子类实现。

应用场景:

  1. 多个子类有公共方法,并且逻辑基本相同。
  2. 可以把重要的、复杂的、核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
  3. 重构时,模板方法模式是一个经常使用的模式,将相同的代码抽取到父类中。

命令模式

将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。

优点:
类间解耦;可扩展性;命令模式结合其他模式会更优秀。

缺点:
可能会导致系统中出现过多的具体命令类,因此需要在项目中慎重考虑使用。

应用场景:

  1. 使用命令模式作为“回调”在面向对象系统中的替代;
  2. 需要在不同的时间指令请求、将请求排队;
  3. 系统需要支持的撤销(undo);
  4. 需要将系统中所有的数据更新操作保存到日志里,以便在系统崩溃时,可以根据日志读回所有的数据更新命令,重新调用execute()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新;
  5. 一个系统需要支持交易(transaction)。

责任链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,知道有对象处理它为止。

优点:
责任链模式将请求和处理分开,请求者不知道是谁处理的,处理者可以不用知道请求的全貌;提高系统的灵活性。

缺点:

  1. 降低程序的性能,每个请求都是从链头遍历到链尾,当链比较长时,性能大幅下降。
  2. 不易于调试,由于采用了类似递归的方式,调试的时候逻辑比较复杂。

应用场景:
一个请求需要一系列的处理工作;业务流的处理,例如,文件审批;对系统进行补充扩展。


策略模式

定义一组算法,将每个算法都封装起来,并且使他们之间可以互换。

优点:
提供了管理相关算法族的办法;提供了可以替换继承关系的方法;可以避免使用多重条件转换语句。

缺点:
客户端必须知道所有的策略类,并自行决定使用哪一个策略类;策略模式造成很多的策略类。

应用场景:
多个类只是在算法或行为上稍有不同的场景;算法需要自由切换的场景;需要屏蔽算法规则的场景。


迭代器模式

提供一种方法访问一个容器对象中各个元素,又不需要暴露该对象的内部细节。

优点:

  1. 迭代器模式简化了访问容器元素的操作,具备一个统一的遍历接口。
  2. 封装遍历算法,使算法独立于聚集角色。

缺点:

  1. 迭代器模式给使用者一个序列化的错觉,从而产生错觉。
  2. 迭代器的元素都是Object类型,没有类型特征。

中介者模式:

用一个中介对象封装一系列对象(同事)的交互,中介者使对象不需要显式地相互作用,从而使其耦合松散,而且可以独立地改变他们之间的交互。

优点:

  1. 减少类间的依赖,将原有的一对多的依赖变成一对一的依赖,使得对象之间的关系更以维护和理解。
  2. 避免同事对象之间过度耦合,同事类只依赖于中介者,使同事类更易被复用,中介类和同事类可以相互独立地演化。
  3. 中介者模式将对象的行为和协作抽象化,将对象在小尺度的行为上与其他对象的相互作用分开处理。

缺点:

  1. 中介者模式降低了同事对象的复杂性,但增加了中介者类的复杂性。
  2. 中介者类经常充满了各个具体同事类的关系协调代码,这种代码是不能复用的。

注意事项:

  1. 不应当在责任划分混乱时使用;
  2. 不应当对数据类和方法类使用;正确理解封装。

观察者模式

定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

优点:
观察者和被观察者之间是抽象耦合;支持广播通信。

缺点:

  1. 如果一个主题有多个直接或者间接的观察者,则通知所有的观察者会花费很多时间,且开发和调试都比较复杂。
  2. 如果在主题之间有循环依赖,被观察者会触发他们间进行循环调用,导致系统崩溃。
  3. 如果对观察者的通知是通过另外线程进行异步投递,系统必须保证投递的顺利执行。
  4. 虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有提供相应的机制使观察者知道所观察的对象是如何发生变化。

应用场景:

  1. 关联行为场景;
  2. 事件多级触发场景;
  3. 跨系统的消息交换场景。

注意事项:
广播链的问题;异步处理的问题。


备忘录模式

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样,以后就可以将该对象恢复到原先保存的状态。

应用场景:
需要保存和恢复数据的相关场景状态;提供一个可回滚的操作;需要监控副本的场景;数据库连接的事务管理使用的就是备忘录模式。

注意事项:

  1. 备忘录的生命周期。备忘录创建出来就要在最近的代码中使用,要主动管理它的生命周期,建立就要使用,不使用就要立刻删除其引用,等待垃圾回收器对它的回收处理。
  2. 备忘录的性能。不要频繁建立备份的场景中使用备忘录模式。

访问者模式

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

优点:

  1. 使得增加新的操作变得很容易,增加新的操作只需要增加新的访问类。
  2. 将有关的行为集中到一个访问者对象中,而不是分散到一个个元素类中。
  3. 访问者模式可以跨过几个类的等级结构访问属于不同等级结构的成员类。
  4. 积累状态。

缺点:
增加新的元素类变得困难;破坏封装;违背依赖倒置原则。

应用场景:

  1. 一个对象结构类有很多类对象,它们有不同的接口,当这些对象实施依赖于具体类的操作时,即使用迭代器模式不能胜任的场景下,可以采用访问者模式。
  2. 需要对一个对象结构中的对象进行很多不同且无关的操作,避免操作污染类。
  3. 业务要求遍历多个不同的对象,这本身也是访问者模式的出发点,迭代器模式只能访问同类或同接口的数据,而访问者模式是对迭代器模式的扩充,可以遍历不同的对象,执行不同的操作。

状态模式

当一个对象内在状态改变时允许改变行为,这个对象看起来像改变了其类型。

优点:

  1. 结构清晰;遵循设计原则;封装性非常好。
  2. 缺点:子类太多,不易管理。

效果:

  1. 状态模式需要对每一个系统可能取得的状态创建一个状态类的子类。
  2. 由于每一个状态都被包装到了类里面,就可以不必采用过程性的处理方式,使长篇累牍的条件转移语句。
  3. 可以在系统的不同部分使用相同的一些状态类的对象。
  4. 状态模式会造成大量的小状态类,但是可以使程序免于大量的条件转移语句,使程序实际上更易维护。
  5. 系统所选的状态子类均是从一个抽象状态类或借口继承而来,JAVA语言的特性使得在Java语言中使用状态模式较为完全,多态性原则是状态模式的核心。

使用场景:
对象的行为依赖于它所处的状态,即行为随状态改变而改变的场景;对象在某个方法里依赖于一重或多重条件分支语句。


解释器模式

给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。

优点:
简单的语法分析工具;扩展性,修改语法规则只要修改相应的非终结符表达式即可,若扩展语法,则只要增加非终结符类即可。

缺点:
解释器模式会引起类膨胀;采用递归调用的方法。

使用场景:
重复发生的问题可以使用解释器模式;一个简单语法需要解释的场景。


后记

本文旨在对java常用的设计模式有个基本的了解。

在实际使用中,需依据业务定制实现(maybe是以上设计模式的变种),请勿生搬硬套,为了使用设计模式而使用。

坚持原创技术分享,您的支持将鼓励我继续创作!