Java Design Patterns: Flyweight and Proxy Patterns
Flyweight Pattern
The Flyweight pattern is a structural design pattern that aims to minimize memory usage by sharing as much data as possible with similar objects. It's particularly useful when you need to create a large number of similar objects, as it reduces the number of objects created and thus saves memory.
The Flyweight pattern consists of three main components:
- Abstract Flyweight: Defines the interface through which flyweight objects can receive and act on extrinsic state.
- Concrete Flyweight: Implements the flyweight interface and adds storage for intrinsic state.
- Flyweight Factory: Creates and manages flyweight objects, ensuring proper sharing. It maintains a pool of flyweight objects and provides them to clients.
Let's illustrate this pattern with a practical example. Imagine we're creating a text editor that needs to display various characters with different formatting. Instead of creating a new object for each character, we can use the Flyweight pattern to share common character objects.
First, we define an interfcae for our flyweight objects:
interface Character {
void display(String font);
}
Next, we create a concrete flyweight class that implements this interface:
class ConcreteCharacter implements Character {
private final char symbol;
private final String intrinsicState;
public ConcreteCharacter(char symbol) {
this.symbol = symbol;
this.intrinsicState = "Default styling";
System.out.println("Creating character: " + symbol);
}
@Override
public void display(String font) {
System.out.println("Displaying '" + symbol + "' with font: " + font +
" and intrinsic state: " + intrinsicState);
}
}
Now, we implement the flyweight factory that manages the character objects:
class CharacterFactory {
private static final Map<character concretecharacter=""> characterPool = new HashMap<>();
public static Character getCharacter(char symbol) {
ConcreteCharacter character = characterPool.get(symbol);
if (character == null) {
character = new ConcreteCharacter(symbol);
characterPool.put(symbol, character);
}
return character;
}
}</character>
Finally, we can test our implementation:
public class FlyweightDemo {
public static void main(String[] args) {
String text = "HELLO";
String[] fonts = {"Arial", "Times New Roman", "Courier"};
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
Character character = CharacterFactory.getCharacter(c);
character.display(fonts[i % fonts.length]);
}
}
}
When we run this code, we'll see that each character is created only once, even though it's used multiple times with different formatting.
Advantages of Flyweight Pattern
- Significantly reduces the number of objects created, saving memory.
- Improves application performance by minimizing object creation overhead.
Disadvantages of Flyweight Pattern
- Increases system complexity by requiring state separation into intrinsic and extrinsic states.
- Can lead to confusion if the external state isn't properly managed.
When to Use
- When your application needs to create a large number of similar objects.
- When object creation is expensive in terms of memory or processing time.
Important Considerations
- Properly distinguish between intrisnic and extrinsic state to avoid thread safety issues.
- Always use a factory object to control the creation and sharing of flyweight objects.
Proxy Pattern
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It's useful when you need to add additional functionality to an object without changing its interface.
The Proxy pattern has three main components:
- Subject: Defines the common interface for RealSubject and Proxy.
- RealSubject: The actual object that the proxy represents.
- Proxy: Maintains a reference to the RealSubject and controls access to it.
There are different types of proxies:
- Static Proxy: The proxy class is created at compile time.
- Dynamic Proxy: The proxy class is generated at runtime.
Let's demonstrate this pattern with an example of accessing a protected resource. Imagine we have a sensitive document that requires authentication before access.
Static Proxy
First, we define the subject interface:
interface Document {
void displayContent();
}
Next, we create the real subject:
class RealDocument implements Document {
private final String content;
public RealDocument(String content) {
this.content = content;
}
@Override
public void displayContent() {
System.out.println("Displaying document content: " + content);
}
}
Then, we implement the proxy:
class DocumentProxy implements Document {
private final RealDocument realDocument;
private final String user;
public DocumentProxy(String content, String user) {
this.realDocument = new RealDocument(content);
this.user = user;
}
@Override
public void displayContent() {
if (authenticate(user)) {
realDocument.displayContent();
} else {
System.out.println("Access denied for user: " + user);
}
}
private boolean authenticate(String user) {
// In a real application, this would check credentials
return !"unauthorized".equals(user);
}
}
Finally, we test our static proxy:
public class StaticProxyDemo {
public static void main(String[] args) {
Document document = new DocumentProxy("Secret plans", "authorized_user");
document.displayContent();
Document restricted = new DocumentProxy("Secret plans", "unauthorized");
restricted.displayContent();
}
}
Dynamic Proxy
For dynamic proxies, we use Java's reflection API. The proxy is generated at runtime:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
class DynamicDocumentProxy implements InvocationHandler {
private final Document realDocument;
private final String user;
public DynamicDocumentProxy(Document realDocument, String user) {
this.realDocument = realDocument;
this.user = user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("displayContent")) {
if (authenticate(user)) {
return method.invoke(realDocument, args);
} else {
System.out.println("Access denied for user: " + user);
return null;
}
}
return method.invoke(realDocument, args);
}
private boolean authenticate(String user) {
return !"unauthorized".equals(user);
}
}
Testing the dynamic proxy:
public class DynamicProxyDemo {
public static void main(String[] args) {
Document realDoc = new RealDocument("Confidential information");
Document proxy = (Document) Proxy.newProxyInstance(
Document.class.getClassLoader(),
new Class[]{Document.class},
new DynamicDocumentProxy(realDoc, "authorized_user")
);
proxy.displayContent();
}
}
Advantages of Proxy Pattern
- Provides a level of indirection when accessing an object, allowing for additional functionality.
- Enhances flexibility by separating the real object from its representation.
- Can add access control, logging, or other cross-cutting concerns without modifying the real object.
Disadvantages of Proxy Pattern
- Can introduce performance overhead due to the additional layer of indirection.
- Increases system complexity, especially with dynamic proxies.
Important Considerations
- Proxy pattern differs from Adapter pattern: Adapter changes the interface, while Proxy maintains the same interface.
- Proxy pattern differs from Decorator pattern: Decorator adds functionality, while Proxy controls access.