Explore the Mediator Pattern in TypeScript, leveraging interfaces and strong typing to manage complex interactions between objects. Learn how to implement, test, and integrate this pattern in modern frameworks.
In software development, managing communication between multiple objects can become complex and unwieldy. The Mediator Pattern offers a solution by centralizing communication, reducing dependencies, and promoting loose coupling. In this section, we will explore how to implement the Mediator Pattern in TypeScript, leveraging its strong typing and interface capabilities to enforce correct usage and facilitate maintenance.
The Mediator Pattern is a behavioral design pattern that encapsulates how a set of objects interact. Instead of objects communicating directly with each other, they communicate through a mediator. This pattern promotes loose coupling by keeping objects from referring to each other explicitly and allows you to vary their interaction independently.
By using the Mediator Pattern, you can simplify object interactions, making your code more maintainable and scalable.
TypeScript provides powerful features such as interfaces and strong typing, which make implementing the Mediator Pattern both efficient and robust. Let’s explore how to define and implement the Mediator and Colleague components using TypeScript.
First, we define an interface for the Mediator. This interface will outline the methods that the Mediator must implement to facilitate communication between Colleagues.
interface Mediator {
notify(sender: Colleague, event: string): void;
}
Next, we define a Colleague interface. Colleagues will implement this interface and interact with the Mediator.
interface Colleague {
setMediator(mediator: Mediator): void;
}
Concrete Colleagues are specific implementations of the Colleague interface. They interact with each other through the Mediator.
class ConcreteColleague1 implements Colleague {
private mediator: Mediator;
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
doA(): void {
console.log("Colleague1 does A.");
this.mediator.notify(this, 'A');
}
doB(): void {
console.log("Colleague1 does B.");
this.mediator.notify(this, 'B');
}
}
class ConcreteColleague2 implements Colleague {
private mediator: Mediator;
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
doC(): void {
console.log("Colleague2 does C.");
this.mediator.notify(this, 'C');
}
doD(): void {
console.log("Colleague2 does D.");
this.mediator.notify(this, 'D');
}
}
The Concrete Mediator implements the Mediator interface and coordinates communication between Colleagues.
class ConcreteMediator implements Mediator {
private colleague1: ConcreteColleague1;
private colleague2: ConcreteColleague2;
setColleague1(colleague: ConcreteColleague1): void {
this.colleague1 = colleague;
}
setColleague2(colleague: ConcreteColleague2): void {
this.colleague2 = colleague;
}
notify(sender: Colleague, event: string): void {
if (event === 'A') {
console.log("Mediator reacts on A and triggers following operations:");
this.colleague2.doC();
}
if (event === 'D') {
console.log("Mediator reacts on D and triggers following operations:");
this.colleague1.doB();
}
}
}
TypeScript’s strong typing and interface capabilities provide several benefits when implementing the Mediator Pattern:
In complex systems, messages or events exchanged between Colleagues can be strongly typed to ensure consistency. TypeScript’s union types and enums can be used to define a set of valid messages.
type Event = 'A' | 'B' | 'C' | 'D';
class ConcreteMediatorWithTypes implements Mediator {
private colleague1: ConcreteColleague1;
private colleague2: ConcreteColleague2;
setColleague1(colleague: ConcreteColleague1): void {
this.colleague1 = colleague;
}
setColleague2(colleague: ConcreteColleague2): void {
this.colleague2 = colleague;
}
notify(sender: Colleague, event: Event): void {
if (event === 'A') {
console.log("Mediator reacts on A and triggers following operations:");
this.colleague2.doC();
}
if (event === 'D') {
console.log("Mediator reacts on D and triggers following operations:");
this.colleague1.doB();
}
}
}
TypeScript’s generics allow you to create flexible and reusable Mediators that can handle different types of Colleagues.
interface GenericMediator<T extends Colleague> {
notify(sender: T, event: string): void;
}
class GenericConcreteMediator<T extends Colleague> implements GenericMediator<T> {
private colleagues: T[] = [];
addColleague(colleague: T): void {
this.colleagues.push(colleague);
}
notify(sender: T, event: string): void {
// Handle notification logic
}
}
Type guards can be employed to handle different types of messages or events in a type-safe manner.
function isColleague1(colleague: Colleague): colleague is ConcreteColleague1 {
return (colleague as ConcreteColleague1).doA !== undefined;
}
class TypeGuardMediator implements Mediator {
notify(sender: Colleague, event: string): void {
if (isColleague1(sender)) {
console.log("Handling event for Colleague1");
} else {
console.log("Handling event for another Colleague");
}
}
}
Decoupling Colleagues from the Mediator can be achieved using interfaces and dependency injection. This approach enhances testability and flexibility.
class DependencyInjectedMediator implements Mediator {
private colleagues: Colleague[] = [];
register(colleague: Colleague): void {
this.colleagues.push(colleague);
colleague.setMediator(this);
}
notify(sender: Colleague, event: string): void {
// Handle notification logic
}
}
Documenting the interactions and message protocols between Colleagues and the Mediator is crucial for maintaining and extending the system. Clear documentation helps new developers understand the system’s architecture and facilitates debugging.
Testing the Mediator Pattern involves verifying the interactions between Colleagues and the Mediator. Unit tests can be written to test Colleagues and the Mediator in isolation, while integration tests ensure they work together correctly.
Colleagues can be tested independently by mocking the Mediator.
class MockMediator implements Mediator {
notify(sender: Colleague, event: string): void {
// Mock implementation
}
}
// Test ConcreteColleague1
const mockMediator = new MockMediator();
const colleague1 = new ConcreteColleague1();
colleague1.setMediator(mockMediator);
colleague1.doA();
The Mediator can be tested by verifying that it correctly coordinates communication between Colleagues.
const mediator = new ConcreteMediator();
const colleague1 = new ConcreteColleague1();
const colleague2 = new ConcreteColleague2();
mediator.setColleague1(colleague1);
mediator.setColleague2(colleague2);
colleague1.setMediator(mediator);
colleague2.setMediator(mediator);
colleague1.doA(); // Verify that colleague2.doC() is called
The Mediator Pattern can be integrated into frontend frameworks like Angular or React to manage component interactions.
In Angular, the Mediator Pattern can be used to manage communication between components.
@Injectable()
class AngularMediatorService implements Mediator {
private components: Colleague[] = [];
register(component: Colleague): void {
this.components.push(component);
}
notify(sender: Colleague, event: string): void {
// Handle component communication
}
}
// Component A
@Component({
selector: 'app-component-a',
template: `<button (click)="doSomething()">Do Something</button>`
})
class ComponentA implements Colleague {
constructor(private mediator: AngularMediatorService) {
this.mediator.register(this);
}
doSomething(): void {
this.mediator.notify(this, 'somethingHappened');
}
}
In React, the Mediator Pattern can be implemented using context or state management libraries.
const MediatorContext = React.createContext<Mediator | null>(null);
const ComponentA: React.FC = () => {
const mediator = useContext(MediatorContext);
const handleClick = () => {
if (mediator) {
mediator.notify(this, 'somethingHappened');
}
};
return <button onClick={handleClick}>Do Something</button>;
};
When implementing the Mediator Pattern, it is essential to handle errors and exceptions gracefully. This can be achieved by:
While the Mediator Pattern offers many benefits, there are potential pitfalls to consider:
The Mediator Pattern is a powerful tool for managing complex interactions between objects, and TypeScript’s features enhance its implementation. By centralizing communication through a Mediator, you can reduce dependencies, improve maintainability, and create more scalable systems. Whether integrating into frontend frameworks or using in backend services, the Mediator Pattern provides a robust solution for managing interactions in a type-safe and efficient manner.