本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是享元模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:
接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构
在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。例如,围棋和五子棋中的黑白棋子、图像中的坐标点或颜色,这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式
模式定义:享元模式运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的不变对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
解决什么问题:享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象,具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。
优点:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力
缺点:享元模式 使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化;
使用场景:当满足如下场景时可以使用享元模式:
需要注意的是:享元对象应该为不可变对象,不可变对象指一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set()
等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码
享元模式的主要角色有如下。
角色间的关系UML如下:
按照如上UML图角色代码如下:
// 抽象享元角色
interface IFlyweight {void operation(UnsharedConcreteFlyweight unsharedConcreteFlyweight);
}
// 具体享元角色
class ConcreteFlyweight implements IFlyweight {private String intrinsicState;public ConcreteFlyweight(String intrinsicState) {this.intrinsicState = intrinsicState;}@Overridepublic void operation(UnsharedConcreteFlyweight unsharedConcreteFlyweight) {System.out.println("Object address: " + System.identityHashCode(this));System.out.println("IntrinsicState: " + this.intrinsicState);System.out.println("ExtrinsicState: " + ExtrinsicState());}
}
// 非享元角色
class UnsharedConcreteFlyweight {private String extrinsicState;UnsharedConcreteFlyweight(String extrinsicState) {insicState = extrinsicState;}public String getExtrinsicState() {return extrinsicState;}public void setExtrinsicState(String extrinsicState) {insicState = extrinsicState;}
}
// 享元工厂
class FlyweightFactory {private static Map<String, IFlyweight> pool = new HashMap<>();// 因为内部状态具备不变性,因此作为缓存的键public static IFlyweight getFlyweight(String intrinsicState) {if (!ainsKey(intrinsicState)) {IFlyweight flyweight = new ConcreteFlyweight(intrinsicState);pool.put(intrinsicState, flyweight);}(intrinsicState);}
}
客户端调用代码如下:
public class FlyweightPattern {public static void main(String[] args) {IFlyweight flyweight1 = Flyweight("intrinsicState-a");IFlyweight flyweight2 = Flyweight("intrinsicState-b");IFlyweight flyweight3 = Flyweight("intrinsicState-a");UnsharedConcreteFlyweight unsharedConcreteFlyweight_1 = new UnsharedConcreteFlyweight("extrinsicState-1");UnsharedConcreteFlyweight unsharedConcreteFlyweight_2 = new UnsharedConcreteFlyweight("extrinsicState-2");UnsharedConcreteFlyweight unsharedConcreteFlyweight_3 = new UnsharedConcreteFlyweight("extrinsicState-3");flyweight1.operation(unsharedConcreteFlyweight_1);flyweight2.operation(unsharedConcreteFlyweight_2);flyweight3.operation(unsharedConcreteFlyweight_3);}
}
打印结果如下:
内部状态一致时,可以看到打印的是同一个享元对象。
就以下棋为例进行实践举例:设计一个在线棋牌游戏大厅
一个游戏厅中有成千上万个房间,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。具体的代码如下所示。其中,ChessPiece 类表示棋子,ChessBoard 类表示一个棋局,里面保存了象棋中 32 个棋子的信息
public class ChessPiece {//棋子private String id;private String text;private String color;private int positionX;private int positionY;public ChessPiece(String id, String text, Color color, int positionX, int positionY) {this.id = = lor = color;this.positionX = positionX;this.positionY = positionX;}// ...省略其他属性和getter/setter方法...
}public class ChessBoard {//棋局private Map<String , ChessPiece> chessPieces = new HashMap<>();public ChessBoard() {init();}private void init() {chessPieces.put("black-che-0-0", new ChessPiece("black-che-0-0", "車", ChessPiece.Color.BLACK, 0, 0));chessPieces.put("red-che-0-1", new ChessPiece("red-che-0-0","馬", ChessPiece.Color.BLACK, 0, 1));//...省略摆放其他棋子的代码...}public void move(String chessPieceId, int toPositionX, int toPositionY) {//...省略...}
}
为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象。因为游戏大厅中有成千上万的房间(实际上,百万人同时在线的游戏大厅也有很多),那保存这么多棋局对象就会消耗大量的内存。有没有什么办法来节省内存呢?这个时候,享元模式就可以派上用场了。像刚刚的实现方式,在内存中会有大量的相似对象。这些相似对象的 id、text、color 都是相同的,唯独 positionX、positionY 不同。实际上,我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。这样,棋盘只需要记录每个棋子的位置信息就可以了。具体的代码实现如下所示:
ample.designpattern.flyweight;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;import java.util.HashMap;
import java.util.Map;public class ChessBoard {public static void main(String[] args) {ChessPieceUnit chessPieceUnit1 = ChessPiece("black-che");PiecePos piecePos1 = new PiecePos(0, 1);ChessPieceUnit chessPieceUnit2 = ChessPiece("black-che");PiecePos piecePos2 = new PiecePos(5, 1);ChessPieceUnit chessPieceUnit3 = ChessPiece("red-che");PiecePos piecePos3 = new PiecePos(0, 4);ve(piecePos1);ve(piecePos2);ve(piecePos3);}
}// 抽象享元角色
interface ChessPieceUnit {void move(PiecePos piecePos);
}// 非享元角色
@Data
@AllArgsConstructor
class PiecePos {int toPositionX;int toPositionY;
}// 具体享元角色
@AllArgsConstructor
@Getter
class ChessPieceUnitImpl implements ChessPieceUnit {private String name;private String text;private String color;@Overridepublic void move(PiecePos piecePos) {System.out.println("Object address: " + System.identityHashCode(this));System.out.println(this.name + "棋子所在棋盘位置为: " + PositionX + "," + PositionY);}
}// 享元工厂
class ChessPieceUnitFactory {private static final Map<String, ChessPieceUnitImpl> pieces = new HashMap<>();static {ChessPieceUnitImpl chessPieceUnit_black_che = new ChessPieceUnitImpl("black-che", "車", "black");ChessPieceUnitImpl chessPieceUnit_red_che = new ChessPieceUnitImpl("red-che", "車", "red");pieces.put(chessPieceUnit_Name(), chessPieceUnit_black_che);pieces.put(chessPieceUnit_Name(), chessPieceUnit_red_che);//...省略其他棋子的代码...}public static ChessPieceUnitImpl getChessPiece(String chessPieceId) {(chessPieceId);}
}
上面的代码中,我们利用工厂类来缓存 ChessPieceUnitImpl 信息(也就是 id、text、color)。通过工厂类获取到的 ChessPieceUnitImpl 就是享元。所有的 ChessBoard 对象共享这 32 个 ChessPieceUnitImpl 对象(因为象棋中只有 32 个棋子)。在使用享元模式之前,记录 1 万个棋局,我们要创建 32 万(32*1 万)个棋子的 ChessPieceUnitImpl 对象。利用享元模式,我们只需要创建 32 个享元对象供所有棋局共享使用即可,大大节省了内存。实际上,它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的
享元模式应用于Java中的基本类型Cache和String类型的缓存。
Java中的IntegerCache就是享元模式的一个应用:当我们通过自动装箱,也就是调用 valueOf()
来创建 Integer 对象的时候
Integer i = 59;底层执行了:Integer i = Integer.valueOf(59);
如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建
/*** Cache to support the object identity semantics of autoboxing for values between* -128 and 127 (inclusive) as required by JLS.** The cache is initialized on first usage. The size of the cache* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.* During VM initialization, java.lang.Integer.IntegerCache.high property* may be set and saved in the private system properties in the* sun.misc.VM class.*/
private static class IntegerCache {static final int low = -128;static final int high;static final Integer cache[];static {// high value may be configured by propertyint h = 127;String integerCacheHighPropValue =sun.SavedProperty("java.lang.Integer.IntegerCache.high");if (integerCacheHighPropValue != null) {try {int i = parseInt(integerCacheHighPropValue);i = Math.max(i, 127);// Maximum array size is Integer.MAX_VALUEh = Math.min(i, Integer.MAX_VALUE - (-low) -1);} catch( NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}}high = h;cache = new Integer[(high - low) + 1];int j = low;for(int k = 0; k < cache.length; k++)cache[k] = new Integer(j++);// range [-128, 127] must be interned (JLS7 5.1.7)assert IntegerCache.high >= 127;}private IntegerCache() {}
}
以下的例子中:
Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);
第一种创建方式并不会使用到 IntegerCache,而后面两种创建方法可以利用 IntegerCache 缓存,返回共享的对象,以达到节省内存的目的。举一个极端一点的例子,假设程序需要创建 1 万个 -128 到 127 之间的 Integer 对象。使用第一种创建方式,我们需要分配 1 万个 Integer 对象的内存空间;使用后两种创建方式,我们最多只需要分配 256 个 Integer 对象的内存空间
Java的Sting也用到了享元模式,JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作字符串常量池,细节参照我这篇Blog:【深入理解JVM 三】类Class文件结构。
String s1 = "tml";
String s2 = "tml";
String s3 = new String("tml");System.out.println(s1 == s2);
System.out.println(s1 == s3);
上面代码的运行结果是:一个 true,一个 false。跟 Integer 类的设计思路相似,String 类利用享元模式来复用相同的字符串常量(也就是代码中的tml
)。
分别看看享元模式和几个相似概念的对比:
在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。但是区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。
在享元模式的实现中,我们通过工厂类来缓存已经创建好的对象。这里的缓存实际上是存储的意思,跟我们平时所说的数据库缓存、CPU 缓存、MemCache 缓存是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。
对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?虽然对象池、连接池、线程池、享元模式都是为了复用,但是对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。
所以说设计结构类似但是设计意图不同
享元模式从概念上看非常简单,意图就是复用相似对象,结构上和多例模式、缓存、池化技术等类似都用到了工厂,但是设计意图却大相径庭,例如多例模式主要用于限制对象个数,缓存时提高对象访问效率,池化技术主要目的是节省时间。从这里也能看出,有些模型设计虽然结构非常类似,但是出发点却不同,这也印证了学习设计模式的出发点应为设计意图而非死记结构,最终结构只是设计的落地方式而已。
本文发布于:2024-01-29 02:37:34,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170646705912096.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |