面向对象的思想确实很好地解决了抽象性的问题,以至于在面向对象的眼中,万事万物一切皆对象。不可避免的是,采用面向对象的编程方式,可能会增加一些资源和性能上的开销。不过,在大多数情况下,这种影响还不是太大,所以,它带来的空间和性能上的损耗相对于它的优点而言,基本上不用考虑。但是,在某些特殊情况下,大量细粒度对象的创建、销毁以及存储所造成的资源和性能上的损耗,可能会在系统运行时形成瓶颈。那么我们该如何去避免产生大量的细粒度对象,同时又不影响系统使用面向对象的方式进行操作呢?享元设计模式提供了一个比较好的解决方案。
享元模式(英语:Flyweight Pattern)是一种软件设计模式。它使用共享物件,用来尽可能减少内存使用量以及分享资讯给尽可能多的相似物件;它适合用于当大量物件只是重复因而导致无法令人接受的使用大量内存。通常物件中的部分状态是可以分享。常见做法是把它们放在外部数据结构,当需要使用时再将它们传递给享元。图1.1为享元模式的结构图。
图1.1享元模式结构图
享元模式(Flyweight):运用共享的技术有效地支持大量细粒度的对象。
抽象享元角色(Flyweight):此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口或抽象类。那些需要外部状态(External State)的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的。
具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内部状态的话,必须负责为内部状态提供存储空间。享元对象的内部状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。
复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称作不可共享的享元对象。这个角色一般很少使用。
享元工厂(FlyweightFactoiy)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。
客户端(Client)角色:本角色还需要自行存储所有享元对象的外部状态。
内部状态与外部状态:在享元对象内部并且不会随着环境改变而改变的共享部分,可以称之为享元对象的内部状态,反之随着环境改变而改变的,不可共享的状态称之为外部状态。
公共交换电话网的使用方式就是生活中常见的享元模式的例子。公共交换电话网中的一些资源,例如拨号音发生器、振铃发生器和拨号接收器,都是必须由所有用户共享的,不可能为每一个人都配备一套这样的资源,否则公共交换电话网的资源开销也太大了。当一个用户拿起听筒打电话时,他根本不需要知道到底使用了多少资源,对用户而言所有的事情就是有拨号音,拨打号码,拨通电话就行了。所以,就有很有人会共用一套资源,非常节省,这就是享元模式的基本思想。
假如我们要开发一个类似MS Word的字处理软件,下面分析一下将如何来实现。对于这样一个字处理软件,它需要处理的对象既有单个字符,又有由字符组成的段落以及整篇文档,根据面向对象的设计思想,不管是字符、段落还是文档都应该作为单个的对象去看待。我们暂不考虑段落和文档对象,只考虑单个的字符,于是可以很容易的得到下面的结构图:
Java代码:
//抽象的字符类
public abstract class Charactor{
//属性
protected char letter;
protected int fontsize;
//显示方法
public abstract void display();
}
//具体的字符类A
public class CharactorA extends Charactor{
//构造函数
public CharactorA(){
this.letter = 'A';
this.fontsize = 12;
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
//具体的字符类B
public class CharactorB extends Charactor{
//构造函数
public CharactorB(){
this.letter = 'B';
this.fontsize = 12;
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
我们的这段代码完全符合面向对象的思想,但是却为此搭上了太多的性能损耗,代价很昂贵。
一篇文档的字符数量很可能达到成千上万,甚至更多,那么在内存中就会同时存在大量的Charactor对象,这时候的内存开销可想而知。
我们对内存中的对象稍加分析就能发现,虽然内存中Character实例很多,但是里面有很多实例差不多是相同的,比如CharactorA类的实例就有可能出现过很多次,这些不同的CharactorA的实例之间只有部分状态不同而已。那么,我们是不是可以只创建一份CharactorA的实例,然后让整个系统共享这个实例呢?直接使用显然是行不通的。比如一份文档中使用了很多的字符A,虽然它们的属性letter相同,都是'A',但是它们的fontsize却不相同的,即字符大小并不相同。显然,对于实例中的相同状态是可以共享的,不同的状态就不能共享了。
为了解决这个问题,我们可以变换一下思路:首先将不可共享的状态从类里面剔除出去,即去掉fontsize这个属性,这时候我们再写一下代码:
Java代码:
//抽象的字符类
public abstract class Charactor{
//属性
protected char letter;
//显示方法
public abstract void display();
}
//具体的字符类A
public class CharactorA extends Charactor{
//构造函数
public CharactorA(){
this.letter = 'A';
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
//具体的字符类B
public class CharactorB extends Charactor{
//构造函数
public CharactorB(){
this.letter = 'B';
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
经过这次重构,类里面剩余的状态就可以共享了,下面我们要做的工作就是要控制Charactor类的创建过程。如果已经存在了“A”字符这样的实例,就不需要再创建,直接返回实例;如果没有,则创建一个新的实例,这跟单例模式的做法有点类似了。在单例模式中是由类自身维护一个唯一的实例,享元模式则引入一个单独的工厂类CharactorFactory来完成这项工作:
Java代码:
public class CharactorFactory{
private Hashtable<String,Charactor> charactors = new Hashtable<String,Charactor>();
//构造函数
public CharactorFactory(){
charactors.put("A", new CharactorA());
charactors.put("B", new CharactorB());
}
//获得指定字符实例
public Charactor getCharactor(String key){
Charactor charactor = (Charactor)charactors.get(key);
if (charactor == null){
if(key.equals("A")){
charactor = new CharactorA();
}else if(key.equals("B")){
charactor = new CharactorB();
}
charactors.put(key, charactor);
}
return charactor;
}
}
经过本次重构,已经可以使用同一个实例来存储可共享的状态,下面还需要做的工作就是要处理被剔除出去的那些不可共享的状态。缺少了这些不可共享的状态,Charactor对象就无法正常工作。
那么如何解决对象中不可共享状态的问题呢?
我们把这些不可共享的状态仍然保留在Charactor对象中,把不同的状态通过参数化的方式,由客户程序注入。以下代码是我们最终实现的一个版本:
Java代码:
//抽象的字符类
public abstract class Charactor{
//属性
protected char letter;
protected int fontsize;
//显示方法
public abstract void display();
//设置字体大小
public abstract void setFontSize(int fontsize);
}
//具体的字符类A
public class CharactorA extends Charactor{
//构造函数
public CharactorA(){
this.letter = 'A';
this.fontsize = 12;
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
//设置字体大小
public void setFontSize(int fontsize){
this.fontsize = fontsize;
}
}
//具体的字符类B
public class CharactorB extends Charactor{
//构造函数
public CharactorB(){
this.letter = 'B';
this.fontsize = 12;
}
//显示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
//设置字体大小
public void setFontSize(int fontsize){
this.fontsize = fontsize;
}
}
//客户程序
public class ClinetTest{
public static void main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//设置字符A的大小
a.setFontSize(12);
//显示字符B
a.display();
//设置字符B的大小
b.setFontSize(14);
//显示字符B
b.display();
}
}
本文通过给出享元模式的典型应用例子,来介绍了享元模式的具体应用,但享元模式在一般的开发中并不常用,而是常常应用于系统底层的开发,以便解决系统的性能问题。
适用性
Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当以下情况都成立时使用Flyweight模式。
1)一个应用程序使用了大量的对象。
2)完全由于使用大量的对象,造成很大的存储开销。
3)对象的大多数状态都可变为外部状态。
4)如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
5)应用程序不依赖对象标识。
优缺点
1)享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
2)享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。
相关推荐
11. 享元模式:享元模式的主要思想是以共享的方式高效的支持大量的细粒度对象。这种模式的优点是可以大幅度的降低内存中对象的数量,但缺点是增加了系统的复杂度。 12. 代理模式:代理模式的主要思想是给某一个对象...
【UML与设计模式浅析】 UML(Unified Modeling Language),即统一建模语言,是一种用于软件系统分析和设计的标准图形表示法。它为软件开发提供了可视化工具,以描绘系统的需求、架构、交互以及实现等多个方面。UML...
11. **享元模式**:享元模式是用于性能优化的创建型模式,通过共享大量相似对象来减少内存使用。它区分了内部状态(不可变)和外部状态(可变),只共享内部状态相同的对象。 12. **代理模式**:代理模式是结构型...
同时,为了提升用户黏性,唯品会推出了会员制度,通过积分兑换、会员专享优惠等方式增强用户忠诚度。 总之,唯品会的经营模式是多维度的,包括特卖模式、品牌合作、供应链管理、大数据应用、多元化营销以及不断拓展...
享元模式 代理模式 1、 适配器模式(Adapter) 将一个类的接口转换成客户希望的另一个接口,适配器模式使得原本的由于接口不兼容而不能一起工作的那些类可以一起工作。 应用场景:老代码接口不适应新的接口需求,或者...
11. **享元模式**:用于减少大量相似对象的创建,通过共享技术来有效支持大量细粒度的对象。区分对象的内在状态和外在状态,只共享内在状态不变的对象。 12. **代理模式**:结构型模式,为其他对象提供一种代理以...
"浅析交换机路由器的配置维护和管理" 在信息化时代,网络安全问题日益严峻,交换机路由器作为企业网络的核心设备,承担着重要的安全责任。本文对交换机的概念、配置、维护和故障注意事项进行了探讨。 首先,交换机...
当innodb_file_per_table设置为OFF时,使用的是共享表空间模式,当设置为ON时,则为独享表空间模式。需要注意的是,这个设置仅对新创建的表有效,对于已经创建的表,即使更改了innodb_file_per_table参数,也不会...
此外,它还通过促销活动、优惠券和会员专享福利来吸引和留住顾客。 2.4 扩张战略 2.4.1 联盟战略 亚马逊通过与各类商家和品牌建立合作关系,丰富产品线,提高自身竞争力。同时,与其他互联网公司合作,如与Netflix...
例如,“PLUS会员”每年支付198元,可以享受包括360元运费券礼包、24小时专属客服在内的十大权益。这些权益不仅减少了客户的直接支出,也增加了企业的收入,使得企业的净资产收益率得以提高。据京东财报显示,其净...
豆卡网的搭建选用稳定且高效的技术方案,初期采用中国万网的G享虚拟主机,后期视运营情况考虑升级至独立服务器。服务器配置包含足够的存储空间、数据库支持,以及对.NET和PHP的支持,确保并发连接数能满足用户需求。...
此外,还可以使用导航守卫(如全局守卫、组件守卫或路线独享守卫)来控制路由的导航行为,比如在跳转前进行权限验证。 总的来说,Vue Router 通过路由模式的灵活选择和强大的路由配置,为 Vue.js 应用提供了高效且...
付费会员制度通过提供专属权益,如优先配送、优惠券、专享活动等,吸引用户投入一定的费用成为会员。这种投资感使得用户更愿意持续使用平台服务,从而增强了用户粘性。京东PLUS会员就享有免运费、优先发货等特权,这...
C2M反向定制是消费者驱动型的制造模式,通过精确洞察消费者需求,制造出符合需求的产品,然后进行精准营销。这种方式对数据处理能力有较高的要求,数据清洗是其中关键的一步,它保证了数据的质量,是大数据精准营销...