揭秘享元模式-轻松实现资源高效利用的秘密武器

举报
dancer 发表于 2024/03/30 07:14:34 2024/03/30
【摘要】 享元模式,作为一种优雅的软件设计模式,恰如其分地应对了资源浪费这一普遍挑战。在这个信息爆炸的时代,软件系统往往面临着处理庞大对象数量的压力,每个对象都消耗宝贵的存储和计算资源。说到解决这一问题,享元模式就如同轻装上阵的艺术,精妙地引导我们走向共享与复用的智慧之路。通过享元模式,系统可以以细粒度地复用对象,那些具有广泛相似性的对象会共享一个单一实体。这一策略巧妙地减少了不必要的对象创建,实现了内存的

logo.png

💪🏻 制定明确可量化的目标,坚持默默的做事。


引言:

    在当今快节奏的数字时代,软件开发领域的挑战愈发严峻。随着应用程序的复杂性不断增加,开发者们不得不面对一个关键问题:如何在资源有限的前提下提高软件性能?这个问题不仅关乎应用的流畅运行,更直接影响到用户体验和市场竞争力。
    在这个引人注目的背景下,我们不禁思索:有没有一种方法能够在有限资源下突破性能瓶颈?答案是肯定的。以享元模式为代表的设计模式为我们提供了一种引人瞩目的解决方案。
    享元模式,作为一种优秀的性能优化手段,正逐渐成为软件开发领域的热门话题。它能够在保证系统运行效率的前提下,最大化地利用有限的资源,为我们开发者提供了一种引人瞩目的性能优化思路。

    在接下来的系列文章中,我们将深入探讨享元模式的应用实践和优化原理,分享如何利用享元模式来提高软件性能的有效技巧和策略。让我们一起探寻享元模式这个性能优化的利器,为我们的软件开发之路带来新的启发和突破!

        

一、简介

    在软件设计模式的世界中,享元模式以其独特的特色和强大的优势脱颖而出。与其他设计模式相比,享元模式侧重于共享和最大化复用相似对象的内部状态,从而在有限的资源下提供了令人惊叹的性能优势。
    与传统的设计模式相比,享元模式独具特色。它注重将对象的内部状态和外部状态相分离,以便共享相同的内部状态,从而有效减少对象的数量,节省系统资源,提高系统的性能。
    在享元模式的基本结构中,我们可以看到它的简洁而精巧。通过对内部状态和外部状态的分离,结合工厂模式的对象创建,以及享元工厂和享元对象的协同工作,享元模式为我们提供了一个优雅而高效的解决方案。

     现在,让我们携手深入探索享元模式的奥秘,打开它的设计原理和精髓,为您揭示如何运用享元模式来优化系统性能,实现更加高效的软件设计。让我们一同踏上这段令人兴奋的探索之旅,为我们的软件设计注入新的活力和智慧!

        

二、实现资源的极致利用

公共自行车与享元模式的智慧共享

    公共自行车共享系统,都市中的便捷之选。扫码即走,归还即停,每辆自行车都是享元模式的生动演绎:内核共享,外表各异。车架、车座等固定不变,而骑行者的身份、借还时间和地点则灵活多变。
    这些自行车智能地“复用”着共通资源,避免了不必要的浪费。同样,在软件开发中,享元模式将共性信息抽象共享,个性化数据动态传递。如此设计,系统仅需存储少量实例,却能服务众多用户,大幅降低内存消耗。
    想象一下,若每位软件用户都占用独立资源,那将是何等的负担!幸而享元模式如魔法般降临,它用有限的实体支撑起无限的服务需求。正如几辆自行车就能满足整个城市的骑行需求一样。

    本文将深入探讨享元模式的奥秘,揭示它在软件设计中如何轻装上阵,提升性能。让我们一起掌握这一强大工具,构建更加高效、轻盈的软件世界。

HOW

    享元模式通过合理地分离和共享对象的内部状态与外部状态,实现了资源的极致利用。让我们一步步揭示这种模式是如何精准地执行这一目标的。

1. 内部状态共享

  • 享元模式首先要求将对象的状态分为内部状态和外部状态。内部状态是对象共有的属性,它不会随着环境的改变而改变,因此可以被共享。实施这一步骤,软件设计师需要辨别出哪些属性是可以共享的,这些属性会被用来创建享元对象。这意味着,相似对象之间的重复信息只会存储一份,极大地节省了存储空间。

  2. 享元工厂

  • 通过使用一个享元工厂,系统能够精确地保证一个类的一个实例只被创建一次。当客户端请求一个享元对象时,享元工厂先检查是否已经创建了这个对象的实例,如果是,则返回已有的实例,否则,创建一个新的实例。这样所有的请求都会复用已有的实例,极少地占用内存。

  3. 外部状态传递

  • 由于内部状态已经被共享,享元对象需要通过某种方式得知外部的上下文,即外部状态。在使用享元对象时,客户端会将外部状态传递给享元对象。这样,不需要在每个对象中存储外部状态,从而节省资源,且可以动态地改变对象的外部状态。

  4. 独立的外部状态管理

  • 为了使系统能够更加高效地运作,外部状态通常由客户端代码来管理,这意味着享元模式并不对如何管理外部状态做出约束,提供了灵活性。客户端可以根据自己的需求设计最优的方式来存储和传递外部状态。

  5. 减少对象的互相依赖

  • 由于享元模式的使用,系统中对象的数量大幅度减少,这也意味着对象之间的相互引用和依赖也随之减少,进一步优化了内存的使用,并且简化了对象之间的关系,易于管理和维护。

        

三、案例探讨

3.1 场景

如权限控制。几乎所有PC端的应用系统都有权限控制。

  1. 一般用户:

  • 只能查看本部门人员列表的权限。

  2. 部门经理:

  • 不仅可查看本部门人员列表,还可以查看本部门人员薪资的权限。

  3. 部门主管:

  • 除了部门人员和人员薪资外,还有部门战略、目标和预算等的权限。

    现在我们要来实现这样的功能,怎么实现?

         

3.2 不用模式实现:一坨坨代码实现

 思路

    为了减轻数据库的压力,我们把用户的权限放到内在中。这样每次操作的时候,就直接在内在中进行权限的校验,速度会更快一些,这就是 “典型的以空间换时间”的做法。

  一坨坨代码

/**
 * 描述授权数据的model <br/>
 *
 * @author danci_
 * @date 2024/2/7 08:22:35
 */
public class AuthorizationModel {
    /**
     * 人员
     */
    private String user;
    /**
     * 案例实体
     */
    private String securityEntity;
    /**
     * 权限
     */
    private String permit;
    /* 省略的get set方法 */
}

    用一个类模拟保存内存数据(真正的项目可能用一些缓存中间件来实现,比如Redis、Memcached等)

/**
 * 数据缓存 <br/>
 *
 * @author danci_
 * @date 2024/2/7 08:23:01
 */
public class CacheDb {
    /**
     * 用来存放授权数据的值
     */
    public static List<String> colDb = new ArrayList<>();
    static {
        // 通过静态块来初始权限数据信息
        colDb.add("张三,人员列表,查看");
        colDb.add("李四,人员列表,查看");
        colDb.add("李四,薪资数据,查看");
        colDb.add("王五,薪资数据,查看");
        colDb.add("赵六,薪资数据,修改");
        // 配置更多权限
        colDb.add("王五,人员列表,查看");
        colDb.add("王五,人员列表,查看");
        colDb.add("王五,人员列表,查看");
        colDb.add("赵六,薪资数据,查看");
        colDb.add("赵六,人员列表,查看");
    }
}

    实现登录和权限控制

/**
 * 安全管理,实现成单例(简单起见,用懒汉式单例) <br/>
 *
 * @author danci_
 * @date 2024/2/7 08:23:31
 */
public class SecurityMgr {
    private static SecurityMgr securityMgr = new SecurityMgr();
    private SecurityMgr() {}
    public static SecurityMgr getInstance() {
        return securityMgr;
    }
    /**
     * 在运行期间,用来存放登陆人员对应的数据
     * 在Web应用中,这些数据
     */
    private Map<String, List<AuthorizationModel>> map = new HashMap<>();
    /**
     * 模拟登陆的功能
     */
    public void login(String user) {
        // 登陆时就需要把该用户所拥有的权限,从数据库中取来,放到缓存中
        List<AuthorizationModel> col = queryByUser(user);
        map.put(user, col);
    }
    /**
     * 判断用户对某个安全实体是否拥有某种权限
     * @param user 被检测权限的用户
     * @param securityEntity 安全实体
     * @param permit 权限
     */
    public boolean hasPermit(String user, String securityEntity, String permit) {
        List<AuthorizationModel> col = map.get(user);
        if (null == col || 0 == col.size()) {
            System.out.println(user + " 没有登陆或没有权限!");
            return false;
        }
        for (AuthorizationModel authorizationModel : col) {
            // 输出当前实例,看看是否同一个实例对象
            System.out.println("authorizationModel == " + authorizationModel);
            if (authorizationModel.getSecurityEntity().equals(securityEntity)
                    && authorizationModel.getPermit().equals(permit)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 从数据库中获取某人的所有权限
     */
    private List<AuthorizationModel> queryByUser(String user) {
        List<AuthorizationModel> col = new ArrayList<>();
        for (String s : CacheDb.colDb) {
            String[] ss = s.split(",");
            if (ss[0].equals(user)) {
                AuthorizationModel am = new AuthorizationModel();
                am.setUser(user);
                am.setSecurityEntity(ss[1]);
                am.setPermit(ss[2]);
                col.add(am);
            }
        }
        return col;
    }
}

    添加客户端测试下

/**
 * 描述类的作用 <br/>
 *
 * @author danci_
 * @date 2024/2/7 08:33:12
 */
public class Client {
    public static void main(String[] args) {
        SecurityMgr mgr = SecurityMgr.getInstance();
        mgr.login("张三");
        mgr.login("李四");
        boolean bool1 = mgr.hasPermit("张三", "人员列表", "查看");
        boolean bool2 = mgr.hasPermit("李四", "人员列表", "查看");
        boolean bool3 = mgr.hasPermit("李四", "薪资数据", "查看");
        System.out.println("张三是否有人员列表 查看权限:" + bool1);
        System.out.println("李四是否有人员列表 查看权限:" + bool2);
        System.out.println("李四是否有薪资数据 查看权限:" + bool3);
        mgr.login("王五");
        boolean bool4 = mgr.hasPermit("王五", "人员列表", "查看");
        boolean bool5 = mgr.hasPermit("王五", "薪资数据", "查看");
        boolean bool6 = mgr.hasPermit("王五", "薪资数据", "修改");
        System.out.println("王五是否有人员列表 查看权限:" + bool4);
        System.out.println("王五是否有薪资数据 查看权限:" + bool5);
        System.out.println("王五是否有薪资数据 修改权限:" + bool6);
    }
}

  运行结果如下:

authorizationModel == cx.securt.AuthorizationModel@77459877
authorizationModel == cx.securt.AuthorizationModel@5b2133b1
authorizationModel == cx.securt.AuthorizationModel@5b2133b1
authorizationModel == cx.securt.AuthorizationModel@72ea2f77
张三是否有人员列表 查看权限:true
李四是否有人员列表 查看权限:true
李四是否有薪资数据 查看权限:true
authorizationModel == cx.securt.AuthorizationModel@33c7353a
authorizationModel == cx.securt.AuthorizationModel@681a9515
authorizationModel == cx.securt.AuthorizationModel@33c7353a
authorizationModel == cx.securt.AuthorizationModel@33c7353a
authorizationModel == cx.securt.AuthorizationModel@681a9515
authorizationModel == cx.securt.AuthorizationModel@3af49f1c
authorizationModel == cx.securt.AuthorizationModel@19469ea2
王五是否有人员列表 查看权限:true
王五是否有薪资数据 查看权限:true
王五是否有薪资数据 修改权限:false

    输出显示张三有查看人员权限,李四和王五都有查看列表权限 和 查看薪资权限,王五没有修改薪资数据权限,输出正确。(true:有权限,false:无权限)

        

3.3 痛点

    虽然这然缓存权限信息,权限校验时的速度大大加快了,实现和挻不错,同时也有如下问题值提深思:

  1. 数据过时

  • 当缓存数据设置了一个固定的TTL(Time To Live),且该TTL较长时,如果原始数据源中的数据在这段时间内发生了变化,缓存中的数据就会变得过时。过时的数据可能导致应用程序做出错误的决策,影响用户体验和业务逻辑。

  2. 内存浪费

  • 如果缓存的数据项TTL设置得过长,而这些数据项又不再被频繁访问,那么它们会长时间占据宝贵的内存资源,造成内存浪费。在内存资源有限的情况下,这可能导致其他重要的数据或进程无法得到足够的内存支持。

  3. 缓存污染

  • 当缓存中存储了大量不再需要或已经过时的数据时,这种现象被称为缓存污染。缓存污染不仅降低了缓存的效率,还增加了缓存管理的复杂性。当需要访问的数据不在缓存中时,系统可能需要花费更多的时间和资源来查找和加载这些数据。

  4. 一致性问题

  • 在分布式系统中,多个节点可能共享同一个数据项的缓存。如果每个节点对该数据项的TTL设置不一致,就可能导致数据一致性问题。例如,一个节点可能从缓存中读取了过时的数据,而另一个节点读取了最新的数据,这会造成数据不一致和难以预测的行为。

  5. 缺乏灵活性

  • 固定TTL的缓存策略缺乏灵活性,无法根据数据的实际访问模式和更新频率进行动态调整。这可能导致在某些情况下缓存效率不高,无法满足应用程序的性能需求。

  6. 维护成本

  • 管理本地内存缓存需要投入一定的维护成本,包括监控缓存的使用情况、调整TTL设置、处理缓存失效等。如果TTL设置不当,可能会导致维护成本增加,同时影响应用程序的稳定性和性能。

    本文研究设计模式,关于缓存问题就提到这(了解一下即可)。

  痛点

    看示例输出实例部分,@后面的值不同代表不同的对象(一个人一个权限信息对应一个对象),一个人有N个权限信息就有N个对象,有N个用户就有N*N个对象,这个对象的数据是很恐怖的,这会耗费大量的内存,甚至可能导致系统崩溃。

  分析

    每个权限对象的粒度很小,对于某一种权限数据是重复的,这些大量重复的数据耗费了大量的内存。如:
    张三  人员列表   拥有   查看权限
    李四  人员列表   拥有   查看权限
    王五  人员列表   拥有   查看权限
    像“人员列表   拥有   查看权限” 这个权限授权给不同的人员,找个描述应该是一样的。如果有一万个人都有这个权限,按上面的示例就有一万个重复数据。

    有什么方法能解决这一万个重复数据问题么?

        

3.4 解决方案分析

    用来解决上述问题的方案是享元模式。

 分析

  1. 开销分析

  • 大量用户情况下创建大量的对象,消耗大量的内存。使用享元模式可以减少对象实例的数量来减少内存的消耗。

  2. 可行性评估

  • 相同的权限数据是可以共享的。使用享元模式可以通过共享数据来减少对象实例的数量。

  3. 性能需求分析

  • 本场景下需要快速响应用户操作,那么减少对象数量可以提高性能并降低内存占用。这时享元模式可能是一个合适的选择。

  4. 复杂度考量

  • 本文主要研究享元模式,所以不考虑复杂度问题。实际项目的如果想用享元模式一定要考虑使用享元模式时,开发团队对享元模式是否熟悉,需要权衡利弊,并考虑其他可能的解决方案。

  5. 可扩展性和可维护性

  • 本文主要研究享元模式,所以也不考虑此问题。

  6. 场景匹配

  • 对象多且只有少量的状态、对象创建和销毁的开销较大、需要快速响应用户操作,减少对象创建和销毁的时间,使用享模式很合适。

 注意

    注意,并不是所有的对象都适合缓存,因为缓存的是对象的实例,实例里面存放的主要是对象属性的值。因此,如果被缓存的对象的属性值经常变动, 那就不适合缓存了,因为真实对象的属性值变化了,那么缓存中的对象也必须 要跟着变化,否则缓存中的数据就跟真实对象的数据不同步,可以说是错误的数据了。

    内部状态:从重复出现的对象中分离出不变且重复出现的数据。

    外部状态:从重复出现的对象中分离出变化的数据不再缓存了。

        

四、深入享元模式

4.1 核心思想

    共享相同内部状态的对象,减少对象数量,降低内存开销。

         

4.2 结构和说明

  • Flyweight:享元接口,通过这个接口 Flyweight 可以接受并作用于外部状态,通过这个接口传入外部的状态,在享元对象的 方法处理中可能会使用这些外部的数据。
  • ConcreteFlyweight:具体的享元实体对象,必须是可共享的,需要封装 Flyweight的内部状态。
  • UnsharedConcreteFlyweight:非共享的享元实现对象,并不是所有的 Flyweight 实现对象都需要共享。非共享的享元实现对象通常是对共享享元对象的组合对象。
  • FlyweightFactory:享元工厂,主要用来创建并管理共享对象,并对外提供访问共享享元的接口。
  • Client:享元客户端,主要的工作是维持一个对 Flyweight 的引用,计算或存储享元对象的外部状态,当然这里可以访问共享和不共享的 Flyweight 对象。

        

4.3 示例代码

    享元接口,接受并作用于外部状态

/**
 * 享元接口,通过这个接口享元可以接受并作用于外部状态 <br/>
 *
 * @author danci_
 * @date 2024/2/7 12:16:37
 */
public interface Flyweight {
    /**
     * 传入外部状态
     * @param extrinsicState 外部状态
     */
    public void operation(String extrinsicState);
}

    具体的享元接口的实现-共享享元的实现

/**
 * 共享享元对象 <br/>
 *
 * @author danci_
 * @date 2024/2/7 12:17:54
 */
public class ConcreteFlyweight implements Flyweight {
    /**
     * 描述内部状态
     */
    private String intrinsicState;
    public ConcreteFlyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }
    @Override
    public void operation(String extrinsicState) {
        // 具体的功能处理,可能会用到享元内部、外部状态
    }
    public String getIntrinsicState() {
        return intrinsicState;
    }
    public void setIntrinsicState(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }
}

    具体的享元接口的实现-不需要共享的享元的实现

/**
 * 不需要共享的享元对象 <br/>
 * 通常是将被共享的享元对象作为子节点组合出来的对象 <br/>
 *
 * @author danci_
 * @date 2024/2/7 12:19:47
 */
public class UnsharedConcreteFlyweight implements Flyweight {
    /**
     * 描述对象的状态
     */
    private String allState;
    @Override
    public void operation(String extrinsicState) {
        // 具体的功能处理,可能会用到享元内部、外部状态
    }
}

    享元模式,客户端不直接创建共享享元对象实例,是通过享元工厂来创建

/**
 * 享元工厂 <br/>
 *
 * @author danci_
 * @date 2024/2/7 12:20:34
 */
public class FlyweightFactory {
    /**
     * 缓存多个 Flyweight 对象,这里是示意
     */
    private Map<String, Flyweight> flyweightMap = new HashMap<>();

    /**
     * 获取 key 对应的享元对象
     * @param key 获取享元对象的 key
     * @return key对应的享元对象
     */
    public Flyweight getFlyweight(String key) {
        // 先从缓存中查找,是否存在 key 对应的 Flyweight 对象
        Flyweight f = flyweightMap.get(key);
        if (null == f) {
            // 不存在,则创建一个新的 Flyweight 对象
            f = new ConcreteFlyweight(key);
            // 把这个新的 Flyweight 对象添加到缓存中,然后返回这个新的 Flyweight 对象
            flyweightMap.put(key, f);
        }
        return f;
    }

    客户端

/**
 * Client 对象,通常会维持一个Flyweight的引用 <br/>
 * 计算或存储一个或多个 Flyweight 的外部状态 <br/>
 *
 * @author danci_
 * @date 2024/2/7 12:24:55
 */
public class Client {
    public static void main(String[] args) {
        // 具体的功能处理
    }
}

        

4.4 使用享元模式重构示例

 分析3.1 的场景案例

    重复出现的数据主要是对安全实体和权限的描述。如“人员列表,查看权限”、“人员列表,修改权限”,“薪资列表,查看权限” 和 “薪资列表,修改权限” 等等这些权限数据定义为享元。而和这些权限结合的人员数据定义为享元的外部数据

 实现如下结构(图中含有不共享的实现,本示例只有共享的实现)

 代码实现

    定义享接口,外部使用享元通过面向接口来编程

/**
 * 享元接口,描述权限数据 <br/>
 *
 * @author danci_
 * @date 2024/2/7 12:16:37
 */
public interface Flyweight {
    /**
     * 判断传入的安全和权限,是否为享元对象的内部状态匹配
     * @param securityEntity 安全实体
     * @param permit 权限
     * @return true 表示匹配,false 表示不匹配
     */
    public boolean match(String securityEntity, String permit);
}

    享元对象,这个对象需要封装授权数据中重复出现部分的数据

/**
 * 封装授权数据中重复出现部分的享元对象 <br/>
 *
 * @author danci_
 * @date 2024/2/7 12:46:01
 */
public class AuthorizationFlyweight implements Flyweight {
    /**
     * 内部状态,安全实体
     */
    private String securityEntity;
    @Override
    public boolean match(String securityEntity, String permit) {
        return this.securityEntity.equals(securityEntity)
                && this.permit.equals(permit);
    }
    /**
     * 内部状态,权限
     */
    private String permit;

    public AuthorizationFlyweight(String state) {
        String[] ss = state.split(",");
        securityEntity = ss[0];
        permit = ss[1];
    }
    public String getSecurityEntity() {
        return this.securityEntity;
    }
    public String getPermit() {
        return this.permit;
    }
}

    提供享元工厂来负责对象的共享管理和对外提供访问享元的接口

/**
 * 享元工厂,通常实现成为单例 <br/>
 *
 * @author danci_
 * @date 2024/2/7 12:50:10
 */
public class FlyweightFactory {
    private static FlyweightFactory flyweightFactory = new FlyweightFactory();
    private FlyweightFactory() {}
    public static FlyweightFactory getInstance() {
        return flyweightFactory;
    }
    /**
     * 缓存多个 Flyweight 对象
     */
    private Map<String, Flyweight> flyweightMap = new HashMap<>();
    /**
     * 获取 key 对应的享元对象
     * @param key 获取享元对象的key
     * @return key对应的享元对象
     */
    public Flyweight getFlyweight(String key) {
        // 先从缓存中查找,是否存在 key 对应的 Flyweight对象
        Flyweight flyweight = flyweightMap.get(key);
        // 存在则返回 flyweight 对象
        if (null == flyweight) {
            // 不存在,则创建一个新的 Flyweight 对象
            flyweight = new AuthorizationFlyweight(key);
            flyweightMap.put(key, flyweight);
            // 然后返回这个新的 Flyweight 对象
        }
        return flyweight;
    }
}

    使用享元对象,按照前面的实现,需要一个对象来提供安全管理的业务功能,即 SecurityMgr 类,这个类现在在享元模式中充当了 Client 的角色。有点变化如下

  • 缓存的每个人员的权限数据,类型变在了 Flyweight。
  • 在原来的 queryByUser 方法中,通过 new 来创建授权对象的地方修改成了通过享元工厂来获取享元对象,这是使用享元模式最重要的一点改变,也就是不是直接去创建对象实例了,而是通过享元工厂来获取享元对象实现。
/**
 * 安全管理,实现成单例(简单起见,用懒汉式单例) <br/>
 *
 * @author danci_
 * @date 2024/2/7 08:23:31
 */
public class SecurityMgr {
    private static SecurityMgr securityMgr = new SecurityMgr();
    private SecurityMgr() {}
    public static SecurityMgr getInstance() {
        return securityMgr;
    }
    /**
     * 在运行期间,用来存放登陆人员对应的数据
     * 在Web应用中,这些数据通常会存放到 session 中
     */
    private Map<String, List<Flyweight>> map = new HashMap<>();
    /**
     * 模拟登陆的功能
     */
    public void login(String user) {
        // 登陆时就需要把该用户所拥有的权限,从数据库中取来,放到缓存中
        List<Flyweight> col = queryByUser(user);
        map.put(user, col);
    }
    /**
     * 判断用户对某个安全实体是否拥有某种权限
     * @param user 被检测权限的用户
     * @param securityEntity 安全实体
     * @param permit 权限
     */
    public boolean hasPermit(String user, String securityEntity, String permit) {
        List<Flyweight> col = map.get(user);
        if (null == col || 0 == col.size()) {
            System.out.println(user + " 没有登陆或没有权限!");
            return false;
        }
        for (Flyweight flyweight : col) {
            // 输出当前实例,看看是否同一个实例对象
            System.out.println("flyweight == " + flyweight);
            if (flyweight.match(securityEntity, permit)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 从数据库中获取某人的所有权限
     */
    private List<Flyweight> queryByUser(String user) {
        List<Flyweight> col = new ArrayList<>();
        for (String s : CacheDb.colDb) {
            String[] ss = s.split(",");
            if (ss[0].equals(user)) {
                Flyweight flyweight = FlyweightFactory.getInstance().getFlyweight(ss[1] + "," + ss[2]);
                col.add(flyweight);
            }
        }
        return col;
    }
}

    CacheDb 和 客户端代码不变,运行测试看效果:

flyweight == cx.flyweight.m.AuthorizationFlyweight@77459877
flyweight == cx.flyweight.m.AuthorizationFlyweight@77459877
flyweight == cx.flyweight.m.AuthorizationFlyweight@77459877
flyweight == cx.flyweight.m.AuthorizationFlyweight@5b2133b1
张三是否有人员列表 查看权限:true
李四是否有人员列表 查看权限:true
李四是否有薪资数据 查看权限:true
flyweight == cx.flyweight.m.AuthorizationFlyweight@5b2133b1
flyweight == cx.flyweight.m.AuthorizationFlyweight@77459877
flyweight == cx.flyweight.m.AuthorizationFlyweight@5b2133b1
flyweight == cx.flyweight.m.AuthorizationFlyweight@5b2133b1
flyweight == cx.flyweight.m.AuthorizationFlyweight@77459877
flyweight == cx.flyweight.m.AuthorizationFlyweight@77459877
flyweight == cx.flyweight.m.AuthorizationFlyweight@77459877
王五是否有人员列表 查看权限:true
王五是否有薪资数据 查看权限:true
王五是否有薪资数据 修改权限:false

    与3.2 输出结果对比

     从输出中看出,使用享元模式重构示例之后输出的对象信息中,相同的权限数据为同一个对象,即只有“人员列表,查看” 和 “薪资数据,查看” 两个对象,实现了权限数据的共享

    总言之,通过共享封装了安全实体和权限的对象,无论多少人拥有这个权限,实际的对象实例都只有一个,这就即减少了对象的数目,又节点了宝贵的内存空间,从而解决了前面提出的问题。

        

4.5 认识享元模式

 定义

    享元模式(Flyweight Pattern)是一种结构型设计模式,其核心是通过共享相似对象的方式来减少内存的使用,从而提升应用程序的性能。

 变与不变

    享元模式设计的重点就在于分离变与不变。把一个对象的状态分成内部状态和外部状态,内部状态是不变的,外部状态是可变的。然后通过共享不变的部分,达到减少对象数量并节约内存的目的。在享元对象需要的时候,可以从外部传入外部状态给共享的对象,共享对象会在功能处理的时候,使用自己内部的状态和这些外部的状态。

 共享与不共享

    享元模式中,核心思想是将对象划分为两个部分:内部状态和外部状态。内部状态是共享的部分,它是不随对象实例变化的状态,可以在多个对象之间共享。而外部状态则是非共享的部分,它随着对象实例的变化而变化,通常通过方法参数传递或者由客户端管理。

  共享:

  • 内部状态(也称为固有状态):这部分状态是共享的,意味着多个享元对象实例可以具有相同的内部状态。内部状态被存储在一个共享的存储结构中(如缓存、池等),并且当一个新的享元对象请求具有相同内部状态时,它不会创建一个新的对象实例,而是返回已经存在的具有相同内部状态的对象的引用。

  不共享:

  • 外部状态(也称为参数化状态):这部分状态是不共享的,每个享元对象实例都有自己独立的外部状态。外部状态通常通过方法调用时传递的参数来设置,或者由客户端在运行时动态管理。外部状态是对象特有的,不会在其他享元对象之间共享。

 内部状态与外部状态

    在享元模式中,内部状态 外部状态是两个核心概念,它们共同构成了享元对象的完整状态。

  内部状态

  • 共部状态是享元对象之间可以共享的状态部分。这些状态是固定的,不会随着享元对象的使用而改变。在享元模式中,共部状态被存储在享元对象中,多个享元对象可以共享相同的共部状态。这种共享机制有助于减少对象实例的数量,从而降低内存消耗。
  • 共部状态通常包括那些对于多个对象实例来说都相同的属性或数据,例如,在一个图形系统中,按钮对象的背景色、字体样式等可能就是共部状态。

  外部状态

  • 外部状态是享元对象特有的状态部分,它不会被其他享元对象共享。外部状态随着享元对象的使用而变化,并且通常是由客户端在运行时动态设置的。外部状态可以看作是享元对象在使用过程中的临时数据或参数。
  • 由于外部状态不是共享的,因此每个享元对象都可以拥有自己独特的外部状态组合,这使得享元模式能够在保持对象复用的同时,仍然能够满足不同客户的需求。

 实例池-享元工厂

    指的是缓存和管理对象实例的程序,通常实例池会提供对象实例的运行环境,并控制对象实例的生命周期。

        

4.6 适用场景

 考虑因素

    想用享元模式解决内存使用效率问题时,设计者需要细致地考量一系列的问题以确保模式的正确应用和优化效果。这些问题将为决策过程提供重要的指导,帮助识别并克服实施中的难点。下面是设计时需思考的关键问题:

  1.可共享的元素是什么?

  • 确定哪些数据是不变且可共享的(内部状态),这将成为享元对象的核心。
  • 区分那些会随着应用的上下文改变而变化的状态(外部状态)。

  2.如何有效地管理外部状态?

  • 考虑客户端如何存储和传递外部状态,确保状态的一致性和正确性。
  • 设计合适的接口与享元对象交互,避免接口使得享元对象过于复杂。

  3.享元模式对现有架构的影响如何?

  • 分析享元模式的引入将如何与现有架构融合,包括与其它设计模式的兼容性。
  • 评估该模式的引入是否会增加代码的复杂性和维护难度。

  4.如何平衡内存节省与性能开销?

  • 考察对象共享减少内存使用的同时,是否会对性能造成影响(如对象查找时间)。
  • 在实施享元模式前后进行性能基准测试,确保所做优化真正提升了应用性能。

  5.如何确保线程安全?

  • 如果在多线程环境中使用享元模式,思考如何保障共享对象的线程安全性。
  • 设计同步机制,以防止多个线程同时修改共享对象。

  6.实现复用的最佳途径是什么?

  • 考虑应该如何实现享元工厂,以支持对象的复用和管理。
  • 确定何时创建新的享元实例,何时返回现有实例。

  7.何时使用或避免享元模式?

  • 识别愈加复杂的对象管理是否值得换取内存的节约。
  • 确定享元模式是否适用于应用的特定部分,而不是盲目地应用于所有场景。

  8.是否有替代方案?

  • 考虑是否有其他设计模式或解决方案可以达到同样的性能优化效果,但具有更低的实施复杂度。

    在深入思考这些问题后,设计者应该能够判断享元模式是否适合他们的具体场景,以及如何设计一个既节约内存又保持高效和安全性的系统。通过仔细评估这些方面,我们可以确信享元模式的使用将带来显著的优势,而不是无谓的复杂性。 

 适用场景

    享元模式在软件设计中用于优化内存使用,特别是那些创建大量相似对象并可能耗尽内存资源的场景。该模式通过分享对象来减少内存消耗。以下是生活中一些可以借鉴享元模式设计思想的场景:

  1.文字处理软件中的字符实例

  • 文本编辑器用于处理大量的文字,在这种情况下,每个字符可以作为一个享元对象。由于文档中经常出现的相同字符(如字母e),共享字符对象可以节省大量内存。

  2.多用户在线游戏的非玩家角色(NPC)

  • 在线多用户游戏中,经常有成百上千的NPC。为每一个NPC都创建独立的对象将会使内存消耗巨大。使用享元模式,相同类型的NPC可以共享一个对象实例。

  3.图形软件中的图形对象

  • 对于设计和图形软件,可以用享元模式来管理共享相同属性(例如颜色、线条风格)的图形对象。用户的不同设计中可能会使用相同的图形元素。

  4.公共交通系统的票务管理

  • 在一个区域性的公共交通系统中,每一张车票对象可能都有相同的座位类型和路线数据。享元模式可以用来管理重复的数据,比如共享相同线路和座位类型的车票对象。

  5.粒子系统

  • 游戏和模拟程序中的粒子系统通常需要生成大量的粒子(比如烟雾、雨滴、火花)。这些粒子的很多属性可以用享元模式来共享,以减少内存消耗。

  6.UI元素

  • 在复杂的应用程序或网站中,按钮、图标和其他图形用户界面元素经常被重复使用。享元模式可以使得这些样式和行为相同的组件共享一个实例。

  7.数据库连接池

  • 数据库连接是另一个可应用享元模式的例子。维护一个包含多个可重复使用连接的数据库连接池,可以供所有请求共享,而不是为每个用户请求创建新的连接。

    在实现这些场景的设计时,分享的思想可以显著提高系统的效率,尤其是在处理大量的相似对象或数据时。设计必须确保共享的对象不包含特定于实例的状态,任何特定的状态都应外部管理并传递给享元对象,才能在不牺牲功能的前提下,实现内存使用的优化。 

        

五、享元模式的实现、局限和考量

    本章节将深入讨论享元模式的实际实现步骤,同时探讨在使用享元模式时需要考虑的局限性和相关的考量因素。

实现步骤

  识别共享状态和外部状态

  • 共享状态(内部状态):这些是存放在享元对象内部,不随环境改变而改变的信息。
  • 外部状态(外围状态):这些状态取决于具体的场景,并会随着环境的改变而改变。

  创建享元接口

  • 定义一个享元接口,声明具有内部状态处理能力的方法。

  实现具体享元类

  • 实现上述接口,将内部状态作为成员变量,确保其不可变。

  构建享元工厂

  • 负责创建和管理享元对象。
  • 确保相同的享元对象被复用。

  实现客户端

  • 维护外部状态。
  • 通过享元工厂获取享元对象,并传入外部状态以完成特定的功能。

局限和考量

  局限性:

  • 内部状态的不可变性:一旦共享的内部状态需要更新,可能需要重新设计或影响现有的享元对象。
  • 外部状态管理复杂化:客户端必须能有效地管理外部状态,否则可能会使得系统的复杂性增加。
  • 系统设计难度增加:正确实施享元模式需要深入理解系统的内部结构,可能会增加系统设计的难度。

  性能考量

  • 内存与计算平衡:享元模式通过共享对象降低内存消耗,但过多依赖享元对象可能会增加计算成本。
  • 复杂性与性能权衡:引入享元模式可能会使代码结构更复杂,但从长远来看,这种复杂性可能会换来更高的性能表现。

  线程安全

  • 如果应用于多线程环境,享元对象的共享可能需要额外的同步机制来保证线程安全,这可能会降低系统性能。

  对象池和垃圾回收

  • 使用对象池可以进一步提高性能,但需要注意避免因对象池的增长导致垃圾回收变得复杂,并影响性能。

        

结语:

    通过本文的深入探讨,我们清晰地认识到享元模式作为一种高效且实用的设计模式,在助力开发者实现资源优化的目标上发挥着至关重要的作用。它不仅通过共享对象实例来减少内存占用,降低系统开销,而且还通过精细化管理和控制资源的使用,达到了资源的极致利用。无论是从理论层面还是实践应用上,享元模式都展现出了其强大的生命力和广泛的应用前景。

    然而,正如每一枚硬币都有两面,享元模式虽然带来了显著的性能提升和资源优化,但在实际应用中也需要我们审慎地考虑其局限性和潜在挑战。比如,正确地识别和划分内部状态与外部状态,以及如何在多线程环境下保证线程安全等问题,都需要我们深入思考和精心设计。

    最后,我想用一句话来鼓励每一位正在阅读这篇文章的开发者:不要害怕挑战,不要满足于现状。享元模式的世界充满了无限的可能和挑战,也带来了无尽的机会和收获。只有勇于探索,敢于实践,我们才能不断突破自我,实现技术的飞跃和进步。让我们携手并进,共同迎接更多挑战,创造更加辉煌的未来!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。