Hôm nay, chúng ta sẽ cùng tìm hiểu về Flyweight design pattern trong Java (Mẫu thiết kế Flyweight)

Flyweight Design Pattern trong Java
Theo GoF (Gang of Four), mục đích của Flyweight design pattern là:
Sử dụng chia sẻ để hỗ trợ số lượng lớn các đối tượng dạng nhỏ một cách hiệu quả.
Flyweight là một mẫu thiết kế thuộc nhóm Structural design pattern, tương tự như Facade pattern, Adapter Pattern and Decorator pattern. Mẫu thiết kế này được sử dụng khi ta cần tạo rất nhiều đối tượng từ một lớp nào đó. Vì mỗi đối tượng đều chiếm dung lượng bộ nhớ, điều này có thể gây ảnh hưởng trên các thiết bị có bộ nhớ hạn chế như thiết bị di động hay hệ thống nhúng, Flyweight giúp giảm tải bộ nhớ bằng cách chia sẻ các đối tượng thay vì tạo mới hoàn toàn. Trước khi áp dụng Flyweight design pattern, bạn nên cân nhắc các yếu tố sau:
- Số lượng đối tượng cần tạo trong ứng dụng là rất lớn.
- Việc tạo đối tượng tiêu tốn nhiều bộ nhớ và có thể tốn thời gian.
- Các thuộc tính của đối tượng có thể chia thành intrinsic (nội tại) và extrinsic (ngoại tại). Trong đó, thuộc tính extrinsic nên được xác định bởi chương trình phía client.
Để áp dụng Flyweight pattern, ta cần chia thuộc tính của đối tượng thành intrinsic và extrinsic. Intrinsic properties là những thuộc tính cốt lõi, làm cho đối tượng là duy nhất. Extrinsic properties được thiết lập bởi client và dùng để thực hiện các thao tác khác nhau. Ví dụ, đối tượng Circle có thể có các thuộc tính extrinsic như màu sắc và chiều rộng. Khi áp dụng Flyweight, chúng ta cần tạo một Flyweight Factory để quản lý và trả về các đối tượng được chia sẻ. Trong ví dụ này, giả sử chúng ta cần vẽ một hình có chứa Line và Oval. Ta sẽ có một interface Shape cùng với các lớp triển khai cụ thể như Line và Oval. Lớp Oval sẽ có thuộc tính nội tại để xác định liệu có tô màu cho hình Oval với màu đã cho hay không, trong khi đó Line sẽ không có bất kỳ thuộc tính nội tại nào.
Flyweight Design Pattern Interface và các lớp cụ thể
Shape.java
package com.journaldev.design.flyweight;
import java.awt.Color;
import java.awt.Graphics;
public interface Shape {
public void draw(Graphics g, int x, int y, int width, int height,
Color color);
}
Line.java
ackage com.journaldev.design.flyweight;
import java.awt.Color;
import java.awt.Graphics;
public class Line implements Shape {
public Line(){
System.out.println("Creating Line object");
//adding time delay
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void draw(Graphics line, int x1, int y1, int x2, int y2,
Color color) {
line.setColor(color);
line.drawLine(x1, y1, x2, y2);
}
}
Oval.java
package com.journaldev.design.flyweight;
import java.awt.Color;
import java.awt.Graphics;
public class Oval implements Shape {
//intrinsic property
private boolean fill;
public Oval(boolean f){
this.fill=f;
System.out.println("Creating Oval object with fill="+f);
//adding time delay
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void draw(Graphics circle, int x, int y, int width, int height,
Color color) {
circle.setColor(color);
circle.drawOval(x, y, width, height);
if(fill){
circle.fillOval(x, y, width, height);
}
}
}
Lưu ý rằng tôi cố ý thêm độ trễ khi tạo đối tượng của các lớp cụ thể để minh họa rằng Flyweight rất hữu ích cho các đối tượng mất nhiều thời gian để khởi tạo.
Flyweight Factory
Factory Flyweight sẽ được sử dụng bởi các chương trình phía client để khởi tạo đối tượng, vì vậy chúng ta cần giữ một bản đồ (map) các đối tượng trong factory, và bản đồ này không nên được truy cập trực tiếp bởi ứng dụng phía client. Bất cứ khi nào chương trình phía client gọi để lấy một instance của đối tượng, nếu đối tượng đã tồn tại trong HashMap thì sẽ trả về đối tượng đó; nếu không, tạo một đối tượng mới, lưu vào Map rồi trả về. Chúng ta cần đảm bảo rằng mọi thuộc tính nội tại đều được xem xét khi tạo đối tượng. Lớp flyweight factory của chúng ta như sauShapeFactory.java
package com.journaldev.design.flyweight;
import java.util.HashMap;
public class ShapeFactory {
private static final HashMap<ShapeType,Shape> shapes = new HashMap<ShapeType,Shape>();
public static Shape getShape(ShapeType type) {
Shape shapeImpl = shapes.get(type);
if (shapeImpl == null) {
if (type.equals(ShapeType.OVAL_FILL)) {
shapeImpl = new Oval(true);
} else if (type.equals(ShapeType.OVAL_NOFILL)) {
shapeImpl = new Oval(false);
} else if (type.equals(ShapeType.LINE)) {
shapeImpl = new Line();
}
shapes.put(type, shapeImpl);
}
return shapeImpl;
}
public static enum ShapeType{
OVAL_FILL,OVAL_NOFILL,LINE;
}
}
Lưu ý việc sử dụng Enum trong Java để đảm bảo an toàn kiểu (type safety), sử dụng composition thông qua shapes map để lưu trữ các đối tượng, và áp dụng mẫu thiết kế Factory trong phương thức getShape để tạo hoặc tái sử dụng đối tượng một cách hiệu quả.
Ví dụ Flyweight Design Pattern cho khách hàng
Dưới đây là một chương trình mẫu sử dụng Flyweight pattern. DrawingClient.java
package com.journaldev.design.flyweight;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import com.journaldev.design.flyweight.ShapeFactory.ShapeType;
public class DrawingClient extends JFrame{
private static final long serialVersionUID = -1350200437285282550L;
private final int WIDTH;
private final int HEIGHT;
private static final ShapeType shapes[] = { ShapeType.LINE, ShapeType.OVAL_FILL,ShapeType.OVAL_NOFILL };
private static final Color colors[] = { Color.RED, Color.GREEN, Color.YELLOW };
public DrawingClient(int width, int height){
this.WIDTH=width;
this.HEIGHT=height;
Container contentPane = getContentPane();
JButton startButton = new JButton("Draw");
final JPanel panel = new JPanel();
contentPane.add(panel, BorderLayout.CENTER);
contentPane.add(startButton, BorderLayout.SOUTH);
setSize(WIDTH, HEIGHT);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
startButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Graphics g = panel.getGraphics();
for (int i = 0; i < 20; ++i) {
Shape shape = ShapeFactory.getShape(getRandomShape());
shape.draw(g, getRandomX(), getRandomY(), getRandomWidth(),
getRandomHeight(), getRandomColor());
}
}
});
}
private ShapeType getRandomShape() {
return shapes[(int) (Math.random() * shapes.length)];
}
private int getRandomX() {
return (int) (Math.random() * WIDTH);
}
private int getRandomY() {
return (int) (Math.random() * HEIGHT);
}
private int getRandomWidth() {
return (int) (Math.random() * (WIDTH / 10));
}
private int getRandomHeight() {
return (int) (Math.random() * (HEIGHT / 10));
}
private Color getRandomColor() {
return colors[(int) (Math.random() * colors.length)];
}
public static void main(String[] args) {
DrawingClient drawing = new DrawingClient(500,600);
}
}
Tôi đã dùng sinh số ngẫu nhiên để tạo nhiều loại hình dạng khác nhau trong khung hình. Khi bạn chạy chương trình client này, bạn sẽ thấy có độ trễ khi tạo lần đầu các đối tượng Line và Oval với các giá trị fill khác nhau (true/false). Sau đó chương trình chạy nhanh hơn vì đã dùng lại các đối tượng được chia sẻ. Khi bạn nhấn nút “Draw” nhiều lần, khung hình sẽ trông như hình bên dưới.

Bạn cũng sẽ thấy output như sau trong dòng lệnh, xác nhận rằng các đối tượng đã được chia sẻ.
Creating Line object
Creating Oval object with fill=true
Creating Oval object with fill=false
Đó là tất cả về mẫu thiết kế Flyweight, chúng ta sẽ tìm hiểu thêm các mẫu thiết kế khác trong những bài viết sau. Nếu bạn thấy hay, hãy để lại ý kiến ở phần bình luận và chia sẻ cho mọi người cùng biết nhé.
Tìm hiểu thêm: Những design pattern thông dụng trong Java và ví dụ
Ví dụ về mẫu thiết kế Flyweight trong JDK (Java Development Kit):
Tất cả các lớp wrapper trong Java sử dụng đối tượng được cache sẵn trong phương thức valueOf(), thể hiện việc áp dụng Flyweight design pattern. Ví dụ điển hình nhất là String Pool trong lớp String của Java.
Những diểm quan trọng của Flyweight design pattern
- Trong ví dụ của chúng ta, mã phía client không bị bắt buộc phải tạo đối tượng thông qua Flyweight factory, nhưng chúng ta hoàn toàn có thể ép buộc điều đó để đảm bảo mã client sử dụng đúng cách triển khai theo mẫu Flyweight. Tuy nhiên, đây hoàn toàn là một quyết định thiết kế phụ thuộc vào từng ứng dụng cụ thể.
- Flyweight pattern có thể tăng độ phức tạp, và nếu số lượng đối tượng chia sẻ quá lớn, sẽ xảy ra trade-off giữa bộ nhớ và thời gian. Vì vậy, chúng ta cần sử dụng mẫu này một cách hợp lý, dựa trên nhu cầu cụ thể của ứng dụng.
- Việc triển khai mẫu thiết kế Flyweight không còn hiệu quả khi một đối tượng có quá nhiều thuộc tính nội tại , vì điều này sẽ khiến việc xây dựng lớp Factory trở nên phức tạp và khó quản lý.
Đó là tất cả về Flyweight Design Pattern trong Java.