俄罗斯方块想必大家都不陌生,从小玩到大的,如果让你设计一款俄罗斯方块游戏,那么该如何下手呢?今天咱们只讨论如何创建各种图形,那么开始吧
参考上面的俄罗斯方块类,每个方块有不同的形状shap对象以及不同的坐标station对象。由于方块的形状是有限的,如果每个方块对象都需要一个新的shap对象,如果屏幕足够大,那么内存占用会比较大,造成了资源的浪费。
分析一下,每个方块的形状都是有限的,既然如此,是不是可以考虑共享shap对象呢?而坐标对于每个方块来说都是唯一的,因此无法共享。
import java.util.HashMap;
import java.util.Map;
// 形状类
class Shape {
private final String shapeName;
public Shape(String shapeName) {
this.shapeName = shapeName;
}
public String getShapeName() {
return shapeName;
}
}
// 坐标类
class Position {
private final int x;
private final int y;
public Position(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
// 俄罗斯方块类
class TetrisBlock {
private final Shape shape; // 内部状态:形状
private final Position position; // 外部状态:坐标
public TetrisBlock(Shape shape, Position position) {
this.shape = shape;
this.position = position;
}
public void draw() {
System.out.println("绘制了一个" + shape.getShapeName() + "形状的俄罗斯方块,坐标为:" + position.getX() + "," + position.getY());
}
}
// 享元工厂类
class TetrisBlockFactory {
// 缓存形状对象的hashMap
private static final Map<String, Shape> shapeMap = new HashMap<>();
public static TetrisBlock getTetrisBlock(String shapeName, int x, int y) {
// 先查看是否已经存在对应形状的对象,如果存在则直接返回
Shape shape = shapeMap.get(shapeName);
if (shape == null) {
// 如果不存在,则创建新的形状对象,并将其存储在 map 中
shape = new Shape(shapeName);
shapeMap.put(shapeName, shape);
System.out.println("创建了一个" + shapeName + "形状的俄罗斯方块");
}
Position position = new Position(x, y);
return new TetrisBlock(shape, position);
}
}
// 客户端代码
public class Main {
public static void main(String[] args) {
// 获取不同形状的俄罗斯方块对象,并设置不同的坐标位置
TetrisBlock block1 = TetrisBlockFactory.getTetrisBlock("L形", 10, 10);
TetrisBlock block2 = TetrisBlockFactory.getTetrisBlock("T形", 20, 20);
TetrisBlock block3 = TetrisBlockFactory.getTetrisBlock("I形", 30, 30);
// 再次获取相同形状的俄罗斯方块对象,但坐标位置不同,此时形状对象可以复用
TetrisBlock block4 = TetrisBlockFactory.getTetrisBlock("L形", 40, 40);
TetrisBlock block5 = TetrisBlockFactory.getTetrisBlock("T形", 50, 50);
// 绘制俄罗斯方块
block1.draw();
block2.draw();
block3.draw();
block4.draw();
block5.draw();
}
}
通过上面的例子可以发现,享元模式实际上是缓存了可以重用的部分,在本例中就是Shap对象,这种情况下主要是减少形状对象(Shap)的创建,从而减少了内存的消耗。
鉴别是否可以用享元模式,需要确认可共享的部分是有限的并且大量重复出现的。在本例中形状(Shap)对象就符合这种特征,而位置(Sataion)对象则不符合。同时,如果共享的部分是唯一的,那么直接使用单例就可以,也就没有使用享元的必要了。
其次,共享部分通常被设计为不可变的,假设某个共享对象被多个客户端同时引用,其中一个客户端修改了共享部分的状态,这样会影响到其他客户端引用该共享对象的状态,从而导致不确定的行为。在本例子中,Shap通过构造函数参数初始化其状态一次,它没有向其他对象公开任何 setter 或公共字段。
享元模式的使用场景:
1 大量对象的重复使用:当系统中存在大量相似对象,且这些对象的状态可以被外部化时,可以考虑使用享元模式。通过共享相同的对象实例,可以减少内存的消耗,提高系统的性能。
2 对象的状态可以被分为内部状态和外部状态:享元模式通过将对象的内部状态和外部状态分离,可以在一定程度上减少对象的数量。内部状态是对象可以共享的状态,而外部状态是对象不同的变化部分。通过在享元对象中共享内部状态,并在需要时传递外部状态,可以实现对象的共享。
3 需要缓存的对象:享元模式常用于需要缓存对象的情况,或者对象的创建和销毁代价较高时,例如数据库连接池、线程池等。通过共享已经创建的对象实例,可以避免频繁地创建和销毁对象,从而提高系统的性能和资源利用率。这种方式比直接缓存对象来说能够更加精细和灵活的控制需要缓存的共享部分和可变部分,避免缓存整个对象
参考:Flyweight (refactoring.guru)
设计模式总结文章(一边学习一边更新中)