You are here
Home > java > Core Java >

Flyweight Design Pattern With Examples Using Java 21

Flyweight Design PatternFlyweight Design Pattern in Java: A Comprehensive Guide

The Flyweight Design Pattern is one of the structural design patterns introduced by the Gang of Four (GoF). It focuses on minimizing memory usage and improving performance by sharing as much data as possible with other similar objects. This pattern is particularly useful in applications where a large number of objects with similar characteristics are required.

In this article, we will dive deep into the Flyweight pattern, understand its components, explore its advantages and limitations, and look at real-world use cases. We will also implement the pattern in Java with clear, concise examples to solidify the concepts.

Table of Contents

What is the Flyweight Pattern?

The Flyweight Pattern is designed to reduce the number of objects created and decrease memory usage by sharing objects. It optimizes memory usage by sharing a common state among multiple objects. This allows objects to be reused when dealing with a large number of similar objects, instead of creating new ones.

It separates the intrinsic state (shared, immutable state) from the extrinsic state (unique, contextual state) of an object.

What is intrinsic state and extrinsic state in the Flyweight Pattern?

Please note that these are the most important key concepts & backbone of the Flyweight design pattern. Therefore, it becomes crucial to understand them clearly.

The words intrinsic & extrinsic are made by applying prefixes ‘in’ & ‘ex’ in the word ‘trinsic’. However, it is not a word by itself, we can interpret “trinsic” as relating to the qualities or attributes of an object, with a prefix (like “in-” or “ex-“) determining whether those attributes are internal (intrinsic) or external (extrinsic).

  • Intrinsic State: Shared, constant data that is stored in the flyweight object.

  • Extrinsic State: Context-specific, dynamic data that is supplied externally.

Intrinsic State

  • Definition:
    • Intrinsic state is the shared, constant, or unchanging information stored inside a flyweight object.
    • It is independent of the context where the object is used and remains same across all instances.
  • Example: Imagine we are designing a library system. Each book in the library has fixed details like its title, author, and ISBN number. A book titled “Effective Java” by Joshua Bloch has an ISBN of “9780134685991”. These details are constant, no matter who borrows the book.
    • Intrinsic State: Title, author, ISBN number.
  • In Programming: Intrinsic state is stored within the flyweight object and is shared across multiple instances.

Extrinsic State

  • Definition:
    • Extrinsic state is the context-dependent, dynamic information that varies depending on how or where the flyweight object is used.
    • It is passed or provided by the client rather than stored within the flyweight object.
  • Example: Continuing with the library system analogy, the borrower’s name, due date, and return status of a book will not have the fixed values.
    • Extrinsic State: Borrower’s name, due date, and return status
  • In Programming: Extrinsic state is passed to the flyweight object as a parameter during runtime.

Other Examples of intrinsic state and extrinsic state

Example#1: Hotel Room Management

Imagine a hotel booking system. Each room has fixed attributes like its room type (e.g., “Deluxe Room”, “Standard Room”), bed count, and daily rate.

For example: A “Deluxe Room” with two queen-sized beds costs $100 per night. These details remain the same, no matter who books the room.

  • Intrinsic State: Room type, bed count, daily rate
  • Extrinsic State: Guest name, booking dates, and any special requests (e.g., extra pillows)

Example#2: Car Rental Service

Suppose you are managing a car rental service. Each car model has fixed details like its make, model, fuel type, and base rental rate per day.

For example: A “Toyota Corolla” with a gasoline engine costs $20 per day to rent. These details don’t vary, regardless of who rents the car.

  • Intrinsic State: Car make, model, fuel type, and base rental rate
  • Extrinsic State: Customer name, rental dates, and additional options (e.g., GPS, child seat)

Example#3: Coffee Shop Menu

Imagine a coffee shop. Each drink on the menu has fixed details like its name, ingredients, and base price.

For example: A “Cappuccino” is made with espresso, steamed milk, and foam, and costs $5. These details are consistent, no matter who orders the drink.

  • Intrinsic State: Drink name, ingredients, base price
  • Extrinsic State: Customer name, size (small, medium, large), and add-ons (e.g., extra shot, almond milk)

Example#4: Taxi Booking System

Let’s consider designing a taxi booking system. Each type of ride has fixed details like its ride type (e.g., “Economy”, “Luxury”), base fare, and per kilometer rate.

For example: An “Economy” ride has a base fare of $3 and costs $1 per kilometer. These details are constant, no matter who books the ride.

  • Intrinsic State: Ride type, base fare, per kilometer rate
  • Extrinsic State: Passenger name, pickup location, destination, and trip duration

Example#5: Theme Park Ticketing

Imagine you are managing a theme park ticketing system. Each ticket type has fixed details like the type of ticket (e.g., “Adult Pass”, “Child Pass”), price, and validity duration.

For example: An “Adult Pass” costs $40 and is valid for a single day. These details don’t change, no matter who buys the ticket.

  • Intrinsic State: Ticket type, price, validity duration
  • Extrinsic State: Ticket holder’s name, the date of visit, and any applied discounts

How does the Flyweight Pattern work?

In the Flyweight pattern, the concepts of intrinsic state and extrinsic state are key to optimizing resource usage. Objects are separated into intrinsic data and extrinsic data. The intrinsic data is stored in a central place and reused, while the extrinsic data is kept separately. In this way, it optimizes memory usage by reusing an intrinsic data among multiple objects and passing the extrinsic data on demand.

Components of the Flyweight Pattern

  1. Flyweight Interface:
    Defines the common interface for objects that can be shared.
  2. Concrete Flyweight:
    Implements the Flyweight interface and represents shared data (intrinsic state).
  3. Unshared Concrete Flyweight:
    Represents objects that are not shared but may work in conjunction with shared objects.
  4. Flyweight Factory:
    Ensures that Flyweight objects are shared. It checks if an existing object is available; if not, it creates one and stores it for future use.
  5. Client:
    Maintains references to Flyweight objects and provides the extrinsic state required for their operation.

Implementation of Flyweight Design Pattern in Java

The Flyweight pattern is used to minimize memory usage by sharing objects with similar intrinsic states. Let’s consider the use-case of a Library System:

  • Intrinsic State: Shared attributes that are independent of the object’s context (e.g., Title, Author, ISBN Number of a book).
  • Extrinsic State: Attributes that depend on the object’s context and are not shared (e.g., Borrower’s Name, Due Date, and Return Status).

Let’s implement the Flyweight Pattern step by step in Java.


Step#1: Define the Flyweight Interface

The flyweight interface declares common methods that will be used by concrete flyweight objects. It declares a method to perform an operation, accepting the extrinsic state as a parameter.

public interface Book {

   void displayDetails(BorrowDetails borrowDetails);
}

Step#2: Define the Extrinsic State Class

The extrinsic state (BorrowDetails) contains contextual information that changes for each use. This is an optional step, but we are including it separately to follow the Single Responsibility Principle. Some people combine the fields of this class with Concrete Flyweight class.

It contains contextual data specific to the object instance (e.g., borrower name, due date, return status). Passed dynamically to the displayDetails() method.

public class BorrowDetails {

    private final String borrowerName; // Extrinsic State
    private final String dueDate; // Extrinsic State
    private final boolean isReturned; // Extrinsic State

    public BorrowDetails(String borrowerName, String dueDate, boolean isReturned) {
        this.borrowerName = borrowerName;
        this.dueDate = dueDate;
        this.isReturned = isReturned;
    }

    public String getBorrowerName() {
        return borrowerName;
    }

    public String getDueDate() {
        return dueDate;
    }

    public boolean isReturned() {
        return isReturned;
    }
}

Step#3: Create the Concrete Flyweight Class

The Concrete Flyweight implements the Flyweight interface. It contains the intrinsic state, which can be shared. Shared data (e.g., title, author, ISBN) is stored in the flyweight object.

public class BookImpl implements Book {

    private final String title; // Intrinsic State
    private final String author; // Intrinsic State
    private final String isbn; // Intrinsic State

    public BookImpl(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }

    @Override
    public void displayDetails(BorrowDetails borrowDetails) {
        System.out.println("Book Details:");
        System.out.println("Title: " + title);
        System.out.println("Author: " + author);
        System.out.println("ISBN: " + isbn);
        System.out.println("Borrower: " + borrowDetails.getBorrowerName());
        System.out.println("Due Date: " + borrowDetails.getDueDate());
        String returnStatus = borrowDetails.isReturned() ? "Returned" : "Not Returned";
        System.out.println("Return Status: " + returnStatus);
        System.out.println("----------------------------");
    }
}

Step#4: Implement the Flyweight Factory Class

The Flyweight Factory manages the pool of Flyweight objects. It ensures that a new book is created only if it doesn’t already exist in the cache. It avoids unnecessary object creation, that optimizes memory usage.

import java.util.HashMap;
import java.util.Map;

public class BookFactory {

    private final Map<String, Book> bookCache = new HashMap<>();

    public Book getBook(String title, String author, String isbn) {

        String key = title + "_" + author + "_" + isbn;
        if (!bookCache.containsKey(key)) {
           bookCache.put(key, new BookImpl(title, author, isbn));
           System.out.println("Creating a new book entry for: " + title);
        } else {
           System.out.println("Reusing existing book entry for: " + title);
        }
        return bookCache.get(key);
    }
}

Step#5: Use & Test the Flyweight Pattern in the Main Program

The client uses the Flyweight Factory to request shared objects. It separates intrinsic and extrinsic states and dynamically combines them to achieve the desired functionality. Here, we demonstrate how to use the Flyweight pattern with a library system.

public class LibrarySystemDemo {

    public static void main(String[] args) {

          BookFactory bookFactory = new BookFactory();

        // Create Books (Intrinsic State)
        Book book1 = bookFactory.getBook("Effective Java", "Joshua Bloch", "123-456");
        Book book2 = bookFactory.getBook("Clean Code", "Robert C. Martin", "789-101");
          // Reuses the first book
        Book book3 = bookFactory.getBook("Effective Java", "Joshua Bloch", "123-456"); 

        // Create Borrow Details (Extrinsic State)
        BorrowDetails details1 = new BorrowDetails("Alice", "2025-02-10", false);
        BorrowDetails details2 = new BorrowDetails("Bob", "2025-03-05", true);
        BorrowDetails details3 = new BorrowDetails("Charlie", "2025-01-15", false);

         // Display Book Details
        book1.displayDetails(details1);
        book2.displayDetails(details2);
        book3.displayDetails(details3);
    }
}

Output:

Creating a new book entry for: Effective Java
Creating a new book entry for: Clean Code
Reusing existing book entry for: Effective Java
Book Details:
Title: Effective Java
Author: Joshua Bloch
ISBN: 123-456
Borrower: Alice
Due Date: 2025-02-10
Return Status: Not Returned
----------------------------
Book Details:
Title: Clean Code
Author: Robert C. Martin
ISBN: 789-101
Borrower: Bob
Due Date: 2025-03-05
Return Status: Returned
----------------------------
Book Details:
Title: Effective Java
Author: Joshua Bloch
ISBN: 123-456
Borrower: Charlie
Due Date: 2025-01-15
Return Status: Not Returned
----------------------------

Implementation of Flyweight Design Pattern Using Java 21

Let’s try to implement the same example using Java 21. Here is a refined implementation of the Flyweight pattern for the same Library System using modern Java features up to JDK 21. We will incorporate features like records, sealed classes, text blocks, and improved switch expressions, while maintaining the functionality of the Flyweight pattern.

Step#1: Define the Flyweight Interface Using Sealed Classes

Sealed classes allow us to restrict which classes can extend the Book interface.

public sealed interface Book permits BookImpl {

    void displayDetails(BorrowDetails borrowDetails);
}

Step#2: Create the Concrete Flyweight Class Using Records

Using record for BookImpl is perfect since the intrinsic state fields (title, author, isbn) are immutable.

  • Records: Automatically generate immutable classes with getter methods, equals, hashCode, and toString.
  • Text Blocks: Simplify multi-line strings using triple quotes (“””).
  • formatted Method: Used for cleaner string formatting.
public record BookImpl(String title, String author, String isbn) implements Book {

    @Override
    public void displayDetails(BorrowDetails borrowDetails) {

        System.out.println("""
                Book Details:
                Title: %s
                Author: %s
                ISBN: %s
                Borrower: %s
                Due Date: %s
                Return Status: %s
                --------------------------------
                """.formatted(title, author, isbn,
                             borrowDetails.borrowerName(),
                             borrowDetails.dueDate(),
                             borrowDetails.isReturned() ? "Returned" : "Not Returned"));
    }
}

Step#3: Define the Extrinsic State Using Records

The BorrowDetails class can also be modeled as a record to make it immutable and concise.

public record BorrowDetails(String borrowerName, String dueDate, boolean isReturned) {

}

Step#4: Implement the Flyweight Factory with Map.ofNullable (Optional Handling)

The Flyweight Factory uses a modern Map and Optional to manage book caching. ConcurrentHashMap ensures thread-safety for the factory when handling concurrent access. Optional and orElseGet handle potential null values cleanly and avoids explicit if-else checks.

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

public class BookFactory {
   private final Map<String, Book> bookCache = new ConcurrentHashMap<>();
   public Book getBook(String title, String author, String isbn) {
       String key = "%s_%s_%s".formatted(title, author, isbn);
       return Optional.ofNullable(bookCache.get(key))
         .orElseGet(() -> {
             System.out.println("Creating a new book entry for: " + title);
             Book book = new BookImpl(title, author, isbn);
             bookCache.put(key, book);
             return book;
         });
   }
}

Step#5: Use List.of() and enhanced looping in the Main Program

Using List.of() factory method of Java 9 and enhanced looping to simplify client-side logic. Enhanced For Loops and List.of() simplifies the creation and iteration of lists.

import java.util.List;

public class LibrarySystem {

    public static void main(String[] args) {
       BookFactory bookFactory = new BookFactory();

      // Create Books (Intrinsic State)
      Book book1 = bookFactory.getBook("Effective Java", "Joshua Bloch", "123-456");
      Book book2 = bookFactory.getBook("Clean Code", "Robert C. Martin", "789-101");
      Book book3 = bookFactory.getBook("Effective Java", "Joshua Bloch", "123-456"); // Reuses book1

      // Create Borrow Details (Extrinsic State)
      List<BorrowDetails> borrowDetailsList = List.of(
             new BorrowDetails("Alice", "2025-02-10", false),
             new BorrowDetails("Bob", "2025-03-05", true),
             new BorrowDetails("Charlie", "2025-01-15", false)
      );

      // Display Book Details Using Enhanced For Loop
      var books = List.of(book1, book2, book3);
      for (int i = 0; i < books.size(); i++) {
          books.get(i).displayDetails(borrowDetailsList.get(i));
      }
   }
}

Output:

Creating a new book entry for: Effective Java
Creating a new book entry for: Clean Code
Book Details:
Title: Effective Java
Author: Joshua Bloch
ISBN: 123-456
Borrower: Alice
Due Date: 2025-02-10
Return Status: Not Returned
--------------------------------
Book Details:
Title: Clean Code
Author: Robert C. Martin
ISBN: 789-101
Borrower: Bob
Due Date: 2025-03-05
Return Status: Returned
--------------------------------
Book Details:
Title: Effective Java
Author: Joshua Bloch
ISBN: 123-456
Borrower: Charlie
Due Date: 2025-01-15
Return Status: Not Returned
--------------------------------

Advantages of Using Modern Java Features

Let’s summarize the advantages of using modern Java features:

  • Readability:
    • record classes reduce boilerplate code for immutable data.
    • Text blocks make multi-line strings more readable.
  • Concurrency:
    • ConcurrentHashMap ensures safe multi-threaded access to shared data.
  • Clean Handling of Nulls:
    • Optional eliminates the risk of NullPointerException.
  • Conciseness:
    • Features like List.of, and formatted reduce clutter.

Advantages of the Flyweight Pattern

  1. Memory Efficiency:
    By reusing shared objects, memory usage is drastically reduced, especially in scenarios requiring many similar objects.
  2. Performance Boost:
    Object creation overhead is minimized since objects are reused.
  3. Reduced Redundancy:
    The Flyweight Pattern prevents duplication of similar objects, ensuring consistency.

Disadvantages/Limitations

  1. Increased Complexity:
    The separation of intrinsic and extrinsic states adds complexity to the codebase.
  2. Not Suitable for All Scenarios:
    If most of the state is extrinsic, the pattern loses its effectiveness.
  3. Thread-Safety Concerns:
    Shared objects must be handled carefully in multithreaded environments to avoid data inconsistencies.

Real-World Use Cases

Let’s explore the use cases where we can apply the Flyweight design Pattern.

  1. Text Editors:
    In text editors like Microsoft Word, characters are stored as Flyweight objects. For example, the character “A” with a specific font and size can be reused multiple times.
  2. Gaming:
    In a 2D game, sprites of trees, buildings, or enemies can be reused instead of creating new instances for every entity.
  3. Caching Systems:
    Applications that rely on caching, such as object pools or connection pools, benefit from the Flyweight pattern.
  4. Graphics Applications:
    Applications like CAD or GIS systems use Flyweight to manage graphical elements efficiently.

UML Diagram for Flyweight Pattern

Here’s a UML representation of the Flyweight pattern:

     +-------------------+
     |   Flyweight       |
     |-------------------|
     | + operation()     |
     +-------------------+
             â–²
             |
 +-------------------+      +---------------------------+
 | ConcreteFlyweight |      | UnsharedConcreteFlyweight |
 |-------------------|      |---------------------------|
 | + operation()     |      | + operation()            |
 +-------------------+      +---------------------------+
             â–²
             |
 +-------------------+
 | FlyweightFactory  |
 |-------------------|
 | - pool: Map       |
 |-------------------|
 | + getFlyweight()  |
 +-------------------+
             â–²
             |
      +----------------+
      |     Client     |
      |----------------|
      | - extrinsic    |
      |----------------|
      | + use()        |
      +----------------+

Key Components in the UML

  • Flyweight: Declares a method (operation) that accepts extrinsic data.
  • ConcreteFlyweight: Implements the Flyweight interface and stores the intrinsic state.
  • UnsharedConcreteFlyweight: Represents non-shared Flyweight objects.
  • FlyweightFactory: Creates and manages Flyweight objects.
  • Client: Supplies the extrinsic state and interacts with the Flyweight objects.

FAQs

How can we identify Flyweight in an existing code?

We can identify it by looking for repeated objects with significant shared properties. Also, check if memory usage can be optimized by reusing these objects.

When should we to avoid Flyweight Pattern?

When the extrinsic state outweighs the benefits of shared intrinsic state. In cases requiring thread safety, as shared objects can introduce synchronization challenges.

How can we compare Flyweight Pattern with Factory Method & Prototype Patterns?

  1. Factory Method Pattern:
    • Focuses on object creation logic.
    • Flyweight reuses objects instead of creating new ones.
  2. Prototype Pattern:
    • Clones existing objects instead of sharing them.
    • Flyweight avoids duplication by reusing shared objects.

What are some Real-World Examples of Flyweight Pattern?

  1. Text Rendering Systems:
    Each letter is stored once and reused in various contexts.
  2. Game Development:
    Game elements like trees or soldiers that have the same appearance but differ in position.
  3. Database Connection Pooling:
    Shared database connections are reused for performance optimization.

Conclusion

The Flyweight Pattern is a powerful design pattern that excels in scenarios where memory optimization and performance are critical in applications with many similar objects. Although its complexity may increase due to intrinsic and extrinsic state separation, but the benefits in resource-constrained scenarios are great. Developers can achieve a balance between performance and maintainability by understanding the principles and carefully applying the pattern.


One thought on “Flyweight Design Pattern With Examples Using Java 21

  1. Great explanation of the Flyweight Pattern! The breakdown of intrinsic and extrinsic states with real-world examples like hotel room management, car rentals, and coffee shops makes it easy to understand. The Java implementation, especially the modern approach using Java 21 features like records and sealed classes, is a nice touch. Thanks for sharing such a detailed and well-structured guide.

Leave a Reply


Top