[definition, purpose, use cases, design, example, implementation]
Behavioral patterns are concerned with managing algorithms, relationships, and responsibilities between objects, ensuring that objects communicate effectively and efficiently to perform tasks.
This pattern allows an object to pass the request along a chain of handlers. Upon receiving a request, each handler decides whether to process the request or to pass it to the next handler in the chain.
When you have a set of conditions, and you need to pick the one to be applied based on priority.
Say, you have a shopping cart and a set of offers where we want to pick which to be applied based on a set priority. The offers (priority wise):
public void setNextOfferHandler(OfferHandler nextOfferHandler) { this.nextOfferHandler = nextOfferHandler; }
public abstract void applyOrder(Order order); }
2. Create Concrete Offer Classes with the logic
```java
public class Offer1 extends OfferHandler {
@Override
public void applyOffer(Order order) {
if(order.cost> 200 && order.isPrimeUser) {
order.setOfferType("OFFER 1");
System.out.println(order.getOfferType() + " applied to " + order.getOrderId());
}
else if(nextOfferHandler!=null) {
nextOfferHandler.applyOffer(order);
}
}
}
public class Offer4 extends OfferHandler {
@Override
public void applyOffer(Order order) {
if(order.getCost()>25 && order.getItemCategories().contains("GROCERIES")) {
order.setOfferType("OFFER 4");
System.out.println(order.getOfferType() + " applied to " + order.getOrderId());
}
else if (nextOfferHandler!=null) {
nextOfferHandler.applyOffer(order);
}
}
}
public class ChainOfResponsibility {
public static void main(String[] args) {
// Initiate offers
OfferHandler offer1 = new Offer1();
OfferHandler offer2 = new Offer2();
OfferHandler offer3 = new Offer3();
OfferHandler offer4 = new Offer4();
// Set up the chain
offer1.setNextOfferHandler(offer2);
offer2.setNextOfferHandler(offer3);
offer3.setNextOfferHandler(offer4);
Order order1 = new Order("1", 30, List.of("GROCERIES"), true, null);
Order order2 = new Order("2", 30, List.of("GROCERIES"), false, null);
offer1.applyOffer(order1); // OFFER 2 applied to 1
offer1.applyOffer(order2); // OFFER 4 applied to 2
}
}
</br>
When you need to execute some commands, but also want to decouple the command invoker & command executor so you can make client requests encapsulated, queued, logged, or executed later.
1. Undo/Redo Functionalities in text editors, games, etc.
2. Logging or Transactional Mechanisms in Banking systems, File Systems
Command interface, and then its implementations.Receiver class that actually performs the work.Invoker class that asks the command to run.Let’s assume we have a Light class that can be turned on and off, and we want to implement the Command pattern to encapsulate these operations.
Command Interface
// Command Interface
public interface Command {
void execute();
}
Concrete Command Classes ```java // Concrete Command to turn the light on public class LightOnCommand implements Command { private Light light;
public LightOnCommand(Light light) { this.light = light; }
@Override public void execute() { light.turnOn(); } }
// Concrete Command to turn the light off public class LightOffCommand implements Command { private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
} } ```
Receiver class
// Receiver Class
public class Light {
public void turnOn() {
System.out.println("The light is on.");
}
public void turnOff() {
System.out.println("The light is off.");
}
}
Invoker class
// Invoker Class
public class RemoteControl {
private Command command;
// Set the command to be executed
public void setCommand(Command command) {
this.command = command;
}
// Execute the command
public void pressButton() {
command.execute();
}
}
// Client
public class CommandPatternDemo {
public static void main(String[] args) {
// Receiver
Light light = new Light();
// Concrete Commands
Command lightOnCommand = new LightOnCommand(light);
Command lightOffCommand = new LightOffCommand(light);
// Invoker
RemoteControl remote = new RemoteControl();
// Turn the light on
remote.setCommand(lightOnCommand);
remote.pressButton();
// Turn the light off
remote.setCommand(lightOffCommand);
remote.pressButton();
}
}
</br>
The pattern defines
Expression: declares an interface for interpreting the contextExpression: implements an interpret() operation associated with terminal symbols in grammar (simplest rules)Expression: represents more complex grammar rules & composes expressions using other expressionsContext: contains info that is global to the interpreterLet’s create a simple example where we interpret mathematical expressions using the Interpreter pattern. We’ll define an expression that can handle addition, subtraction, and numbers.
Expression interface
interface Expression {
int interpret();
}
Terminal Expression classes for Numbers
class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret() {
return this.number;
}
}
class AddExpression implements Expression {
private Expression leftExpression;
private Expression rightExpression;
public AddExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret() {
return leftExpression.interpret() + rightExpression.interpret();
}
}
class SubtractExpression implements Expression {
private Expression leftExpression;
private Expression rightExpression;
public SubtractExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret() {
return leftExpression.interpret() - rightExpression.interpret();
}
}
Client code that builds the expression treepublic static void main(String[] args) {
// Create number expressions
Expression ten = new NumberExpression(10);
Expression twenty = new NumberExpression(20);
Expression five = new NumberExpression(5);
// Create the expression tree: (10 + 20) - 5
Expression addExpression = new AddExpression(ten, twenty);
Expression subtractExpression = new SubtractExpression(addExpression, five);
// Interpret the expression tree and print the result
int result = subtractExpression.interpret();
System.out.println("(10 + 20) - 5 = " + result); // Output: (10 + 20) - 5 = 25
}
</br>
The pattern allows for sequential access of elements in a collection without exposing its underlying representation.
It provides a standard way to iterate through collections like lists, arrays, or trees while hiding the complexities involved in the iteration process.
Iterator interface: declares the methods needed for iteration (like hasNext() and next())Aggregator interface: provides the iterator to traverse through the elementsIterator: implements the iterator interface to provide the actual mechanism for iterating over the collection.Aggregator: A concrete implementation of the collection.Let’s consider a basic Java example of iterating through an array of Strings
Iterator Interface
public interface Iterator<T> {
boolean hasNext();
T next();
}
Aggregator Interface
public interface IterableCollection<T> {
Iterator<T> createIterator();
}
Concrete Iterator Implementation
public class NameIterator implements Iterator<T> {
private String[] names;
private int index;
public NameIterator(String[] names) {
this.names = names;
this.index = 0;
}
@Override
public boolean hasNext() {
return index<names.length;
}
@Override
public String next() {
if(this.hasNext()) {
return names[index++];
}
return null;
}
}
Concrete Collection Implementationpublic class NameCollection implements IterableCollection<T> {
private String[] names;
public NameCollection(String[] names) {
this.names = names;
}
@Override
public Iterator<String> createIterator() {
return NameIterator(names);
}
}
Client Code to Use the Iteratorpublic static void main (String[] args) {
String[] names = { "Dave", "Joseph", "Jessica", "Nick" };
NameCollection nameCollection = new NameCollection(names);
Iterator<String> iterator = nameCollection.createIterator();
while ((iterator.hasNext())) {
String name = iterator.next();
System.out.println("Name: " + name);
}
}
</br>
When you want to loosely couple objects and manage their interactions (control and logic) in another class
Apache Airflow & Apache Nifi, the mediator pattern can manage the interaction between different steps or components of a workflow.Mediator interface: defines a method for communication between colleague objectsConcreteMediator class: implements the mediator interface and coordinates communication between colleague objectsColleague interface: abstract class or interface for objects that communicate through the mediatorConcreteColleague classes: implements the colleague interface and communicates with other colleagues through the mediatorLet’s use a chatroom as an example where multiple users can communicate with each other. We’ll use the Mediator pattern to manage the interactions between users.
Mediator interfacepublic interface Mediator {
void sendMessage (String message, User user);
}
Colleague interface/abstract classimport java.awt.*;
public abstract class User {
protected Mediator mediator;
protected String name;
public User(Mediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}
public abstract void receiveMessage(String message);
public abstract void sendMessage(String message);
}
ConcreteMediatorpublic class ChatRoom implements Mediator {
private List<User> users = new ArrayList<>();
public void addUser(User user) {
users.add(user);
}
@Override
public void sendMessage(String message, User user) {
for (User u : users) {
if (u != user) {
u.receiveMessage(message);
}
}
}
}
ConcreteColleague classespublic class ConcreteUser extends User {
public ConcreteUser(Mediator mediator, String name) {
super(mediator, name);
}
@Override
public void receiveMessage (String message) {
System.out.println(name + " received: " + message);
}
@Override
public void sendMessage (String message) {
System.out.println(name + " sent: " + message);
mediator.sendMessage(message, this);
}
}
public static void main (String[] args) {
ChatRoom chatRoom = new ChatRoom();
ConcreteUser dave = new ConcreteUser (chatRoom, "Dave");
ConcreteUser jessica = new ConcreteUser (chatRoom, "Jessica");
ConcreteUser nick = new ConcreteUser (chatRoom, "Nick");
chatRoom.addUser(dave);
chatRoom.addUser(jessica);
chatRoom.addUser(nick);
dave.sendMessage("Hi! I'm Dave, everyone.");
jessica.sendMessage("Hi Dave and everyone else");
nick.sendMessage("Hi Dave and Jessica, and everyone else");
}
</br>
The design pattern is used to capture and restore an object’s state without violating encapsulation.
It is particularly useful when you need to implement undo/redo functionality or save and restore the state of an object.
Use Case: In applications where users can make changes to an object (e.g., text editors, graphic design software), the Memento pattern helps implement undo and redo functionality. Each change can be saved as a Memento, allowing users to revert to previous states.
Example: A text editor can use the Memento pattern to save the state of the document at different points in time, allowing users to undo or redo their changes.
Use Case: In version control systems, the state of a file or project needs to be captured and restored. The Memento pattern can be used to save snapshots of files or projects, enabling rollback to previous versions.
Example: A source code repository saves snapshots of code changes, allowing developers to revert to earlier versions of the codebase.
Use Case: In simulation or testing environments, the Memento pattern can be used to save the state of the system at different points for testing purposes or to analyze different scenarios.
Example: A simulation tool saves the state of a simulated environment at various points, enabling users to analyze how changes in parameters affect the system.
Originator class: the object whose state needs to be saved and restoredMemento class: stores the state of the Originator and is used to restore the stateCaretaker class: responsible for keeping the Memento and ensuring it is not modified. It can request the Memento from the Originator and use it to restore the Originator’s state.Let’s consider a simple example of a text editor where we can save and restore the state of the text content.
Originator class whose state needs to be storedpublic class TextEditor {
private String content;
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public Memento save() {
return new Memento(content);
}
public void restore(Memento memento) {
this.content = memento.getContent();
}
}
Memento class which stores the state of Originator objectspublic class Memento {
private final String content;
public Memento (String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
Caretaker class that handles Memento’s requests to restore Originator objectsimport java.util.Stack;
public class Caretaker {
private final Stack<Memento> mementoStack = new Stack<>();
public void saveState(TextEditor editor) {
mementoStack.push(editor.save());
}
public void restoreState(TextEditor editor) {
if(!mementoStack.isEmpty()) {
Memento memento = mementoStack.pop();
editor.restore(memento);
}
}
}
Client code to demonstrate the usagepublic static void main (String[] args) {
TextEditor editor = new TextEditor();
Caretaker caretaker = new Caretaker();
editor.setContent("Version 1");
caretaker.saveState(editor);
editor.setContent("Version 2");
caretaker.saveState(editor);
editor.setContent("Version 3");
System.out.println("Current Content: " + editor.getContent()); // Version 3
caretaker.restoreState(editor);
System.out.println("Restored to: " + editor.getContent()); // Version 2
caretaker.restoreState(editor);
System.out.println("Restored to: " + editor.getContent()); // Version 1
}
</br>
This pattern is particularly useful for implementing distributed event handling systems.
Subject interface: the object that holds the state and notifies observers of any changesObserver interface: the object that gets notified of changes in the subjectConcreteSubject class: concrete implementation of the subject that maintains the state and notifies observersConcreteObserver class: concrete implementation of the observer that reacts to the changes in the subjectLet’s create a simple example where a WeatherStation (the subject) notifies multiple displays (observers) about temperature updates.
Observer interface
public interface Observer {
void update(float temperature);
}
Subject interface
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
ConcreteSubject class - WeatherStation
public class WeatherStation implements Subject {
private List<Observer> observers;
private float temperature;
public WeatherStation() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature);
}
}
public void setTemperature(float temperature) {
this.temperature = temperature;
notifyObservers();
}
}
ConcreteObserver class - Weather Displaypublic class WeatherDisplay implements Observer {
private float temperature;
@Override
public void update(float temperature) {
this.temperature = temperature;
display();
}
public void display() {
System.out.println("Temperature Display: " + temperature + "°C");
}
}
public static void main (String[] args) {
WeatherStation weatherStation = new WeatherStation();
WeatherDisplay display1 = new WeatherDisplay();
WeatherDisplay display2 = new WeatherDisplay();
weatherStation.registerObserver(display1);
weatherStation.registerObserver(display2);
weatherStation.setTemperature(25.0f); // "Temperature Display: 25.0°C" x2
weatherStation.setTemperature(30.0f); // "Temperature Display: 30.0°C" x2
}
</br>
This pattern is typically used when an object needs to change its behavior based on its current state, without changing the object’s class.
NoCoinState, HasCoinState, and SoldState. The behavior of inserting a coin, pressing a button, and dispensing items depends on the machine’s current state.GreenState, YellowState, and RedState, and each state governs the behavior of the traffic light.NotAuthenticatedState, AuthenticatedState, and LockedState. The operations allowed (e.g., login, logout, retry) depend on the current state of the user session.Context class: the object whose behavior changes as its internal state changes. It holds a reference to a State object that defines the current stateState interface: an interface that declares methods corresponding to the behavior that varies by stateConcreteState classes: classes that implement the State interface and define specific behavior for different states.Let’s design a vending machine that has three states: NoCoinState, HasCoinState, and SoldState.
The behavior of dispensing items and accepting coins changes based on the machine’s state.
State interface for declaring behaviors of different statespublic interface State {
void insertCoin();
void ejectCoin();
void pressButton();
void dispense();
}
class VendingMachine {
private State noCoinState;
private State hasCoinState;
private State soldState;
private State currentState;
public VendingMachine() {
noCoinState = new NoCoinState(this);
hasCoinState = new HasCoinState(this);
soldState = new SoldState(this);
currentState = noCoinState; // Initial state
}
public void setState(State state) {
this.currentState = state;
}
public State getNoCoinState() {
return noCoinState;
}
public State getHasCoinState() {
return hasCoinState;
}
public State getSoldState() {
return soldState;
}
// delegate behavior to the current state
public void insertCoin() {
currentState.insertCoin();
}
public void ejectCoin() {
currentState.ejectCoin();
}
public void pressButton() {
currentState.pressButton();
currentState.dispense();
}
}
ConcreteState classes - NoCoinState, HasCoinState, and SoldStatepublic class NoCoinState implements State {
private VendingMachine vendingMachine;
public NoCoinState(VendingMachine vendingMachine) {
this.vendingMachine = vendingMachine;
}
public void insertCoin() {
System.out.println("Coin inserted.");
vendingMachine.setState(vendingMachine.getHasCoinState());
}
public void ejectCoin() {
System.out.println("No coin to eject.");
}
public void pressButton() {
System.out.println("You need to insert a coin first.");
}
public void dispense() {
System.out.println("No item dispensed.");
}
}
class HasCoinState implements State {
VendingMachine vendingMachine;
public HasCoinState(VendingMachine vendingMachine) {
this.vendingMachine = vendingMachine;
}
public void insertCoin() {
System.out.println("Coin already inserted.");
}
public void ejectCoin() {
System.out.println("Coin ejected.");
vendingMachine.setState(vendingMachine.getNoCoinState());
}
public void pressButton() {
System.out.println("Button pressed.");
vendingMachine.setState(vendingMachine.getSoldState());
}
public void dispense() {
System.out.println("No item dispensed.");
}
}
class SoldState implements State {
VendingMachine vendingMachine;
public SoldState(VendingMachine vendingMachine) {
this.vendingMachine = vendingMachine;
}
public void insertCoin() {
System.out.println("Please wait, we're already giving you an item.");
}
public void ejectCoin() {
System.out.println("You can't eject the coin, the item is being dispensed.");
}
public void pressButton() {
System.out.println("Already pressed the button.");
}
public void dispense() {
System.out.println("Item dispensed.");
vendingMachine.setState(vendingMachine.getNoCoinState()); // Back to no coin state
}
}
Client code to test the Vending Machinepublic static void main(String[] args) {
VendingMachine vendingMachine = new VendingMachine();
// Testing the behavior of the vending machine in different states
vendingMachine.insertCoin(); // Coin inserted
vendingMachine.pressButton(); // Button pressed, item dispensed
vendingMachine.ejectCoin(); // No coin to eject, already in NoCoinState
}
</br>
When we want to let the client choose the strategy/algorithm at runtime
Strategy interface: declares a common method that all concrete strategies must implementConcreteStrategy classes: classes that implement the strategy interface and define specific behaviorContext class: the class that uses the Strategy interface. It is configured with a strategy object and calls its methods.Imagine a scenario where we want to sort a list of numbers using different algorithms (e.g., Bubble Sort, Quick Sort, Merge Sort), and we want to dynamically choose the sorting strategy at runtime.
Strategy interfacepublic interface SortStrategy {
void sort(int[] numbers);
}
Concrete Strategies classes - BubbleSort, SelectionSortpublic class SelectionSort implements SortStrategy {
@Override
public void sort (int[] numbers) {
int n = numbers.length;
for (int i = 0; i<n-1; i++) {
int minIdx = i;
for (int j = i+1; j<n; j++) {
if(numbers[j]<numbers[minIdx]) {
minIdx = j;
}
}
int temp = numbers[minIdx];
numbers[minIdx] = numbers[i];
numbers[i] = temp;
}
System.out.println("Sorted using Selection Sort");
}
}
public class BubbleSort implements SortStrategy {
@Override
public void sort (int[] numbers) {
int n = numbers.length;
for (int i = 0; i<n-1; i++) {
for (int j = 0; j<n-i-1; j++) {
if(numbers[j]>numbers[j+1]) {
int temp = numbers[j];
numbers[j] = numbers[j+1];
numbers[j+1] = temp;
}
}
}
System.out.println("Sorted using Bubble Sort");
}
}
Context Class to switch between sorting strategiespublic class SortContext {
private SortStrategy sortStrategy;
// Set strategy at runtime
public void setSortStrategy(SortStrategy sortStrategy) {
this.sortStrategy = sortStrategy;
}
// Sort the array using the selected strategy
public void executeSortStrategy(int[] numbers) {
sortStrategy.sort(numbers);
}
}
Client code to change strategies at runtimepublic static void main (String[] args) {
int[] numbers = { 5, -2, 7, 8, 1, -6, 4};
SortContext context = new SortContext();
SelectionSort selectionSort = new SelectionSort();
BubbleSort bubbleSort = new BubbleSort();
context.setSortStrategy(selectionSort);
context.executeSortStrategy(numbers); // "Sorted using Selection Sort"
context.setSortStrategy(bubbleSort);
context.executeSortStrategy(numbers); // "Sorted using Bubble Sort"
}
</br>
Useful in various scenarios where the structure of an algorithm remains the same but individual steps may vary.
Base class (abstract or concrete): contains the skeleton of the algorithm in the form of a method, usually marked as final to prevent modification. This method calls other methods, some of which may be abstract or have default behaviorConcrete subclasses: override the abstract or optional methods to provide custom behavior for specific steps in the algorithmImagine a game where different players (like a Cricketer or Footballer) need to prepare for a game. The steps might include warming up, playing the game, and cooling down. The sequence is the same, but the specifics vary depending on the sport.
public abstract class Player {
// Template method
public final void prepareForGame() {
warmUp();
playGame();
coolDown();
}
// Common method, can be overridden if needed
public void warmUp() {
System.out.println("General warm-up exercises");
}
// Abstract methods that subclasses will implement
public abstract void playGame();
// Common method, can be overridden if needed
public void coolDown() {
System.out.println("Stretching and cooling down");
}
}
Concrete Subclasses with the custom method implementationspublic class Cricketer extends Player {
@Override
public void playGame() {
System.out.println("Playing cricket: Batting, bowling, fielding");
}
@Override
public void coolDown() {
System.out.println("Cooling down with light jogging");
}
}
public class Footballer extends Player {
@Override
public void playGame() {
System.out.println("Playing football: Dribbling, passing, shooting");
}
@Override
public void coolDown() {
System.out.println("Cooling down with a light walk");
}
}
Client codepublic static void main(String[] args) {
Player cricketer = new Cricketer();
cricketer.prepareForGame();
System.out.println();
Player footballer = new Footballer();
footballer.prepareForGame();
}
</br>
A pattern that allows you to add further operations to objects without modifying them. It enables you to separate algorithms from the objects on which they operate.
This pattern is particularly useful when you have a complex object structure and need to perform operations on it that are subject to frequent change.
Button, TextField, Container accept visitors for operations such as rendering or validation of user inputs.Shape, Image, TextBlock objects can accept visitors that serialize them to formats like JSON or XML.Visitor: an interface or abstract class that declares a visit method for each type of element in the object structure. It moves from element to element and performs some operations.Concrete Visitor: classes that implement/extend the operations defined in the Visitor interface for each elementElement: an interface that defines an accept() method. Each concrete element class implements this method, allowing a visitor to perform an operation on itConcrete Element: classes that implement the accept() method as per the requirementObject Structure: collection of elements that the visitor will traverseSuppose we have a system with different types of files (e.g., TextFile, ImageFile, VideoFile) that we want to perform operations on, such as compression or rendering.
We want to avoid changing the file classes each time we add a new operation.
Visitor interface/abstract classpublic interface Visitor {
void visit (TextFileElement textFile);
void visit (ImageFileElement imageFile);
void visit (VideoFileElement videoFile);
}
Element interface (File Base) that declares the accept() method for element implementationspublic interface FileElement {
void accept(Visitor visitor);
}
Concrete Element classes (File Types) with accept() implementationspublic class TextFileElement implements FileElement {
private String content;
public TextFileElement (String content) {
this.content = content;
}
public String getContent() {
return content;
}
@Override
public void accept (Visitor visitor) {
visitor.accept(this);
}
}
public class ImageFileElement implements FileElement {
private String resolution;
public ImageFileElement (String resolution) {
this.resolution = resolution;
}
public String getResolution() {
return resolution;
}
@Override
public void accept (Visitor visitor) {
visitor.accept(this);
}
}
public class VideoFileElement implements FileElement {
private String duration;
public VideoFileElement (String duration) {
this.duration = duration;
}
public String getDuration() {
return duration;
}
@Override
public void accept (Visitor visitor) {
visitor.accept(this);
}
}
Concrete Visitor classes to define the operations to perform (rendering & compressing files)public class CompressingVisitor implements Visitor {
@Override
void visit (TextFileElement textFile) {
System.out.println("Compressing text file with content: " + textFile.getContent());
}
@Override
void visit (ImageFileElement imageFile) {
System.out.println("Compressing text file with resolution: " + imageFile.getResolution());
}
@Override
void visit (VideoFileElement videoFile) {
System.out.println("Compressing text file with duration: " + videoFile.getDuration);
}
}
public class RenderingVisitor implements Visitor {
@Override
void visit (TextFileElement textFile) {
System.out.println("Rendering text file with content: " + textFile.getContent());
}
@Override
void visit (ImageFileElement imageFile) {
System.out.println("Rendering text file with resolution: " + imageFile.getResolution());
}
@Override
void visit (VideoFileElement videoFile) {
System.out.println("Rendering text file with duration: " + videoFile.getDuration);
}
}
Client code to demonstrate the usagepublic static void main (String[] args) {
// create the object structure (collection of elements)
FileElement[] files = new FileElement[] {
new TextFileElement("Hello world"),
new ImageFileElement("1920x1080"),
new VideoFileElement("2 hours")
};
FileVisitor compressionVisitor = new CompressionVisitor();
FileVisitor renderingVisitor = new RenderingVisitor();
// Apply compression to all files
System.out.println("Applying Compression Visitor:");
for (FileElement file : files) {
file.accept(compressionVisitor);
}
// Apply rendering to all files
System.out.println("\nApplying Rendering Visitor:");
for (FileElement file : files) {
file.accept(renderingVisitor);
}
}