close up photo of programming of codes

codecungnhau.com

Một trang web về kỹ thuật lập trình

Design Patterns: Flyweight

Mục tiêu

Flyweight là một mẫu thiết kế cấu trúc cho phép bạn lắp nhiều đối tượng hơn vào dung lượng RAM có sẵn bằng cách chia sẻ các phần trạng thái chung giữa nhiều đối tượng thay vì giữ tất cả dữ liệu trong mỗi đối tượng.

Đặt vấn đề

Để giải trí sau những giờ làm việc dài, bạn quyết định tạo một trò chơi điện tử đơn giản: người chơi sẽ di chuyển quanh bản đồ và bắn lẫn nhau. Bạn đã chọn triển khai một hệ thống hạt (particle system) thực tế và biến nó thành một tính năng đặc biệt của trò chơi. Số lượng lớn đạn, tên lửa và mảnh đạn từ các vụ nổ sẽ bay khắp bản đồ và mang lại trải nghiệm ly kỳ cho người chơi.

Sau khi hoàn thành, bạn đã push commit cuối cùng, built trò chơi và gửi nó cho bạn bè của bạn để kiểm thử. Mặc dù trò chơi đang chạy hoàn hảo trên máy của bạn, nhưng bạn của bạn không thể chơi lâu. Trên máy tính của anh ấy, trò chơi liên tục bị lỗi sau vài phút chơi. Sau khi dành vài giờ để tìm kiếm gỡ lỗi, bạn phát hiện ra rằng trò chơi bị lỗi do không đủ dung lượng RAM. Hóa ra là thiết bị của anh ta kém hơn nhiều so với máy tính của bạn và đó là lý do tại sao vấn đề xuất hiện rất nhanh trên máy của anh ấy.

Vấn đề thực tế liên quan đến hệ thống hạt của bạn. Mỗi hạt, chẳng hạn như một viên đạn, một tên lửa hoặc một mảnh đạn được thể hiện bằng một đối tượng riêng biệt chứa nhiều dữ liệu. Tại một số thời điểm, khi sự tàn sát trên màn hình của người chơi lên đến đỉnh điểm, các hạt mới được tạo ra không còn đủ với bộ nhớ RAM còn lại, do đó, chương trình bị lỗi.

Giải pháp

Khi kiểm tra kỹ hơn lớp Particle, bạn có thể nhận thấy rằng các trường màu và sprite tiêu tốn nhiều bộ nhớ hơn các trường khác. Điều tồi tệ hơn là hai trường này lưu trữ dữ liệu gần như giống hệt nhau trên tất cả các hạt. Ví dụ, tất cả các viên đạn có cùng màu và sprite.

Các phần khác của trạng thái hạt, chẳng hạn như tọa độ, vectơ chuyển động và tốc độ, là duy nhất cho mỗi hạt. Rốt cuộc, giá trị của các trường này thay đổi theo thời gian. Dữ liệu này đại diện cho bối cảnh luôn thay đổi trong đó hạt tồn tại, trong khi màu sắc và sprite không đổi đối với mỗi hạt.

Dữ liệu không đổi này của một đối tượng thường được gọi là trạng thái nội tại (intrinsic). Nó sống bên trong đối tượng; các đối tượng khác chỉ có thể đọc nó, không thay đổi nó. Phần còn lại của trạng thái của đối tượng, thường bị thay đổi “từ bên ngoài” bởi các đối tượng khác, được gọi là trạng thái ngoại vi (extrinsic).

Mẫu Flyweight đề xuất rằng bạn ngừng lưu trữ trạng thái bên ngoài bên trong đối tượng. Thay vào đó, bạn nên chuyển trạng thái này cho các phương thức cụ thể dựa vào nó. Chỉ trạng thái nội tại nằm trong đối tượng, cho phép bạn sử dụng lại nó trong các ngữ cảnh khác nhau. Do đó, bạn sẽ cần ít đối tượng này hơn vì chúng chỉ khác nhau về trạng thái nội tại, có ít biến thể hơn nhiều so với ngoại vi.

Hãy quay lại trò chơi của chúng ta. Giả sử rằng chúng ta đã trích xuất trạng thái bên ngoài từ lớp Particle của mình, chỉ cần ba vật thể khác nhau là đủ để đại diện cho tất cả các hạt trong trò chơi: một viên đạn, một tên lửa và một mảnh đạn. Như bạn có thể đã đoán ra, một vật thể chỉ lưu trữ trạng thái bên trong được gọi là flyweight.

Lưu trữ trạng thái bên ngoài

Trạng thái bên ngoài di chuyển đến đâu? Một số lớp vẫn nên lưu trữ nó, phải không? Trong hầu hết các trường hợp, nó được chuyển đến đối tượng container, đối tượng này tổng hợp các đối tượng trước khi chúng ta áp dụng mẫu.

Trong trường hợp của chúng ta, đó là đối tượng Game lưu trữ tất cả các hạt trong trường hạt. Để chuyển trạng thái bên ngoài vào lớp này, bạn cần tạo một số trường mảng để lưu trữ tọa độ, vectơ và tốc độ của từng hạt riêng lẻ. Nhưng đó không phải là tất cả. Bạn cần một mảng khác để lưu trữ các tham chiếu đến một flyweight cụ thể đại diện cho một hạt. Các mảng này phải được đồng bộ để bạn có thể truy cập tất cả dữ liệu của một hạt bằng cách sử dụng cùng một chỉ mục.

Một giải pháp thanh lịch hơn là tạo một lớp ngữ cảnh riêng biệt sẽ lưu trữ trạng thái bên ngoài cùng với tham chiếu đến đối tượng flyweight. Cách tiếp cận này sẽ yêu cầu chỉ có một mảng duy nhất trong lớp container.

Đợi một chút! Chúng ta chẳng cần phải có nhiều đối tượng ngữ cảnh này như chúng ta đã có ngay từ đầu sao? Về mặt kỹ thuật là đúng. Nhưng có điều, những vật thể này nhỏ hơn trước rất nhiều. Các trường sử dụng nhiều bộ nhớ nhất đã được chuyển đến chỉ một vài đối tượng flyweight. Giờ đây, một nghìn đối tượng ngữ cảnh nhỏ có thể tái sử dụng một đối tượng flyweight hạng nặng duy nhất thay vì lưu trữ một nghìn bản sao dữ liệu của nó.

Flyweight và tính bất biến

Vì cùng một đối tượng flyweight có thể được sử dụng trong các ngữ cảnh khác nhau, bạn phải đảm bảo rằng trạng thái của nó không thể sửa đổi được. Một flyweight nên khởi tạo trạng thái của nó chỉ một lần, thông qua các tham số của hàm tạo. Nó không được để lộ bất kỳ bộ cài đặt hoặc trường công khai nào cho các đối tượng khác.

Flyweight factory

Để truy cập thuận tiện hơn vào các loại flyweight khác nhau, bạn có thể tạo một phương thức factory để quản lý một nhóm các đối tượng flyweight hiện có. Phương thức chấp nhận trạng thái nội tại của flyweight mong muốn từ client, tìm kiếm một đối tượng flyweight hiện có phù hợp với trạng thái này và trả về nếu nó được tìm thấy. Nếu không, nó tạo ra một flyweight mới và thêm nó vào pool.

Có một số tùy chọn mà phương pháp này có thể được đặt vào. Nơi dễ thấy nhất là flyweight container. Ngoài ra, bạn có thể tạo một lớp factory mới. Hoặc bạn có thể làm cho phương thức factory tĩnh và đặt nó bên trong một lớp flyweight thực tế.

Cấu trúc

  1. Mẫu Flyweight chỉ đơn thuần là một sự tối ưu hóa. Trước khi áp dụng nó, hãy đảm bảo rằng chương trình của bạn có vấn đề tiêu thụ RAM liên quan đến việc có một số lượng lớn các đối tượng tương tự trong bộ nhớ cùng một lúc. Đảm bảo rằng vấn đề này không thể được giải quyết theo bất kỳ cách nào có ý nghĩa khác.
  2. Lớp Flyweight chứa một phần trạng thái của đối tượng ban đầu có thể được chia sẻ giữa nhiều đối tượng. Cùng một đối tượng flyweight có thể được sử dụng trong nhiều ngữ cảnh khác nhau. Trạng thái được lưu trữ bên trong flyweight được gọi là nội tại. Trạng thái được chuyển cho các phương thức của flyweight được gọi là ngoại vi.
  3. Lớp Context chứa trạng thái ngoại vi, duy nhất trên tất cả các đối tượng gốc. Khi một ngữ cảnh được ghép nối với một trong các đối tượng flyweight, nó thể hiện trạng thái đầy đủ của đối tượng ban đầu.
  4. Thông thường, hành vi của đối tượng ban đầu vẫn ở lớp flyweight. Trong trường hợp này, bất kỳ ai gọi phương thức của flyweight cũng phải chuyển các bit thích hợp của trạng thái ngoại vi vào các tham số của phương thức. Mặt khác, hành vi có thể được chuyển sang lớp Context, lớp này sẽ sử dụng flyweight được liên kết đơn thuần như một đối tượng dữ liệu.
  5. Client tính toán hoặc lưu trữ trạng thái ngoại vi của flyweight. Từ quan điểm của client, flyweight là một đối tượng mẫu có thể được định cấu hình trong thời gian chạy bằng cách truyền một số dữ liệu ngữ cảnh vào trong các tham số của các phương thức của nó.
  6. Flyweight Factory quản lý một nhóm các flyweight hiện có. Với factory, client không trực tiếp tạo ra trọng lượng bay. Thay vào đó, chúng gọi factory, chuyển cho nó các bit về trạng thái nội tại của flyweight mong muốn. Factory xem xét các flyweight đã tạo trước đó và trả về một flyweight hiện có phù hợp với tiêu chí tìm kiếm hoặc tạo một flyweight mới nếu không tìm thấy gì.

Khả năng áp dụng

Chỉ sử dụng mẫu Flyweight khi chương trình của bạn phải hỗ trợ một số lượng lớn các đối tượng hầu như không vừa với RAM có sẵn.

Ưu và nhược điểm

😄😄😄

Có thể tiết kiệm rất nhiều RAM, nếu chương trình có rất nhiều đối tượng giống nhau.

🙁🙁🙁

Có thể phải tính toán lại RAM qua các chu kỳ CPU khi một số dữ liệu ngữ cảnh cần được tính toán lại mỗi khi ai đó gọi phương thức flyweight.
Mã trở nên phức tạp hơn nhiều. Các thành viên mới trong nhóm sẽ luôn thắc mắc tại sao trạng thái của một thực thể lại được tách ra theo cách như vậy.

Mối quan hệ với các mẫu khác

Bạn có thể triển khai các nút lá chia sẻ của cây Composite dưới dạng Flyweight để tiết kiệm RAM.

Flyweight cho thấy cách tạo nhiều đối tượng nhỏ, trong khi Facade cho thấy cách tạo một đối tượng duy nhất đại diện cho toàn bộ hệ thống con.

Flyweight sẽ giống với Singleton nếu bằng cách nào đó bạn có thể giảm tất cả các trạng thái được chia sẻ của các đối tượng xuống chỉ còn một đối tượng flyweight. Nhưng có hai điểm khác biệt cơ bản giữa các mẫu này:

  • Chỉ nên có một cá thể Singleton, trong khi Flyweight có thể có nhiều cá thể với các trạng thái nội tại khác nhau.
  • Đối tượng Singleton có thể thay đổi được. Đối tượng Flyweight là bất biến.

Chương trình tham khảo

C++

C#

Python


Đã đăng vào

trong

bởi

Bình luận

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *