Facade pattern in TypeScript

At times, when utilizing specific built-in language features, you may inadvertently end up with duplicated code. Moreover, when interfacing with third-party libraries, you might encounter APIs that are cumbersome to use. Additionally, in scenarios where third-party dependencies are in play, you may find yourself wanting to facilitate easy refactoring or replacement in the future. These situations are optimal candidates for leveraging the Facade Pattern.

Definition

The Facade Pattern is a structural design pattern that provides a simplified interface to a set of interfaces in a subsystem. It aims to make a complex system more accessible by providing a higher-level interface that hides the complexities of the underlying components.

In software development, especially in large applications, it's common to have subsystems with multiple components that work together to achieve a specific functionality. However, interacting with these subsystems directly can be challenging and can lead to code that is difficult to understand and maintain.

The Facade Pattern addresses this issue by introducing a facade (class or function), which acts as a unified interface to a set of interfaces in the subsystem. The facade hides the complexities of the subsystem and provides a simplified interface for clients to interact with.

Implementation

Imagine you have a multimedia application with both a video and an audio player. To create a seamless experience of running them together, displaying a visually appealing muted video with an accompanying audio track, you find yourself dealing with duplicated code scattered across various sections of your application. This redundancy arises from the necessity to repeatedly call specific methods for both the video and audio components.

So, your code currently looks as follows:

// Subsystem components - library for example.
class AudioPlayer {
  play(): void {
    console.log("Playing audio");
  }
}

class VideoPlayer {
  play(): void {
    console.log("Playing video");
  }
}

class Display {
  show(): void {
    console.log("Displaying content");
  }
}
// Usage of library.
const audio = new AudioPlayer();
const video = new VideoPlayer();
const display = new Display();

audio.play();
video.play();
display.show();

The primary issue arises when you need to reuse this code elsewhere, leading to duplication. Additionally, if there's a change in the library, updating references across all instances becomes a cumbersome task. While it may not pose a significant challenge in this specific example, consider a scenario where you use the Button component from material-ui, and later, you need to replace it throughout the entire app. Best of luck with that! 😄

Indeed, the solution to streamline such scenarios is to create a Facade:

// Facade class
class Multimedia {
  private audioPlayer: AudioPlayer;
  private videoPlayer: VideoPlayer;
  private display: Display;

  constructor() {
    this.audioPlayer = new AudioPlayer();
    this.videoPlayer = new VideoPlayer();
    this.display = new Display();
  }

  playMedia(): void {
    this.audioPlayer.play();
    this.videoPlayer.play();
  }

  displayContent(): void {
    this.display.show();
  }
}

Now you need to call it like that:

// Client code
const multimedia = new Multimedia();
multimedia.playMedia();
multimedia.displayContent();

The produced code will be easier to maintain and will be not duplicated across different modules. In addition, the swap of the library requires the change only in a single place.

Common Example

Consider you have a Modal component in your application directly sourced from material-ui. However, your manager expresses dissatisfaction, deeming it as unattractive and cringeworthy. Looking ahead, you anticipate the need for a redesign or even a complete re-implementation of this component. The challenge lies in the uncertainty about whether this component allows for the customization you require. As a result, your existing code is structured like this:

import { Modal } from "@mui";

// Each is in a different file.
const UserModal = () => <Modal />;
const PostsModal = () => <Modal />;
const BlaBlaModal = () => <Modal />;

Rather than directly importing the Modal component from material-ui, consider creating a wrapper component - a facade - that conceals the direct usage of the material-ui library. This wrapper exposes a unified interface for interaction, providing an abstraction layer that hides the intricacies of the underlying library.

import { Modal as MuiModal } from "@mui";

interface ModalProps {
   // Your props here...
}

const Modal = (props: ModalProps ) => {
   return <MuiModal {...props} />
}

Now, instead of directly importing the component from material-ui, import your custom Modal component. This strategic move ensures that any future replacement efforts will be confined to a single file - your facade. All you need to do is adhere to the specified contract outlined in the interface, simplifying the process of upgrading or replacing the underlying library.

import { Modal } from "@components";

// Each is in a different file.
const UserModal = () => <Modal />;
const PostsModal = () => <Modal />;
const BlaBlaModal = () => <Modal />;

Thanks to this approach, you've effectively restricted the modification of three files to just one - the facade file! By encapsulating the library-specific details within the facade, you've created a centralized point for any future changes or replacements, minimizing the impact on the rest of your codebase.

Benefits

  1. Simplified Interface: The facade pattern provides a simplified and unified interface, making it easier for clients to interact with the subsystem.
  2. Code Readability: By encapsulating the subsystem's complexity, the code becomes more readable and understandable. Developers can focus on high-level interactions without delving into the details of individual components.
  3. Ease of Maintenance: Changes to the subsystem can be isolated within the facade, reducing the impact on the rest of the codebase. This makes the system more maintainable and adaptable to future changes.
  4. Promotes Decoupling: The Facade Pattern promotes loose coupling between clients and the subsystem. Clients only need to interact with the facade, and they are shielded from the internal changes in the subsystem.

Conclusion

The Facade Pattern is a valuable tool in simplifying complex systems by providing a clean and straightforward interface to clients. In TypeScript, it becomes even more powerful due to the language's support for type-safety. By employing the Facade Pattern, developers can create more maintainable, readable, and adaptable code, ultimately leading to a more robust software architecture.