Design Patterns Of Facade And Flyweight


一、外观模式

在现实生活中,常常存在办事较复杂的例子,如办房产证或注册一家公司,有时要同多个部门联系,这时要是有一个综合部门能解决一切手续问题就好了。

软件设计也是这样,当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这时如果系统内部发生改变,客户端也要跟着改变,这违背了“开闭原则”,也违背了“迪米特法则”,所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度,这就是外观模式的目标

1、外观模式的定义与特点

外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

在日常编码工作中,我们都在有意无意的大量使用外观模式。只要是高层模块需要调度多个子系统(2个以上的类对象),我们都会自觉地创建一个新的类封装这些子系统,提供精简的接口,让高层模块可以更加容易地间接调用这些子系统的功能。尤其是现阶段各种第三方SDK、开源类库,很大概率都会使用外观模式。

优点和缺点

外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点。

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
  • 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。

外观(Facade)模式的主要缺点如下。

  • 不能很好地限制客户使用子系统类,很容易带来未知风险。
  • 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。

2、外观模式的结构和实现

外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过它访问各个子系统的功能。现在来分析其基本结构和实现方法。

模式的结构

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。外观角色知道哪些子系统负责处理请求,从而将调用端的请求代理给适当的子系统对象。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
  • 客户(Client)角色:通过一个外观角色访问各个子系统的功能。

3、案例分析

组件一个家庭影元系统:

DVD 播放器、投影仪、自动屏幕、环绕立体声、爆米花机,要求完成使用家庭影院功能,其过程为:

  • 直接用遥控器:统筹各设备开关
  • 开爆米花机
  • 放下屏幕
  • 开投影仪
  • 开音响
  • 开 DVD,选 dvd
  • 去拿爆米花
  • 调暗灯光
  • 播放
  • 观影结束后,关闭各种设备

传统解决方案分析

  • 在 ClientTest 的 main 方法中,创建各个子系统的对象,并直接去调用子系统相关方法,会造成调用过程混乱,没有清晰的过程
  • 不利于在 ClientTest 中,去维护对子系统的操作
  • 解决思路:定义一个高层接口,给子系统中的一组接口提供一个 一致的界面(比如在高层接口提供四个方法 ready、play、pause、end),用来访问子系统中的一群接口
  • 也就是说:通过定义一个一致的接口(界面类),用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节 =>(外观模式

外观模式解决问题

  • 外观模式可以理解为转换一群接口,客户只需要调用一个接口,而不用调用多个接口达到目的。
  • 外观模式就是解决多个复杂接口带来的使用困难,起到简化用户操作的目的

4、Mybatis 源码分析

  • Mybatis 中的 Configuration 创建 MetaObject 对象时使用到了外观模式

5、注意事项和细节

  • 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
  • 外观模式对客户端与子系统的耦合关系,让子系统内部的模块更容易维护和扩展
  • 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
  • 当系统需要进行分层设计时,可以考虑使用 Facade 模式
  • 在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,此时,可以考虑为新系统开发一个 Facade 类,来提供遗留系统的比较清晰简单的接口,让新系统与 Facade 类交互,提高复用性
  • 不能过多的或者不合理的使用 Facade 模式,使用外观模式好,还是直接调用模块好,要让以系统有层次,利于维护为目的

6、应用场景和扩展

通常在以下情况可以考虑使用外观模式

  • 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系
  • 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问
  • 当客户端与多个子系统之间有很大的联系时,引入外观模式可以将它们分离,从而提高子系统的独立性和可移植性

扩展:

在外观模式中,当增加或移除子系统时需要修改外观类,违背了 ocp 原则。如果引入 抽象外观类,则在一定程度上解决了这个问题。

二、享元模式

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。

例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。

1、享元模式的定义与特点

享元(Flyweight)模式的定义:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

享元模式的 主要优点 是:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

其主要缺点是:

  • 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
  • 读取享元模式的外部状态会使得运行时间稍微变长。

2、结构和实现

享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。

  • 内部状态指对象共享出来的信息,存储在享元信息内部,并且不会随环境的改变而改变;
  • 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。

比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。

享元模式的本质是缓存共享对象,降低内存消耗。

模式的结构和实现

享元模式的主要角色有如下:

  • 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  • 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
  • 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
  • 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

看结构图:

  • UnsharedConcreteFlyweight 是非享元角色,里面包含了非共享的外部状态信息 info;
  • Flyweight 是抽象享元角色(定义了外部状态和内部状态),里面包含了享元方法 ,operation(UnsharedConcreteFlyweight state),非享元的外部状态以参数的形式通过该方法传入;
  • ConcreteFlyweight 是具体享元角色,包含了关键字 key,它实现了抽象享元接口;
  • FlyweightFactory 是享元工厂角色,它是关键字 key 来管理具体享元;(用于构建一个池容器:集合)
  • 客户角色通过享元工厂获取具体享元,并访问具体享元的相关方法。

3、案例分析

展示网站需求:

小型的外包网站,给客户 A 做一个产品展示网站,客户 A 的朋友也希望做这样的产品展示网站,但是有一部分要求不同:

  • 有客户要求以新闻方式发布
  • 有客户要求以博客方式发布
  • 有客户希望以微信公众号的方式发布

总结:大部分需求是相同的只有一小部分不同的地方,需要分割

传统方案解决

  • 直接复制粘贴一份,然后根据客户不同的需求,进行定制修改
  • 给每个网站租用一个空间

问题分析:

  • 需要的网站相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,造成服务器的资源浪费
  • 解决思路:整合到一个网站中,共享其相关的代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器开销
  • 对于代码来说,由于是一份实例,维护和扩展都更加容易
  • 上面的解决思路就可以使用 享元模式

4、享元模式解决问题

基本介绍:

  • 享元模式(Flyweight Pattern)也叫 蝇量模式:运用共享技术有效地支持大量细粒度的对象
  • 常用系统底层开发,解决系统的性能问题。像 数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的就直接拿过来用,避免重新创建,如果没有我们需要的,则创建一个
  • 享元模式能够解决 重复对象的内存浪费问题,当系统中有大量相似对象,需要缓冲池时。不需要总是创建新对象,可以从缓冲池中获取,这样就可以降低系统内存,同时提高效率
  • 享元模式 经典的应用场景 就是池技术了,String 常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。

案例看代码

5、内部状态和外部状态

比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色多一些,所以棋子颜色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化的,所以棋子坐标就是棋子的外部状态

  • 享元模式提出了两个要求:细粒度和共享对象。这里就涉及到内部状态和外部状态。即:将对象的信息分为两个部分:内部状态和外部状态
  • 内部状态 指对象共享出来的信息,存储在享元对象内部且不会随外部环境的改变而改变
  • 外部状态 指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态

例如:围棋理论上有 361 个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器难以支持更多玩家对弈,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就很好地解决了对象开销问题。

6、JDK Integer 源码分析

自己看

7、应用场景和扩展

应用场景

当系统中多处需要同一组信息时,可以把这些信息封装到一个对象中,然后对该对象进行缓存,这样,一个对象就可以提供给多出需要使用的地方,避免大量同一对象的多次创建,降低大量内存空间的消耗。

享元模式其实是 工厂方法模式 的一个改进机制,享元模式同样要求创建一个或一组对象,并且就是通过工厂方法模式生成对象的,只不过享元模式为工厂方法模式增加了缓存这一功能。

前面分析了享元模式的结构与特点,下面分析它适用的应用场景。享元模式是通过减少内存中对象的数量来节省内存空间的,所以以下几种情形适合采用享元模式。

  • 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
  • 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
  • 由于享元模式需要额外维护一个保存享元的 数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。

扩展

在前面介绍的享元模式中,其结构图通常包含可以共享的部分和不可以共享的部分。在实际使用过程中,有时候会稍加改变,即存在两种特殊的享元模式:单纯享元模式和复合享元模式,下面分别对它们进行简单介绍。

  • 单纯享元模式,这种享元模式中的所有的具体享元类都是可以共享的,不存在非共享的具体享元类。
  • 复合享元模式,这种享元模式中的有些享元对象是由一些单纯享元对象组合而成的,它们就是复合享元对象。虽然复合享元对象本身不能共享,但它们可以分解成单纯享元对象再被共享

Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC