草庐IT

设计模式之享元模式

pluto_charon 2023-03-28 原文

享元模式又称蝇量模式或者羽量模式,属于结构型模式;是指以共享的方式高效的支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

享元对象能做到共享的关键是区分内蕴状态( Internal State)外蕴状态(External State)

内蕴状态是存储在享元对象内部的,并且是不会随环境改变而有所不同的。因此,一个享元可以具有得内蕴状态并可以共享。

外蕴状态是随环境的改变而改变的、不可以共享的状态。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。

外蕴状态不可以影响享元对象的内蕴状态。换句话说,它们是相互独立的。

根据所涉及的享元对象的内部表象不同,享元模式可以分成单纯享元模式复合享元模式两种模式。

单纯享元模式

在单纯享元模式中,所有的享元对象都是可以共享的。类图如下图所示:

单纯享元模式涉及到抽象享元角色、具体享元角色、享元工厂角色、客户端角色以下四种角色:

  • 抽象享元(Flyweight)角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口。那些需要外蕴状态的操作可以通过调用商业方法以参数形式传入。
  • 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享的。
  • 享元工厂(FlyweightFactory〉角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个复合要求的享元对象。如果己经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。
  • 客户端((Client)角色:本角色需要维护一个对所有享元对象的引用。

咖啡摊例子

在一个咖啡摊(Coffee Stall)所使用的系统里,有一系列的咖啡“风味(Flavor)”。客人到摊位上购买咖啡,所有的咖啡均放在台子上,客人自己拿到咖啡后就离开摊位。咖啡有内蕴状态,也就是咖啡的风味;咖啡没有环境因素,也就是说没有外蕴状态。

如果系统为每一杯咖啡都创建一个独立的对象的话,那么就需要创建出很多的细小对象来。这样就不如把咖啡按照种类(即“风味”)划分,每一种风味的咖啡只创建一个对象,并实行共享。

使用咖啡摊主的语言来讲,所有的咖啡都可按“风味”划分成如Capucino、Espresso等,每一种风味的咖啡不论卖出多少杯,都是全同、不可分辨的。所谓共享,就是咖啡风味的共享,制造方法的共享等。因此,享元模式对咖啡摊来说,就意味着不需要为每一份单独调制。摊主可以在需要时,一次性地调制出足够一天出售的某一种风味的咖啡。

单纯享元模式例子的UML类图:

抽象享元角色:

package com.charon.flyweight.simple;

/**
 * @className: Order
 * @description: 抽象享元角色
 * @author: charon
 * @create: 2022-03-23 21:44
 */
public abstract class Order {

    /**
     * 提供咖啡
     */
    public abstract void serve();

    /**
     * 返回咖啡的口味
     * @return
     */
    public abstract String getFlavor();
}

具体享元角色:

package com.charon.flyweight.simple;

/**
 * @className: Flavor
 * @description: 具体享元角色
 * @author: charon
 * @create: 2022-03-23 21:47
 */
public class Flavor extends Order{

    /**
     * 咖啡口味
     */
    private String flavor;

    /**
     * 内蕴状态以参数方式传入
     * @param flavor
     */
    public Flavor(String flavor) {
        this.flavor = flavor;
    }

    @Override
    public void serve() {
        System.out.println("提供的咖啡口味:" + flavor);
    }

    @Override
    public String getFlavor() {
        return this.flavor;
    }
}

享元工厂角色:

package com.charon.flyweight.simple;

/**
 * @className: FlavorFactory
 * @description: 享元工厂角色
 * @author: charon
 * @create: 2022-03-23 21:49
 */
public class FlavorFactory {

    private Flavor[] flavors = new Flavor[10];

    private int orderMade = 0;

    private int totalFlavors = 0;

    /**
     * 根据口味提供咖啡
     * @param flavor
     * @return
     */
    public Order getOrder(String flavor){
        if(orderMade > 0){
            for (int i = 0; i < orderMade; i++) {
                if(flavor.equalsIgnoreCase(flavors[i].getFlavor())){
                    return flavors[i];
                }
            }
        }
        flavors[orderMade] = new Flavor(flavor);
        totalFlavors++;
        return flavors[orderMade++];
    }

    /**
     * 返回创建过的风味咖啡的个数
     * @return
     */
    public int getTotalFlavorsMade(){
        return totalFlavors;
    }
}

客户端:

package com.charon.flyweight.simple;

/**
 * @className: Client
 * @description: 客户端
 * @author: charon
 * @create: 2022-03-23 19:52
 */
public class Client {

    /**
     * 卖出的咖啡总数
     */
    private static Order[] flavors = new Flavor[20];

    private static int orderMade = 0;

    private static FlavorFactory factory;

    public static void main(String[] args) {
        // 创建风味工厂对象
        factory = new FlavorFactory();
        // 创建咖啡对象
        takeOrders("Black Coffee");
        takeOrders("Capucino");
        takeOrders("Espresso");
        takeOrders("Espresso");
        takeOrders("Capucino");
        takeOrders("Capucino");
        takeOrders("Black Coffee");
        
        // 将创建的咖啡卖给客人
        for (int i = 0; i < orderMade; i++) {
            flavors[i].serve();
        }
        System.out.println("卖出的咖啡总数:" + factory.getTotalFlavorsMade());
    }

    /**
     * 提供咖啡
     * @param flavor
     */
    private static void takeOrders(String flavor) {
        flavors[orderMade++] = factory.getOrder(flavor);
    }
}

打印:
    提供的咖啡口味:Black Coffee
    提供的咖啡口味:Capucino
    提供的咖啡口味:Espresso
    提供的咖啡口味:Espresso
    提供的咖啡口味:Capucino
    提供的咖啡口味:Capucino
    提供的咖啡口味:Black Coffee
    卖出的咖啡总数:3

从上面的打印可以看出,虽然咖啡摊提供了7种咖啡,但是所有的咖啡口味却只有三种。

复合享元模式

复合享元模式是将一些单纯享元模式使用组合模式加以复合形成的。这样的复合享元对象本身不能共享,但是他们可以分解成单纯享元对象,而后者可以共享。复杂享元模式的类图如下:

复合享元模式所涉及的角色有抽象享元角色、具体享元角色、复合享元角色、享元工厂角色,以及客户端角色五种角色:

  • 抽象享元(Flyweight)角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口。那些需要外蕴状态的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的
  • 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。
  • 复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称做不可共享的享元对象。
  • 享元工厂(FlyweightFactory)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。
  • 客户端(Client)角色:本角色需要自行存储所有享元对象的外蕴状态。

咖啡屋例子

还是上面的咖啡摊的例子,随着业务的扩大,店家将咖啡摊升级为咖啡屋了。在屋子里提供了很多桌子供客人坐,系统除了需要提供咖啡的口味外,还需要跟踪咖啡被送到哪一桌上。于是,咖啡就有了桌子作为外蕴状态了。

复合享元模式的UML类图:

抽象享元角色:

package com.charon.flyweight.composite;

/**
 * @className: Order
 * @description: 抽象享元模式
 * @author: charon
 * @create: 2022-03-23 21:44
 */
public abstract class Order {

    /**
     * 提供咖啡
     */
    public abstract void serve(Table table);

    /**
     * 返回咖啡的口味
     * @return
     */
    public abstract String getFlavor();
}

具体享元角色:

package com.charon.flyweight.composite;


/**
 * @className: Flavor
 * @description: 具体享元角色
 * @author: charon
 * @create: 2022-03-23 21:47
 */
public class Flavor extends Order {

    /**
     * 咖啡口味
     */
    private String flavor;

    /**
     * 内蕴状态以参数方式传入
     * @param flavor
     */
    public Flavor(String flavor) {
        this.flavor = flavor;
    }
    
	/**
     * 将咖啡卖给客人并备注客人的座位号
     * @param table
     */
    @Override
    public void serve(Table table) {
        System.out.println("提供的咖啡口味:" + flavor + " 桌位在:" + table.getNumber());
    }

    @Override
    public String getFlavor() {
        return this.flavor;
    }
}

享元工厂角色代码不变。

复合享元角色:

package com.charon.flyweight.composite;

/**
 * @className: Table
 * @description:
 * @author: charon
 * @create: 2022-03-23 22:11
 */
public class Table {

    /**
     * 桌子号码
     */
    private int number;

    public Table(int number) {
        this.number = number;
    }

    /**
     * Gets the value of number
     *
     * @return the value of number
     */
    public int getNumber() {
        return number;
    }

    /**
     * Sets the number
     *
     * @param number number
     */
    public void setNumber(int number) {
        this.number = number;
    }
}

客户端:

package com.charon.flyweight.composite;

/**
 * @className: Client
 * @description: 客户端
 * @author: charon
 * @create: 2022-03-23 19:52
 */
public class Client {

    /**
     * 卖出的咖啡总数
     */
    private static Order[] flavors = new Flavor[20];

    private static int orderMade = 0;

    private static FlavorFactory factory;

    public static void main(String[] args) {
        // 创建风味工厂对象
        factory = new FlavorFactory();
        // 创建咖啡对象
        takeOrders("Black Coffee");
        takeOrders("Capucino");
        takeOrders("Espresso");
        takeOrders("Espresso");
        takeOrders("Capucino");
        takeOrders("Capucino");
        takeOrders("Black Coffee");
        
        // 将创建的咖啡卖给客人,并将享元对象的外蕴状态赋值给享元对象
        for (int i = 0; i < orderMade; i++) {
            flavors[i].serve(new Table(i));
        }
        System.out.println("卖出的咖啡总数:" + factory.getTotalFlavorsMade());
    }

    /**
     * 提供咖啡
     * @param flavor
     */
    private static void takeOrders(String flavor) {
        flavors[orderMade++] = factory.getOrder(flavor);
    }
}

打印:
    提供的咖啡口味:Black Coffee 桌位在:0
    提供的咖啡口味:Capucino 桌位在:1
    提供的咖啡口味:Espresso 桌位在:2
    提供的咖啡口味:Espresso 桌位在:3
    提供的咖啡口味:Capucino 桌位在:4
    提供的咖啡口味:Capucino 桌位在:5
    提供的咖啡口味:Black Coffee 桌位在:6
    卖出的咖啡总数:3

享元模式的主要优点是:

  1. 相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

其主要缺点是:

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

享元模式的应用场景

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

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

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

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

有关设计模式之享元模式的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  5. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  6. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  7. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  8. ruby-on-rails - environment.rb 中设置的常量在开发模式中消失 - 2

    了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl

  9. ruby-on-rails - 设计注册确认 - 2

    我在我的项目中有一个用户和一个管理员角色。我使用Devise创建了身份验证。在我的管理员角色中,我没有任何确认。在我的用户模型中,我有以下内容:devise:database_authenticatable,:confirmable,:recoverable,:rememberable,:trackable,:validatable,:timeoutable,:registerable#Setupaccessible(orprotected)attributesforyourmodelattr_accessible:email,:username,:prename,:surname,:

  10. Ruby:标准递归模式 - 2

    我经常迷上ruby​​的一件事是递归模式。例如,假设我有一个数组,它可能包含无限深度的数组作为元素。所以,例如:my_array=[1,[2,3,[4,5,[6,7]]]]我想创建一个方法,可以将数组展平为[1,2,3,4,5,6,7]。我知道.flatten可以完成这项工作,但这个问题是作为我经常遇到的递归问题的一个例子-因此我试图找到一个更可重用的解决方案。简而言之-我猜这种事情有一个标准模式,但我想不出任何特别优雅的东西。任何想法表示赞赏 最佳答案 递归是一种方法,它不依赖于语言。您在编写算法时要考虑两种情况:再次调用函数的情

随机推荐