Flyweight Pattern
1. Giới Thiệu
1.1 Định Nghĩa
Flyweight Pattern là một structural pattern cho phép fit nhiều đối tượng hơn vào RAM bằng cách chia sẻ các phần trạng thái chung giữa nhiều đối tượng thay vì lưu trữ tất cả dữ liệu trong mỗi đối tượng.
1.2 Mục Đích
- Giảm số lượng đối tượng được tạo
- Giảm bộ nhớ và chi phí lưu trữ
- Tăng hiệu suất ứng dụng
- Chia sẻ state giữa nhiều đối tượng
2. Cấu Trúc Cơ Bản
2.1 Text Formatting Example
// Flyweight interface
public interface TextFormat {
void apply(String text);
}
// Concrete Flyweight
public class FontFormat implements TextFormat {
private final String fontName;
private final int fontSize;
private final String color;
public FontFormat(String fontName,
int fontSize, String color) {
this.fontName = fontName;
this.fontSize = fontSize;
this.color = color;
}
@Override
public void apply(String text) {
System.out.println("Applying format: " +
fontName + ", " + fontSize + "pt, " +
color + " to text: " + text);
}
}
// Flyweight Factory
public class TextFormatFactory {
private static final Map<String, TextFormat> formats =
new HashMap<>();
public static TextFormat getFormat(
String fontName, int fontSize, String color) {
String key = fontName + "_" + fontSize + "_" + color;
return formats.computeIfAbsent(key, k ->
new FontFormat(fontName, fontSize, color));
}
public static int getFormatsCount() {
return formats.size();
}
}
// Client code
public class TextEditor {
public void formatText(String text,
String fontName, int fontSize, String color) {
TextFormat format = TextFormatFactory.getFormat(
fontName, fontSize, color);
format.apply(text);
}
}
3. Ví Dụ Thực Tế
3.1 Game Character System
// Intrinsic state (shared)
public class CharacterProperties {
private final String type;
private final BufferedImage sprite;
private final int baseHealth;
private final int baseSpeed;
public CharacterProperties(String type,
String spritePath,
int baseHealth,
int baseSpeed) {
this.type = type;
this.sprite = loadSprite(spritePath);
this.baseHealth = baseHealth;
this.baseSpeed = baseSpeed;
}
private BufferedImage loadSprite(String path) {
// Load sprite image
return null; // Simplified
}
// Getters
}
// Flyweight Factory
public class CharacterFactory {
private static final Map<String, CharacterProperties>
properties = new HashMap<>();
public static CharacterProperties getProperties(
String type) {
return properties.computeIfAbsent(
type, CharacterFactory::createProperties);
}
private static CharacterProperties createProperties(
String type) {
switch (type) {
case "warrior":
return new CharacterProperties(
"warrior",
"warrior.png",
100,
5);
case "mage":
return new CharacterProperties(
"mage",
"mage.png",
70,
7);
default:
throw new IllegalArgumentException(
"Unknown character type");
}
}
}
// Context class (extrinsic state)
public class GameCharacter {
private CharacterProperties properties;
private int x;
private int y;
private int currentHealth;
public GameCharacter(String type, int x, int y) {
this.properties = CharacterFactory
.getProperties(type);
this.x = x;
this.y = y;
this.currentHealth = properties.getBaseHealth();
}
public void move(int dx, int dy) {
this.x += dx * properties.getBaseSpeed();
this.y += dy * properties.getBaseSpeed();
}
public void draw(Graphics g) {
g.drawImage(properties.getSprite(), x, y, null);
}
}
3.2 Particle System
// Intrinsic state
public class ParticleType {
private final BufferedImage texture;
private final int lifetime;
private final Color baseColor;
public ParticleType(String texturePath,
int lifetime, Color baseColor) {
this.texture = loadTexture(texturePath);
this.lifetime = lifetime;
this.baseColor = baseColor;
}
private BufferedImage loadTexture(String path) {
// Load texture
return null; // Simplified
}
// Getters
}
// Flyweight Factory
public class ParticleTypeFactory {
private static final Map<String, ParticleType> types =
new ConcurrentHashMap<>();
public static ParticleType getParticleType(
String name) {
return types.computeIfAbsent(
name, ParticleTypeFactory::createType);
}
private static ParticleType createType(String name) {
switch (name) {
case "fire":
return new ParticleType(
"fire.png",
1000,
Color.ORANGE);
case "smoke":
return new ParticleType(
"smoke.png",
2000,
Color.GRAY);
default:
throw new IllegalArgumentException(
"Unknown particle type");
}
}
}
// Context class
public class Particle {
private ParticleType type;
private Vector2D position;
private Vector2D velocity;
private float scale;
private float alpha;
private int currentLife;
public Particle(String typeName,
Vector2D position,
Vector2D velocity) {
this.type = ParticleTypeFactory
.getParticleType(typeName);
this.position = position;
this.velocity = velocity;
this.scale = 1.0f;
this.alpha = 1.0f;
this.currentLife = type.getLifetime();
}
public void update(float deltaTime) {
position = position.add(
velocity.multiply(deltaTime));
currentLife -= deltaTime;
alpha = (float) currentLife / type.getLifetime();
}
public void render(Graphics2D g) {
// Render particle with current position,
// scale, and alpha
}
}
4. Spring Framework Integration
4.1 Configuration
@Configuration
public class FlyweightConfig {
@Bean
public TextFormatFactory textFormatFactory() {
return new TextFormatFactory();
}
@Bean
public CharacterFactory characterFactory() {
return new CharacterFactory();
}
}
4.2 Service Implementation
@Service
public class GameService {
private final CharacterFactory characterFactory;
private final List<GameCharacter> characters =
new ArrayList<>();
public GameService(CharacterFactory characterFactory) {
this.characterFactory = characterFactory;
}
public GameCharacter createCharacter(
String type, int x, int y) {
GameCharacter character = new GameCharacter(
type, x, y);
characters.add(character);
return character;
}
public void updateAll(float deltaTime) {
characters.forEach(character ->
character.update(deltaTime));
}
}
@Service
public class ParticleService {
private final ParticleSystem particleSystem;
public ParticleService() {
this.particleSystem = new ParticleSystem(1000);
}
public void emitParticles(
String type,
Vector2D position,
int count) {
for (int i = 0; i < count; i++) {
Vector2D velocity = generateRandomVelocity();
particleSystem.emit(type, position, velocity);
}
}
public void update(float deltaTime) {
particleSystem.update(deltaTime);
}
}
5. Testing Flyweight Pattern
5.1 Unit Testing
@ExtendWith(MockitoExtension.class)
class TextFormatFactoryTest {
@Test
void whenGetSameFormat_thenReturnSameInstance() {
// When
TextFormat format1 = TextFormatFactory.getFormat(
"Arial", 12, "black");
TextFormat format2 = TextFormatFactory.getFormat(
"Arial", 12, "black");
// Then
assertSame(format1, format2);
assertEquals(1, TextFormatFactory.getFormatsCount());
}
@Test
void whenGetDifferentFormats_thenCreateNewInstances() {
// When
TextFormat format1 = TextFormatFactory.getFormat(
"Arial", 12, "black");
TextFormat format2 = TextFormatFactory.getFormat(
"Times", 14, "red");
// Then
assertNotSame(format1, format2);
assertEquals(2, TextFormatFactory.getFormatsCount());
}
}
5.2 Performance Testing
@Test
void testMemoryUsage() {
// Given
int characterCount = 1000000;
Runtime runtime = Runtime.getRuntime();
long memoryBefore = runtime.totalMemory() -
runtime.freeMemory();
// When
List<GameCharacter> characters = new ArrayList<>();
for (int i = 0; i < characterCount; i++) {
characters.add(new GameCharacter(
"warrior", i % 1000, i % 1000));
}
// Then
long memoryAfter = runtime.totalMemory() -
runtime.freeMemory();
long memoryUsed = memoryAfter - memoryBefore;
System.out.println("Memory used: " +
memoryUsed / 1024 / 1024 + " MB");
assertEquals(2, CharacterFactory.getTypesCount());
}
6. Lợi Ích và Nhược Điểm
6.1 Lợi Ích
- Tiết kiệm bộ nhớ đáng kể
- Cải thiện hiệu suất
- Giảm số lượng đối tượng
- Tái sử dụng đối tượng hiệu quả
- Thread-safe khi triển khai đúng
6.2 Nhược Điểm
- Tăng độ phức tạp của code
- Khó debug và maintain
- Có thể ảnh hưởng đến hiệu suất nếu factory quá phức tạp
- Khó xử lý state thay đổi
7. Best Practices
7.1 Thread Safety
public class ThreadSafeParticleFactory {
private static final ConcurrentMap<String, ParticleType>
types = new ConcurrentHashMap<>();
public static ParticleType getParticleType(
String name) {
return types.computeIfAbsent(
name, ThreadSafeParticleFactory::createType);
}
private static ParticleType createType(String name) {
// Create particle type
return new ParticleType(/* params */);
}
}
7.2 Memory Management
public class CacheableCharacterFactory {
private static final int MAX_CACHE_SIZE = 100;
private static final Map<String, SoftReference<CharacterProperties>>
cache = new LinkedHashMap<String, SoftReference<CharacterProperties>>(
MAX_CACHE_SIZE + 1, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<String, SoftReference<CharacterProperties>> eldest) {
return size() > MAX_CACHE_SIZE;
}
};
public static CharacterProperties getProperties(
String type) {
SoftReference<CharacterProperties> ref =
cache.get(type);
CharacterProperties props =
(ref != null) ? ref.get() : null;
if (props == null) {
props = createProperties(type);
cache.put(type,
new SoftReference<>(props));
}
return props;
}
}
8. Common Issues và Solutions
8.1 State Management
public class StatefulParticle {
private final ParticleType type;
private final StateManager stateManager;
public StatefulParticle(
String typeName, StateManager stateManager) {
this.type = ParticleTypeFactory
.getParticleType(typeName);
this.stateManager = stateManager;
}
public void update() {
ParticleState state = stateManager
.getState(this);
updateState(state);
stateManager.setState(this, state);
}
}
public class StateManager {
private final Map<Particle, ParticleState> states =
new WeakHashMap<>();
public ParticleState getState(Particle particle) {
return states.computeIfAbsent(
particle, p -> new ParticleState());
}
public void setState(
Particle particle, ParticleState state) {
states.put(particle, state);
}
}
8.2 Resource Loading
public class LazyLoadingTextureFactory {
private static final Map<String, Future<Texture>>
textures = new ConcurrentHashMap<>();
private static final ExecutorService executor =
Executors.newFixedThreadPool(2);
public static Future<Texture> getTexture(
String path) {
return textures.computeIfAbsent(
path, p -> executor.submit(() ->
loadTexture(p)));
}
private static Texture loadTexture(String path) {
// Load texture asynchronously
return new Texture(path);
}
}
9. References
9.1 Books
- Design Patterns: Elements of Reusable Object-Oriented Software
- Head First Design Patterns
- Clean Architecture by Robert C. Martin
- Patterns of Enterprise Application Architecture