Flyweight Design Pattern With Examples Using Java 21 Core Java Design Patterns java Java 21 jdk 21 by devs5003 - January 29, 2025February 1, 20251 Last Updated on February 1st, 2025 Flyweight 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 Toggle What is the Flyweight Pattern?What is intrinsic state and extrinsic state in the Flyweight Pattern?Intrinsic StateExtrinsic StateOther Examples of intrinsic state and extrinsic stateExample#1: Hotel Room ManagementExample#2: Car Rental ServiceExample#3: Coffee Shop MenuExample#4: Taxi Booking SystemExample#5: Theme Park TicketingHow does the Flyweight Pattern work?Components of the Flyweight PatternImplementation of Flyweight Design Pattern in JavaStep#1: Define the Flyweight InterfaceStep#2: Define the Extrinsic State ClassStep#3: Create the Concrete Flyweight ClassStep#4: Implement the Flyweight Factory ClassStep#5: Use & Test the Flyweight Pattern in the Main ProgramOutput:Implementation of Flyweight Design Pattern Using Java 21Step#1: Define the Flyweight Interface Using Sealed ClassesStep#2: Create the Concrete Flyweight Class Using RecordsStep#3: Define the Extrinsic State Using RecordsStep#4: Implement the Flyweight Factory with Map.ofNullable (Optional Handling)Step#5: Use List.of() and enhanced looping in the Main ProgramOutput:Advantages of Using Modern Java FeaturesAdvantages of the Flyweight PatternDisadvantages/LimitationsReal-World Use CasesUML Diagram for Flyweight PatternKey Components in the UMLFAQsHow can we identify Flyweight in an existing code?When should we to avoid Flyweight Pattern?How can we compare Flyweight Pattern with Factory Method & Prototype Patterns?What are some Real-World Examples of Flyweight Pattern?Conclusion 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 Flyweight Interface: Defines the common interface for objects that can be shared. Concrete Flyweight: Implements the Flyweight interface and represents shared data (intrinsic state). Unshared Concrete Flyweight: Represents objects that are not shared but may work in conjunction with shared objects. 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. 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 Memory Efficiency: By reusing shared objects, memory usage is drastically reduced, especially in scenarios requiring many similar objects. Performance Boost: Object creation overhead is minimized since objects are reused. Reduced Redundancy: The Flyweight Pattern prevents duplication of similar objects, ensuring consistency. Disadvantages/Limitations Increased Complexity: The separation of intrinsic and extrinsic states adds complexity to the codebase. Not Suitable for All Scenarios: If most of the state is extrinsic, the pattern loses its effectiveness. 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. 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. Gaming: In a 2D game, sprites of trees, buildings, or enemies can be reused instead of creating new instances for every entity. Caching Systems: Applications that rely on caching, such as object pools or connection pools, benefit from the Flyweight pattern. 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? Factory Method Pattern: Focuses on object creation logic. Flyweight reuses objects instead of creating new ones. 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? Text Rendering Systems: Each letter is stored once and reused in various contexts. Game Development: Game elements like trees or soldiers that have the same appearance but differ in position. 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. Kindly visit other articles on Design Patterns in Java with examples. Related
Flyweight 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 Toggle What is the Flyweight Pattern?What is intrinsic state and extrinsic state in the Flyweight Pattern?Intrinsic StateExtrinsic StateOther Examples of intrinsic state and extrinsic stateExample#1: Hotel Room ManagementExample#2: Car Rental ServiceExample#3: Coffee Shop MenuExample#4: Taxi Booking SystemExample#5: Theme Park TicketingHow does the Flyweight Pattern work?Components of the Flyweight PatternImplementation of Flyweight Design Pattern in JavaStep#1: Define the Flyweight InterfaceStep#2: Define the Extrinsic State ClassStep#3: Create the Concrete Flyweight ClassStep#4: Implement the Flyweight Factory ClassStep#5: Use & Test the Flyweight Pattern in the Main ProgramOutput:Implementation of Flyweight Design Pattern Using Java 21Step#1: Define the Flyweight Interface Using Sealed ClassesStep#2: Create the Concrete Flyweight Class Using RecordsStep#3: Define the Extrinsic State Using RecordsStep#4: Implement the Flyweight Factory with Map.ofNullable (Optional Handling)Step#5: Use List.of() and enhanced looping in the Main ProgramOutput:Advantages of Using Modern Java FeaturesAdvantages of the Flyweight PatternDisadvantages/LimitationsReal-World Use CasesUML Diagram for Flyweight PatternKey Components in the UMLFAQsHow can we identify Flyweight in an existing code?When should we to avoid Flyweight Pattern?How can we compare Flyweight Pattern with Factory Method & Prototype Patterns?What are some Real-World Examples of Flyweight Pattern?Conclusion 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 Flyweight Interface: Defines the common interface for objects that can be shared. Concrete Flyweight: Implements the Flyweight interface and represents shared data (intrinsic state). Unshared Concrete Flyweight: Represents objects that are not shared but may work in conjunction with shared objects. 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. 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 Memory Efficiency: By reusing shared objects, memory usage is drastically reduced, especially in scenarios requiring many similar objects. Performance Boost: Object creation overhead is minimized since objects are reused. Reduced Redundancy: The Flyweight Pattern prevents duplication of similar objects, ensuring consistency. Disadvantages/Limitations Increased Complexity: The separation of intrinsic and extrinsic states adds complexity to the codebase. Not Suitable for All Scenarios: If most of the state is extrinsic, the pattern loses its effectiveness. 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. 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. Gaming: In a 2D game, sprites of trees, buildings, or enemies can be reused instead of creating new instances for every entity. Caching Systems: Applications that rely on caching, such as object pools or connection pools, benefit from the Flyweight pattern. 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? Factory Method Pattern: Focuses on object creation logic. Flyweight reuses objects instead of creating new ones. 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? Text Rendering Systems: Each letter is stored once and reused in various contexts. Game Development: Game elements like trees or soldiers that have the same appearance but differ in position. 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.
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. Reply