Facede Design Pattern in TypeScript (Part 2): Consequences, Implementation& Related Patterns

Hooman Momtaheni
4 min readJun 16, 2024

--

Credit: wikipedia.com

In Part 1 we explored intent, applicability and structure of Facade design pattern, now lets talk about its consequences, implementation and its related patterns. As always the main reference of these articles is “Design Patterns: Elements of Reusable Object-Oriented Software”.

Consequences

The Facade pattern offers the following benefits:

1. It shields clients from subsystem components, thereby reducing the number of objects that clients deal with and making the subsystem easier to use.

2. It promotes weak coupling between the subsystem and its clients. Often the components in a subsystem are strongly coupled. Weak coupling lets you vary the components of the subsystem without affecting its clients. Facades help layer a system and the dependencies between objects. They can eliminate complex or circular dependencies. This can be an important consequence when the client and the subsystem are implemented independently.
Reducing compilation dependencies is vital in large software systems. You want to save time by minimizing recompilation when subsystem classes change. Reducing compilation dependencies with facades can limit the recompilation needed for a small change in an important subsystem. A facade can also simplify porting systems to other platforms, because it’s less likely that building one subsystem requires building all others.

3. It doesn’t prevent applications from using subsystem classes if they need to. Thus, you can choose between ease of use and generality.

Implementation

Consider the following issues when implementing a facade:

  1. Reducing client-subsystem coupling. The coupling between clients and the subsystem can be reduced even further by making Facade an abstract class with concrete subclasses for different implementations of a subsystem. Then clients can communicate with the subsystem through the interface of the abstract Facade class. This abstract coupling keeps clients from knowing which implementation of a subsystem is used.

Example:

// Abstract Facade Class
abstract class ShapeFacade {
abstract draw(): void;
}

// Concrete Subclass: CircleFacade
class CircleFacade extends ShapeFacade {
draw() {
console.log("Drawing a circle");
}
}

// Concrete Subclass: SquareFacade
class SquareFacade extends ShapeFacade {
draw() {
console.log("Drawing a square");
}
}
// Client Code
function drawShape(shape: ShapeFacade) {
shape.draw();
}

// Using CircleFacade
const circleFacade = new CircleFacade();
drawShape(circleFacade);

// Using SquareFacade
const squareFacade = new SquareFacade();
drawShape(squareFacade);

An alternative to subclassing is to configure a Facade object with different subsystem objects. To customize the facade, simply replace one or more of its subsystem objects.

Example:

// Subsystem: Email Service
class EmailService {
sendEmail(email: string, message: string) {
console.log(`Sending email to ${email}: ${message}`);
}
}

// Subsystem: SMS Service
class SMSService {
sendSMS(phone: string, message: string) {
console.log(`Sending SMS to ${phone}: ${message}`);
}
}
// Facade Class: NotificationFacade
class NotificationFacade {
private emailService: EmailService;
private smsService: SMSService;

constructor(emailService: EmailService, smsService: SMSService) {
this.emailService = emailService;
this.smsService = smsService;
}

sendNotification(type: 'email' | 'sms', recipient: string, message: string) {
if (type === 'email') {
this.emailService.sendEmail(recipient, message);
} else if (type === 'sms') {
this.smsService.sendSMS(recipient, message);
} else {
console.log(`Unsupported notification type: ${type}`);
}
}
}
// Client Code
const emailService = new EmailService();
const smsService = new SMSService();

// Create NotificationFacade with EmailService and SMSService
const notificationFacade = new NotificationFacade(emailService, smsService);

// Send email notification
notificationFacade.sendNotification('email', 'user@example.com', 'Hello from EmailService');

// Send SMS notification
notificationFacade.sendNotification('sms', '1234567890', 'Hello from SMSService');

2. Public versus private subsystem classes. A subsystem is analogous to a class in that both have interfaces, and both encapsulate something — a class encapsulates state and operations, while a subsystem encapsulates classes. And just as it’s useful to think of the public and private interface of a class, we can think of the public and private interface of a subsystem.

The public interface to a subsystem consists of classes that all clients can access; the private interface is just for subsystem extenders. The Facade class is part of the public interface, of course, but it’s not the only part. Other subsystem classes are usually public as well.

In TypeScript there are several ways to encapsulate and control the visibility of classes and other members within a subsystem to ensure they are not accessible to the client code such as:

Modules (ES6 imports/exports)

By using ES6 modules, you can control what is exported and therefore accessible to other parts of your application.

// subsystem.ts
class PrivateClass {
// implementation details
}

export class PublicClass {
private privateClassInstance: PrivateClass;

constructor() {
this.privateClassInstance = new PrivateClass();
}

public someMethod() {
// use privateClassInstance
}
}

// client.ts
import { PublicClass } from './subsystem';

const publicClassInstance = new PublicClass();
publicClassInstance.someMethod();
// PrivateClass is not accessible here

Module Pattern with IIFE

You can use an Immediately Invoked Function Expression (IIFE) to create a module pattern that encapsulates private members.

const MyModule = (() => {
class PrivateClass {
// implementation details
aMethod(){}
}

class PublicClass {
public privateClassInstance: PrivateClass;

constructor() {
this.privateClassInstance = new PrivateClass();
}

public someMethod() {
// use privateClassInstance
}
}

return {
PublicClass
};
})();

const publicClassInstance = new MyModule.PublicClass();
publicClassInstance.someMethod();
// PrivateClass is not accessible here

Related Patterns

Abstract Factory can be used with Facade to provide an interface for creating subsystem objects in a subsystem-independent way. Abstract Factory can also be used as an alternative to Facade to hide platform-specific classes.

Mediator is similar to Facade in that it abstracts functionality of existing classes. However, Mediator’s purpose is to abstract arbitrary communication between colleague objects, often centralizing functionality that doesn’t belong in any one of them. A mediator’s colleagues are aware of and communicate with the mediator instead of communicating with each other directly. In contrast, a façade merely abstracts the interface to subsystem objects to make them easier to use; it doesn’t define new functionality, and subsystem classes don’t know about it.

Usually only one Facade object is required. Thus, Facade objects are often Singletons.

--

--

Hooman Momtaheni

Full Stack Web Application Developer | Admire foundations and structures | Helping companies have reliable web apps and motivated software development team