Mediator Pattern
The Mediator pattern is a behavioral design pattern that defines an object (the Mediator) that encapsulates the communication and coordination between multiple objects (Colleagues) without them directly referencing each other. It promotes loose coupling and centralizes complex communication logic, making it easier to manage and maintain interactions between objects.
You can take a quick look at how to use this pattern in React with functional components in the following example:
import React, { useState } from "react";
// Mediator interface
interface Mediator {
notify(sender: Colleague, event: string): void;
}
// Colleague interface
interface Colleague {
setMediator(mediator: Mediator): void;
send(event: string): void;
receive(event: string): void;
}
// Concrete Mediator
class ChatRoomMediator implements Mediator {
private colleagues: Colleague[];
constructor() {
this.colleagues = [];
}
register(colleague: Colleague): void {
this.colleagues.push(colleague);
}
notify(sender: Colleague, event: string): void {
this.colleagues
.filter((colleague) => colleague !== sender)
.forEach((colleague) => colleague.receive(event));
}
}
// Concrete Colleague
class User implements Colleague {
private mediator: Mediator | null;
private name: string;
constructor(name: string) {
this.name = name;
this.mediator = null;
}
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
send(event: string): void {
console.log(`${this.name} sends: ${event}`);
this.mediator?.notify(this, event);
}
receive(event: string): void {
console.log(`${this.name} receives: ${event}`);
}
}
const MediatorExample: React.FC = () => {
const [message, setMessage] = useState("");
const [chatRoomMediator] = useState(new ChatRoomMediator());
const [userA] = useState(new User("User A"));
const [userB] = useState(new User("User B"));
// Set the mediator for each user
userA.setMediator(chatRoomMediator);
userB.setMediator(chatRoomMediator);
// Register the users in the chat room mediator
chatRoomMediator.register(userA);
chatRoomMediator.register(userB);
const sendMessage = () => {
if (message.trim() !== "") {
userA.send(message);
setMessage("");
}
};
return (
<div>
<h3>Mediator Example</h3>
<div>
<input
type='text'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
};
export default MediatorExample;import React, { useState } from "react";
// Mediator interface
interface Mediator {
notify(sender: Colleague, event: string): void;
}
// Colleague interface
interface Colleague {
setMediator(mediator: Mediator): void;
send(event: string): void;
receive(event: string): void;
}
// Concrete Mediator
class ChatRoomMediator implements Mediator {
private colleagues: Colleague[];
constructor() {
this.colleagues = [];
}
register(colleague: Colleague): void {
this.colleagues.push(colleague);
}
notify(sender: Colleague, event: string): void {
this.colleagues
.filter((colleague) => colleague !== sender)
.forEach((colleague) => colleague.receive(event));
}
}
// Concrete Colleague
class User implements Colleague {
private mediator: Mediator | null;
private name: string;
constructor(name: string) {
this.name = name;
this.mediator = null;
}
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
send(event: string): void {
console.log(`${this.name} sends: ${event}`);
this.mediator?.notify(this, event);
}
receive(event: string): void {
console.log(`${this.name} receives: ${event}`);
}
}
const MediatorExample: React.FC = () => {
const [message, setMessage] = useState("");
const [chatRoomMediator] = useState(new ChatRoomMediator());
const [userA] = useState(new User("User A"));
const [userB] = useState(new User("User B"));
// Set the mediator for each user
userA.setMediator(chatRoomMediator);
userB.setMediator(chatRoomMediator);
// Register the users in the chat room mediator
chatRoomMediator.register(userA);
chatRoomMediator.register(userB);
const sendMessage = () => {
if (message.trim() !== "") {
userA.send(message);
setMessage("");
}
};
return (
<div>
<h3>Mediator Example</h3>
<div>
<input
type='text'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
};
export default MediatorExample;In this example, we have a simplified implementation of a Chat Room where multiple users can communicate through a Mediator. The ChatRoomMediator class implements the Mediator interface and manages the communication between users. The User class implements the Colleague interface and interacts with the mediator to send and receive messages.
Use cases
By applying the Mediator pattern, you can decouple communication between multiple objects, centralize complex interaction logic, and simplify coordination between components or modules. It promotes loose coupling, enhances maintainability and extensibility, and provides a clear and standardized way to manage interactions within a system.
The Mediator pattern is particularly useful in scenarios as:
1. Algorithmic Variations:
Implementing different sorting algorithms (e.g., bubble sort, quicksort) that can be switched dynamically.
import React, { useState } from "react";
interface SortStrategy {
sort: (array: number[]) => number[];
}
class BubbleSortStrategy implements SortStrategy {
sort(array: number[]): number[] {
// Bubble sort algorithm implementation
// ...
return array;
}
}
class QuickSortStrategy implements SortStrategy {
sort(array: number[]): number[] {
// Quick sort algorithm implementation
// ...
return array;
}
}
const SortingComponent: React.FC = () => {
const [sortStrategy, setSortStrategy] = useState<SortStrategy>(
() => new BubbleSortStrategy()
);
const [inputArray, setInputArray] = useState<number[]>([]);
const [sortedArray, setSortedArray] = useState<number[]>([]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const values = e.target.value
.split(",")
.map((val) => parseInt(val.trim(), 10));
setInputArray(values);
};
const handleStrategyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedStrategy =
e.target.value === "bubble"
? new BubbleSortStrategy()
: new QuickSortStrategy();
setSortStrategy(selectedStrategy);
};
const handleSort = () => {
const sorted = sortStrategy.sort(inputArray);
setSortedArray(sorted);
};
return (
<div>
<h4>Sorting Component</h4>
<div>
<label>Select Sorting Strategy:</label>
<select onChange={handleStrategyChange}>
<option value='bubble'>Bubble Sort</option>
<option value='quick'>Quick Sort</option>
</select>
</div>
<div>
<label>Input Array:</label>
<input type='text' onChange={handleInputChange} />
</div>
<button onClick={handleSort}>Sort</button>
<div>
<p>Sorted Array: {sortedArray.join(", ")}</p>
</div>
</div>
);
};
export default SortingComponent;import React, { useState } from "react";
interface SortStrategy {
sort: (array: number[]) => number[];
}
class BubbleSortStrategy implements SortStrategy {
sort(array: number[]): number[] {
// Bubble sort algorithm implementation
// ...
return array;
}
}
class QuickSortStrategy implements SortStrategy {
sort(array: number[]): number[] {
// Quick sort algorithm implementation
// ...
return array;
}
}
const SortingComponent: React.FC = () => {
const [sortStrategy, setSortStrategy] = useState<SortStrategy>(
() => new BubbleSortStrategy()
);
const [inputArray, setInputArray] = useState<number[]>([]);
const [sortedArray, setSortedArray] = useState<number[]>([]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const values = e.target.value
.split(",")
.map((val) => parseInt(val.trim(), 10));
setInputArray(values);
};
const handleStrategyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedStrategy =
e.target.value === "bubble"
? new BubbleSortStrategy()
: new QuickSortStrategy();
setSortStrategy(selectedStrategy);
};
const handleSort = () => {
const sorted = sortStrategy.sort(inputArray);
setSortedArray(sorted);
};
return (
<div>
<h4>Sorting Component</h4>
<div>
<label>Select Sorting Strategy:</label>
<select onChange={handleStrategyChange}>
<option value='bubble'>Bubble Sort</option>
<option value='quick'>Quick Sort</option>
</select>
</div>
<div>
<label>Input Array:</label>
<input type='text' onChange={handleInputChange} />
</div>
<button onClick={handleSort}>Sort</button>
<div>
<p>Sorted Array: {sortedArray.join(", ")}</p>
</div>
</div>
);
};
export default SortingComponent;In this example, we have a SortingComponent that allows the user to select a sorting strategy (Bubble Sort or Quick Sort) from a dropdown list and input an array of numbers. When the user clicks the "Sort" button, the selected strategy's sort method is called to sort the input array, and the sorted array is displayed.
2. Configuration Options:
Allowing users to choose different calculation methods (e.g., tax calculation, discount calculation) based on their preferences.
import React, { useState } from "react";
interface CalculationStrategy {
calculate: (amount: number) => number;
}
class TaxCalculationStrategy implements CalculationStrategy {
calculate(amount: number): number {
// Tax calculation logic
// ...
return amount * 0.15; // 15% tax
}
}
class DiscountCalculationStrategy implements CalculationStrategy {
calculate(amount: number): number {
// Discount calculation logic
// ...
return amount * 0.1; // 10% discount
}
}
const CalculationComponent: React.FC = () => {
const [calculationStrategy, setCalculationStrategy] =
useState<CalculationStrategy>(() => new TaxCalculationStrategy());
const [amount, setAmount] = useState(0);
const [result, setResult] = useState(0);
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setAmount(parseFloat(e.target.value));
};
const handleStrategyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedStrategy =
e.target.value === "tax"
? new TaxCalculationStrategy()
: new DiscountCalculationStrategy();
setCalculationStrategy(selectedStrategy);
};
const handleCalculate = () => {
const calculatedResult = calculationStrategy.calculate(amount);
setResult(calculatedResult);
};
return (
<div>
<h4>Calculation Component</h4>
<div>
<label>Select Calculation Strategy:</label>
<select onChange={handleStrategyChange}>
<option value='tax'>Tax Calculation</option>
<option value='discount'>Discount Calculation</option>
</select>
</div>
<div>
<label>Amount:</label>
<input type='number' value={amount} onChange={handleAmountChange} />
</div>
<button onClick={handleCalculate}>Calculate</button>
<div>
<p>Result: {result}</p>
</div>
</div>
);
};
export default CalculationComponent;import React, { useState } from "react";
interface CalculationStrategy {
calculate: (amount: number) => number;
}
class TaxCalculationStrategy implements CalculationStrategy {
calculate(amount: number): number {
// Tax calculation logic
// ...
return amount * 0.15; // 15% tax
}
}
class DiscountCalculationStrategy implements CalculationStrategy {
calculate(amount: number): number {
// Discount calculation logic
// ...
return amount * 0.1; // 10% discount
}
}
const CalculationComponent: React.FC = () => {
const [calculationStrategy, setCalculationStrategy] =
useState<CalculationStrategy>(() => new TaxCalculationStrategy());
const [amount, setAmount] = useState(0);
const [result, setResult] = useState(0);
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setAmount(parseFloat(e.target.value));
};
const handleStrategyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedStrategy =
e.target.value === "tax"
? new TaxCalculationStrategy()
: new DiscountCalculationStrategy();
setCalculationStrategy(selectedStrategy);
};
const handleCalculate = () => {
const calculatedResult = calculationStrategy.calculate(amount);
setResult(calculatedResult);
};
return (
<div>
<h4>Calculation Component</h4>
<div>
<label>Select Calculation Strategy:</label>
<select onChange={handleStrategyChange}>
<option value='tax'>Tax Calculation</option>
<option value='discount'>Discount Calculation</option>
</select>
</div>
<div>
<label>Amount:</label>
<input type='number' value={amount} onChange={handleAmountChange} />
</div>
<button onClick={handleCalculate}>Calculate</button>
<div>
<p>Result: {result}</p>
</div>
</div>
);
};
export default CalculationComponent;In this example, we have a CalculationComponent that allows the user to select a calculation strategy (Tax Calculation or Discount Calculation) from a dropdown list and input an amount. When the user clicks the "Calculate" button, the selected strategy's calculate method is called with the input amount, and the calculated result is displayed.
3. Dynamic Behavior:
Changing the behavior of a component based on user input or external conditions.
import React, { useState } from "react";
interface ActionStrategy {
performAction: () => void;
}
class DefaultActionStrategy implements ActionStrategy {
performAction(): void {
console.log("Default action performed");
}
}
class CustomActionStrategy implements ActionStrategy {
performAction(): void {
console.log("Custom action performed");
}
}
const ActionComponent: React.FC = () => {
const [actionStrategy, setActionStrategy] = useState<ActionStrategy>(
() => new DefaultActionStrategy()
);
const handleActionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedStrategy =
e.target.value === "custom"
? new CustomActionStrategy()
: new DefaultActionStrategy();
setActionStrategy(selectedStrategy);
};
const handlePerformAction = () => {
actionStrategy.performAction();
};
return (
<div>
<h4>Action Component</h4>
<div>
<label>Select Action Strategy:</label>
<select onChange={handleActionChange}>
<option value='default'>Default Action</option>
<option value='custom'>Custom Action</option>
</select>
</div>
<button onClick={handlePerformAction}>Perform Action</button>
</div>
);
};
export default ActionComponent;import React, { useState } from "react";
interface ActionStrategy {
performAction: () => void;
}
class DefaultActionStrategy implements ActionStrategy {
performAction(): void {
console.log("Default action performed");
}
}
class CustomActionStrategy implements ActionStrategy {
performAction(): void {
console.log("Custom action performed");
}
}
const ActionComponent: React.FC = () => {
const [actionStrategy, setActionStrategy] = useState<ActionStrategy>(
() => new DefaultActionStrategy()
);
const handleActionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedStrategy =
e.target.value === "custom"
? new CustomActionStrategy()
: new DefaultActionStrategy();
setActionStrategy(selectedStrategy);
};
const handlePerformAction = () => {
actionStrategy.performAction();
};
return (
<div>
<h4>Action Component</h4>
<div>
<label>Select Action Strategy:</label>
<select onChange={handleActionChange}>
<option value='default'>Default Action</option>
<option value='custom'>Custom Action</option>
</select>
</div>
<button onClick={handlePerformAction}>Perform Action</button>
</div>
);
};
export default ActionComponent;In this example, we have an ActionComponent that allows the user to select an action strategy (Default Action or Custom Action) from a dropdown list. When the user clicks the "Perform Action" button, the selected strategy's performAction method is called. The behavior of the component changes dynamically based on the selected strategy.
Advantages
- Decouples objects by removing direct references between them.
- Simplifies communication and coordination between objects.
- Centralizes complex interaction logic in a single mediator object.
- Enhances reusability and modularity by promoting a modular architecture.
Disadvantages
- Can introduce additional complexity by adding an extra layer of indirection.
- The mediator can become a centralized and complex object if not properly designed.
- Requires careful consideration of the design to ensure proper object interaction and responsibilities.