设计模式专栏:http://t.csdnimg.cn/U54zu
目录
引言:
一、简介
二、实现资源的极致利用
公共自行车与享元模式的智慧共享
HOW
三、案例探讨
3.1 场景
3.2 不用模式实现:一坨坨代码实现
3.3 痛点
3.4 解决方案分析
注意
四、深入享元模式
4.1 核心思想
4.2 结构和说明
4.3 示例代码
4.4 使用享元模式重构示例
4.5 认识享元模式
定义
变与不变
共享与不共享
内部状态与外部状态
实例池-享元工厂
4.6 适用场景
考虑因素
适用场景
五、享元模式的实现、局限和考量
实现步骤
局限和考量
结语:
在当今快节奏的数字时代,软件开发领域的挑战愈发严峻。随着应用程序的复杂性不断增加,开发者们不得不面对一个关键问题:如何在资源有限的前提下提高软件性能?这个问题不仅关乎应用的流畅运行,更直接影响到用户体验和市场竞争力。 |
在这个引人注目的背景下,我们不禁思索:有没有一种方法能够在有限资源下突破性能瓶颈?答案是肯定的。以享元模式为代表的设计模式为我们提供了一种引人瞩目的解决方案。 |
享元模式,作为一种优秀的性能优化手段,正逐渐成为软件开发领域的热门话题。它能够在保证系统运行效率的前提下,最大化地利用有限的资源,为我们开发者提供了一种引人瞩目的性能优化思路。 |
在接下来的系列文章中,我们将深入探讨享元模式的应用实践和优化原理,分享如何利用享元模式来提高软件性能的有效技巧和策略。让我们一起探寻享元模式这个性能优化的利器,为我们的软件开发之路带来新的启发和突破!
在软件设计模式的世界中,享元模式以其独特的特色和强大的优势脱颖而出。与其他设计模式相比,享元模式侧重于共享和最大化复用相似对象的内部状态,从而在有限的资源下提供了令人惊叹的性能优势。 |
与传统的设计模式相比,享元模式独具特色。它注重将对象的内部状态和外部状态相分离,以便共享相同的内部状态,从而有效减少对象的数量,节省系统资源,提高系统的性能。 |
在享元模式的基本结构中,我们可以看到它的简洁而精巧。通过对内部状态和外部状态的分离,结合工厂模式的对象创建,以及享元工厂和享元对象的协同工作,享元模式为我们提供了一个优雅而高效的解决方案。 |
现在,让我们携手深入探索享元模式的奥秘,打开它的设计原理和精髓,为您揭示如何运用享元模式来优化系统性能,实现更加高效的软件设计。让我们一同踏上这段令人兴奋的探索之旅,为我们的软件设计注入新的活力和智慧!
公共自行车共享系统,都市中的便捷之选。扫码即走,归还即停,每辆自行车都是享元模式的生动演绎:内核共享,外表各异。车架、车座等固定不变,而骑行者的身份、借还时间和地点则灵活多变。 |
这些自行车智能地“复用”着共通资源,避免了不必要的浪费。同样,在软件开发中,享元模式将共性信息抽象共享,个性化数据动态传递。如此设计,系统仅需存储少量实例,却能服务众多用户,大幅降低内存消耗。 |
想象一下,若每位软件用户都占用独立资源,那将是何等的负担!幸而享元模式如魔法般降临,它用有限的实体支撑起无限的服务需求。正如几辆自行车就能满足整个城市的骑行需求一样。 |
本文将深入探讨享元模式的奥秘,揭示它在软件设计中如何轻装上阵,提升性能。让我们一起掌握这一强大工具,构建更加高效、轻盈的软件世界。
享元模式通过合理地分离和共享对象的内部状态与外部状态,实现了资源的极致利用。让我们一步步揭示这种模式是如何精准地执行这一目标的。
1. 内部状态共享:
2. 享元工厂:
3. 外部状态传递:
4. 独立的外部状态管理:
5. 减少对象的互相依赖:
|
如权限控制。几乎所有PC端的应用系统都有权限控制。 1. 一般用户:
2. 部门经理:
3. 部门主管:
|
现在我们要来实现这样的功能,怎么实现?
思路
为了减轻数据库的压力,我们把用户的权限放到内在中。这样每次操作的时候,就直接在内在中进行权限的校验,速度会更快一些,这就是 “典型的以空间换时间”的做法。 |
一坨坨代码
/**
* 描述授权数据的model
*
* @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等)
/**
* 数据缓存
*
* @author danci_
* @date 2024/2/7 08:23:01
*/
public class CacheDb {
/**
* 用来存放授权数据的值
*/
public static List colDb = new ArrayList<>();
static {
// 通过静态块来初始权限数据信息
colDb.add("张三,人员列表,查看");
colDb.add("李四,人员列表,查看");
colDb.add("李四,薪资数据,查看");
colDb.add("王五,薪资数据,查看");
colDb.add("赵六,薪资数据,修改");
// 配置更多权限
colDb.add("王五,人员列表,查看");
colDb.add("王五,人员列表,查看");
colDb.add("王五,人员列表,查看");
colDb.add("赵六,薪资数据,查看");
colDb.add("赵六,人员列表,查看");
}
}
实现登录和权限控制
/**
* 安全管理,实现成单例(简单起见,用懒汉式单例)
*
* @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> map = new HashMap<>();
/**
* 模拟登陆的功能
*/
public void login(String user) {
// 登陆时就需要把该用户所拥有的权限,从数据库中取来,放到缓存中
List col = queryByUser(user);
map.put(user, col);
}
/**
* 判断用户对某个安全实体是否拥有某种权限
* @param user 被检测权限的用户
* @param securityEntity 安全实体
* @param permit 权限
*/
public boolean hasPermit(String user, String securityEntity, String permit) {
List 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 queryByUser(String user) {
List 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;
}
}
添加客户端测试下
/**
* 描述类的作用
*
* @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:无权限)
虽然这然缓存权限信息,权限校验时的速度大大加快了,实现和挻不错,同时也有如下问题值提深思:
1. 数据过时
2. 内存浪费
3. 缓存污染
4. 一致性问题
5. 缺乏灵活性
6. 维护成本
|
本文研究设计模式,关于缓存问题就提到这(了解一下即可)。
痛点
看示例输出实例部分,@后面的值不同代表不同的对象(一个人一个权限信息对应一个对象),一个人有N个权限信息就有N个对象,有N个用户就有N*N个对象,这个对象的数据是很恐怖的,这会耗费大量的内存,甚至可能导致系统崩溃。 |
分析
每个权限对象的粒度很小,对于某一种权限数据是重复的,这些大量重复的数据耗费了大量的内存。如: 张三 人员列表 拥有 查看权限 李四 人员列表 拥有 查看权限 王五 人员列表 拥有 查看权限 像“人员列表 拥有 查看权限” 这个权限授权给不同的人员,找个描述应该是一样的。如果有一万个人都有这个权限,按上面的示例就有一万个重复数据。 |
有什么方法能解决这一万个重复数据问题么?
用来解决上述问题的方案是享元模式。
分析
1. 开销分析
2. 可行性评估
3. 性能需求分析
4. 复杂度考量
5. 可扩展性和可维护性
6. 场景匹配
|
注意,并不是所有的对象都适合缓存,因为缓存的是对象的实例,实例里面存放的主要是对象属性的值。因此,如果被缓存的对象的属性值经常变动, 那就不适合缓存了,因为真实对象的属性值变化了,那么缓存中的对象也必须 要跟着变化,否则缓存中的数据就跟真实对象的数据不同步,可以说是错误的数据了。 |
内部状态:从重复出现的对象中分离出不变且重复出现的数据。
外部状态:从重复出现的对象中分离出变化的数据不再缓存了。
共享相同内部状态的对象,减少对象数量,降低内存开销。 |
享元接口,接受并作用于外部状态
/**
* 享元接口,通过这个接口享元可以接受并作用于外部状态
*
* @author danci_
* @date 2024/2/7 12:16:37
*/
public interface Flyweight {
/**
* 传入外部状态
* @param extrinsicState 外部状态
*/
public void operation(String extrinsicState);
}
具体的享元接口的实现-共享享元的实现
/**
* 共享享元对象
*
* @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;
}
}
具体的享元接口的实现-不需要共享的享元的实现
/**
* 不需要共享的享元对象
* 通常是将被共享的享元对象作为子节点组合出来的对象
*
* @author danci_
* @date 2024/2/7 12:19:47
*/
public class UnsharedConcreteFlyweight implements Flyweight {
/**
* 描述对象的状态
*/
private String allState;
@Override
public void operation(String extrinsicState) {
// 具体的功能处理,可能会用到享元内部、外部状态
}
}
享元模式,客户端不直接创建共享享元对象实例,是通过享元工厂来创建
/**
* 享元工厂
*
* @author danci_
* @date 2024/2/7 12:20:34
*/
public class FlyweightFactory {
/**
* 缓存多个 Flyweight 对象,这里是示意
*/
private Map 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的引用
* 计算或存储一个或多个 Flyweight 的外部状态
*
* @author danci_
* @date 2024/2/7 12:24:55
*/
public class Client {
public static void main(String[] args) {
// 具体的功能处理
}
}
分析3.1 的场景案例
重复出现的数据主要是对安全实体和权限的描述。如“人员列表,查看权限”、“人员列表,修改权限”,“薪资列表,查看权限” 和 “薪资列表,修改权限” 等等这些权限数据可定义为享元。而和这些权限结合的人员数据可定义为享元的外部数据。 |
实现如下结构(图中含有不共享的实现,本示例只有共享的实现)
代码实现
定义享接口,外部使用享元通过面向接口来编程
/**
* 享元接口,描述权限数据
*
* @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);
}
享元对象,这个对象需要封装授权数据中重复出现部分的数据
/**
* 封装授权数据中重复出现部分的享元对象
*
* @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;
}
}
提供享元工厂来负责对象的共享管理和对外提供访问享元的接口
/**
* 享元工厂,通常实现成为单例
*
* @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 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 的角色。有点变化如下
/**
* 安全管理,实现成单例(简单起见,用懒汉式单例)
*
* @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> map = new HashMap<>();
/**
* 模拟登陆的功能
*/
public void login(String user) {
// 登陆时就需要把该用户所拥有的权限,从数据库中取来,放到缓存中
List col = queryByUser(user);
map.put(user, col);
}
/**
* 判断用户对某个安全实体是否拥有某种权限
* @param user 被检测权限的用户
* @param securityEntity 安全实体
* @param permit 权限
*/
public boolean hasPermit(String user, String securityEntity, String permit) {
List 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 queryByUser(String user) {
List 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 |
与3.2 输出结果对比
从输出中看出,使用享元模式重构示例之后输出的对象信息中,相同的权限数据为同一个对象,即只有“人员列表,查看” 和 “薪资数据,查看” 两个对象,实现了权限数据的共享。
总言之,通过共享封装了安全实体和权限的对象,无论多少人拥有这个权限,实际的对象实例都只有一个,这就即减少了对象的数目,又节点了宝贵的内存空间,从而解决了前面提出的问题。
享元模式(Flyweight Pattern)是一种结构型设计模式,其核心是通过共享相似对象的方式来减少内存的使用,从而提升应用程序的性能。 |
享元模式设计的重点就在于分离变与不变。把一个对象的状态分成内部状态和外部状态,内部状态是不变的,外部状态是可变的。然后通过共享不变的部分,达到减少对象数量并节约内存的目的。在享元对象需要的时候,可以从外部传入外部状态给共享的对象,共享对象会在功能处理的时候,使用自己内部的状态和这些外部的状态。 |
享元模式中,核心思想是将对象划分为两个部分:内部状态和外部状态。内部状态是共享的部分,它是不随对象实例变化的状态,可以在多个对象之间共享。而外部状态则是非共享的部分,它随着对象实例的变化而变化,通常通过方法参数传递或者由客户端管理。
共享:
不共享:
|
在享元模式中,内部状态 和外部状态是两个核心概念,它们共同构成了享元对象的完整状态。
内部状态
外部状态
|
指的是缓存和管理对象实例的程序,通常实例池会提供对象实例的运行环境,并控制对象实例的生命周期。 |
想用享元模式解决内存使用效率问题时,设计者需要细致地考量一系列的问题以确保模式的正确应用和优化效果。这些问题将为决策过程提供重要的指导,帮助识别并克服实施中的难点。下面是设计时需思考的关键问题:
1.可共享的元素是什么?
2.如何有效地管理外部状态?
3.享元模式对现有架构的影响如何?
4.如何平衡内存节省与性能开销?
5.如何确保线程安全?
6.实现复用的最佳途径是什么?
7.何时使用或避免享元模式?
8.是否有替代方案?
|
在深入思考这些问题后,设计者应该能够判断享元模式是否适合他们的具体场景,以及如何设计一个既节约内存又保持高效和安全性的系统。通过仔细评估这些方面,我们可以确信享元模式的使用将带来显著的优势,而不是无谓的复杂性。
享元模式在软件设计中用于优化内存使用,特别是那些创建大量相似对象并可能耗尽内存资源的场景。该模式通过分享对象来减少内存消耗。以下是生活中一些可以借鉴享元模式设计思想的场景:
1.文字处理软件中的字符实例:
2.多用户在线游戏的非玩家角色(NPC):
3.图形软件中的图形对象:
4.公共交通系统的票务管理:
5.粒子系统:
6.UI元素:
7.数据库连接池:
|
在实现这些场景的设计时,分享的思想可以显著提高系统的效率,尤其是在处理大量的相似对象或数据时。设计必须确保共享的对象不包含特定于实例的状态,任何特定的状态都应外部管理并传递给享元对象,才能在不牺牲功能的前提下,实现内存使用的优化。
本章节将深入讨论享元模式的实际实现步骤,同时探讨在使用享元模式时需要考虑的局限性和相关的考量因素。
识别共享状态和外部状态:
创建享元接口:
实现具体享元类:
构建享元工厂:
实现客户端:
|
局限性:
性能考量:
线程安全:
对象池和垃圾回收:
|
通过本文的深入探讨,我们清晰地认识到享元模式作为一种高效且实用的设计模式,在助力开发者实现资源优化的目标上发挥着至关重要的作用。它不仅通过共享对象实例来减少内存占用,降低系统开销,而且还通过精细化管理和控制资源的使用,达到了资源的极致利用。无论是从理论层面还是实践应用上,享元模式都展现出了其强大的生命力和广泛的应用前景。 |
然而,正如每一枚硬币都有两面,享元模式虽然带来了显著的性能提升和资源优化,但在实际应用中也需要我们审慎地考虑其局限性和潜在挑战。比如,正确地识别和划分内部状态与外部状态,以及如何在多线程环境下保证线程安全等问题,都需要我们深入思考和精心设计。 |
最后,我想用一句话来鼓励每一位正在阅读这篇文章的开发者:不要害怕挑战,不要满足于现状。享元模式的世界充满了无限的可能和挑战,也带来了无尽的机会和收获。只有勇于探索,敢于实践,我们才能不断突破自我,实现技术的飞跃和进步。让我们携手并进,共同迎接更多挑战,创造更加辉煌的未来! |