Clean Coding through Software Design Patterns using TypeScript

Mohamedimranps
Engineering at Bajaj Health
16 min readMar 31, 2024
Purifying the code a.k.a Refactoring

Building maintainable, scalable, and robust software is a constant pursuit for developers. Clean code, a concept popularized by Robert C. Martin, emphasizes writing human-readable, well-structured code that promotes these qualities. TypeScript, a superset of JavaScript, enhances this pursuit by adding static typing, catching errors early, and improving code clarity.

One way to achieve clean code is by leveraging software design patterns. Software design patterns, well-established solutions to recurring problems, further elevate code quality by providing reusable and adaptable structures.

This article explores how TypeScript and design patterns work together to achieve clean code. We’ll cover the basics of TypeScript, the importance of clean coding, common software design patterns, and examples demonstrating their usage.

Table of Contents:

  1. The Power of TypeScript
  2. Classification of Design Patterns for Clean Code
  3. Choosing the Right Pattern
  4. Benefits of Combining TypeScript and Design Patterns

The Power of TypeScript

  • Static Typing: TypeScript enforces data types at compile time, preventing runtime errors caused by incorrect variable assignments. This improves code predictability and maintainability.
  • Improved Readability: Type annotations make code intent clearer, especially for complex data structures. They act as documentation within the code itself.
  • Early Error Detection: TypeScript catches type-related errors during development, reducing debugging time and frustration.

Here’s a simple example demonstrating the benefits of static typing:

A mock signup function demonstrated with & without types

Design Patterns for Clean Code

Word Cloud of Design Patterns’ Terminologies

Design patterns provide solutions to common design problems, promoting code reusability, flexibility, and maintainability. Here are some key categories of design patterns and how TypeScript complements them:

  • Creational Patterns: These patterns focus on object creation. TypeScript’s features like interfaces and abstract classes can be used to define contracts and base structures for creational patterns like Singleton, Factory Method and Builder.
  • Structural Patterns: These patterns deal with object composition. TypeScript’s powerful type system allows for precise definition of relationships between objects in patterns like Adapter and Decorator.
  • Behavioral Patterns: These patterns focus on communication between objects. TypeScript interfaces can define communication protocols for patterns like Observer and Strategy.

Not to worry, we’ll see a brief explanation on each classification with a real-life example that we face in our day-to-day development in any software organization.

Most Popular patterns under Creational Pattern

Singleton Pattern

The Singleton is a Creational pattern that ensures a class has only one instance and provides a global access point to it. To understand this a little better, here is a real-life situation which we all might’ve faced at least once in our development journey.

Let’s look at a practical example using the Singleton pattern:

In many software applications, logging is a crucial aspect for monitoring and debugging. You often need a centralized logging service to record important events, errors, and information about the application’s behaviour during runtime. However, you typically want to avoid creating multiple instances of the logging service to ensure that log messages are consistently recorded and managed in a single location.

Here’s how TypeScript can be used to implement it:

Getting access to the logger instance

Explanation:

  • The Logger class has a private constructor to prevent external instantiation of the class.
  • The getInstance method provides a static way to access the singleton instance of the Logger class. It lazily initializes the instance if it doesn't exist and returns the existing instance if it does.
  • The log method logs a message to the console, but in a real-world scenario, it could save the message to a file, database, or external logging service.
  • When using the Logger, multiple instances are requested using getInstance, but they all refer to the same instance of the Logger class, ensuring that log messages are consistently recorded and managed in a single location.

In this example, the Singleton pattern ensures that there is only one instance of the logging service throughout the application, allowing for centralized logging and consistent behaviour across different parts of the codebase.

Here goes another example:

Connecting to just a single instance of the Database

Explanation:

  • The getInstance method ensures that there is only one instance of the Database class throughout the application.
  • When getInstance is called for the first time, it creates a new instance of the Database class and stores it in the instance variable.
  • On subsequent calls to getInstance, it returns the existing singleton instance without creating a new one.
  • This ensures that all parts of the application share the same instance of the Database class, allowing for centralized management of database connections and operations.

Hope you’re all now got an insight on the benefits of the Singleton Pattern. Here is the list of few other benefits that most of us might overlook.

Benefits of Singleton Pattern

  1. Controlled Access to the Singleton Instance
  2. Resource Sharing and Optimization
  3. Lazy Initialization
  4. Global State Management
  5. Thread Safety
  6. Encapsulation and Abstraction
  7. Promotes Consistency and Cohesion

Factory Method Pattern

The Factory Method pattern creates objects without specifying the exact class to be instantiated. This promotes loose coupling and allows for dynamic object creation based on conditions. Here’s an example:

Explanation:

The Factory Method pattern defines an interface for creating objects (Shape) and concrete classes (Circle, Square) that implement that interface. The factory class (ShapeFactory) has a static createShape method that takes a type argument and returns the appropriate concrete shape class. This allows for code to request shapes without knowing the specific implementation details.

To give you guys a more realistic example, consider using multiple third-party providers for different communication channels like SMS, WhatsApp and Email. Now, based on the preferred mode of channel by the user, you can instantiate certain services using this approach. Here is the code snippet for the same:

Code snippet that triggers communication based on user preferred mode of channel

Benefits of Factory Method Pattern:

In summary, the Factory Pattern offers benefits such as encapsulation of object creation logic, centralized management of object creation, flexibility and extensibility, decoupling of client and concrete classes, abstraction and polymorphism, improved testability, handling complex object construction, and enabling dynamic object creation. These benefits contribute to cleaner, more maintainable, and more scalable software architectures.

That’s all for creational patterns as we’ve seen examples on Singleton and Factory Method, now it’s time to jump into the structural patterns.

Structural Pattern

Types of Structural Design Patterns

In software development, structural design patterns play a crucial role in organizing and managing the relationships between classes and objects. These patterns provide solutions to design problems related to the composition, inheritance, and interaction of classes and objects within a system. We’ll explore structural design patterns, their principles, common use cases, and practical examples to illustrate their implementation in real-world scenarios.

Adapter Pattern

In software development, integrating new components or systems into existing ones can be challenging, especially when dealing with incompatible interfaces. The Adapter pattern serves as a bridge between two incompatible interfaces, allowing them to work together seamlessly without modifying their existing codebase.

Key Components of the Adapter Pattern:

  1. Target: This is the interface that the client expects to interact with.
  2. Adaptee: This is the existing class or interface that needs to be integrated with the client code.
  3. Adapter: This is the intermediary class that implements the Target interface and delegates calls to the Adaptee.

Real-Life Example: Consider a scenario where a company wants to integrate a new payment gateway into its existing e-commerce platform. The new payment gateway uses a different interface than the existing one, making it incompatible with the platform’s current payment processing system. By using the Adapter pattern, the company can create adapters for both the old and new payment gateways, allowing them to seamlessly integrate into the platform without disrupting its existing functionality.

Example code demonstrating the usage of adapter pattern

Explanation:

  • We define interfaces for both the Legacy Payment Gateway and the New Payment Gateway, representing their respective methods.
  • Implementations for the Legacy Payment Gateway (LegacyPaymentProcessor) and the New Payment Gateway (NewPaymentProcessor) are provided.
  • We create an adapter (LegacyToNewPaymentAdapter) that adapts the legacy payment processing method to behave like the new payment authorization method.
  • The client code demonstrates usage scenarios:
    * Directly using the Legacy Payment Gateway.
    * Directly using the New Payment Gateway.
    * Using the adapter to bridge between the Legacy and New Payment Gateways, enabling seamless integration into the e-commerce platform without disruption.

This code snippet showcases how the Adapter pattern can be applied to integrate a new payment gateway into an existing e-commerce platform, ensuring compatibility between different interfaces without modifying the existing codebase.

Facade Pattern

In software engineering, the Facade pattern is a structural design pattern that provides a simplified interface to a complex system of classes, interfaces, or subsystems. It encapsulates the complexities of the underlying system and provides a unified interface to interact with it. The Facade pattern promotes loose coupling, encapsulation, and ease of use by hiding the details of the system’s implementation.

Key Concepts of the Facade Pattern:

Facade:

  • The Facade is a class that acts as an entry point to a complex subsystem. It provides a simplified interface for interacting with the subsystem’s functionality.
  • The Facade shields clients from the complexities of the underlying subsystem and hides its implementation details.
  • Clients interact with the Facade without needing to know about the internal workings of the subsystem.

Subsystem:

  • The subsystem consists of multiple classes, interfaces, or components that work together to perform a specific set of tasks.
  • Subsystem classes may have intricate relationships and dependencies, which can be hidden from clients by the Facade.

Real-World Examples of the Facade Pattern:

  1. Libraries and APIs:
    Many libraries and APIs provide a Facade to simplify complex functionality. For example, a graphics library may offer a Facade for drawing shapes, hiding the details of rendering algorithms and device-specific optimizations.
  2. Operating Systems:
    Operating systems often provide Facades for common system tasks, such as file management, process control, and network communication. These Facades abstract away the complexities of low-level system calls and provide a simplified interface for application developers.
  3. E-commerce Platforms:
    E-commerce platforms may use a Facade to simplify the checkout process, hiding the complexities of inventory management, payment processing, and shipping logistics from the end-user.
Phone Inventory Logic using Facade Pattern

The provided code demonstrates the use of the Facade pattern to simplify the interface and interactions with a complex system composed of multiple subsystems. Let’s break down the code and explain how it applies the Facade pattern:

LocationService:
This class represents a service responsible for checking the availability of products based on the pincode (location).

  • The checkLocationAvailability method checks if a given pincode corresponds to an available city. If not, it logs a message indicating that the product is not available.

PhoneInventory:

  • This class represents a subsystem responsible for managing the inventory of phone models.
  • It maintains a map of phone models and their quantities.
  • The checkAvailability method checks if a given phone model is available in the inventory.
  • The purchasePhone method reduces the inventory count after a phone is purchased.

PhoneStore (Facade):

  • This class acts as a facade, providing a simplified interface for purchasing phones.
  • It encapsulates interactions with the PhoneInventory and LocationService subsystems.
  • The purchasePhone method serves as the facade method. It checks the availability of the requested phone model in the inventory and verifies the location availability using the LocationService.
  • If the phone model is available and the location is valid, it proceeds with the purchase and updates the inventory. Otherwise, it logs appropriate messages indicating the unavailability of the product.

Client Code:

  • In the client code, a PhoneStore instance is created to simulate the purchase process.
  • The purchasePhone method of the PhoneStore is called multiple times with different phone models and pin codes to demonstrate the purchasing process.

Benefits of the Facade Pattern:

Simplified Interface:

The Facade pattern simplifies the interface to a complex system, making it easier for clients to interact with.

  • Clients can use the Facade’s methods without needing to understand the inner workings or complexities of the subsystem.

Encapsulation:

  • The Facade encapsulates the complexities of the subsystem, hiding its implementation details and internal dependencies.
  • This promotes information hiding and reduces the impact of changes within the subsystem on client code.

Loose Coupling:

  • By providing a unified interface, the Facade pattern decouples clients from the subsystem, reducing dependencies and promoting flexibility.
  • Changes to the subsystem’s implementation can be isolated within the Facade, minimizing the impact on client code.

Promotes Reusability:

  • The Facade pattern encourages reusable design by abstracting complex functionality into a single, cohesive interface.
  • Clients can reuse the Facade across different parts of the application without needing to understand the underlying complexities.

Hopefully I made the above two structural patterns as easy to understand and now it’s time to move on to the last and final pattern — Behavioral Pattern.

Behavioral Patterns

Types of Behavioral Patterns

In software engineering, Behavioral Design Patterns focus on how objects interact and communicate with each other to fulfill various responsibilities and tasks. These patterns address the interactions between objects and the delegation of responsibilities, aiming to improve the flexibility, reusability, and maintainability of software systems. Let's delve into Behavioral Design Patterns, discussing their principles, common use cases, and practical examples.

Key Concepts of Behavioral Design Patterns:

Object Interaction:

  • Behavioral patterns govern how objects collaborate and communicate with each other to achieve specific functionalities.
  • They define the protocols, messages, and interactions between objects, promoting cohesive behavior within the system.

Responsibility Delegation:

  • Behavioral patterns facilitate the delegation of responsibilities and behaviors among objects, allowing them to collaborate effectively.
  • They distribute tasks and functionalities in a way that promotes separation of concerns and modular design.

Strategy Pattern

The Strategy Pattern is a behavioral design pattern that enables an algorithm’s behavior to be selected at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently of the client that uses it.

Real-Life Example: Consider a navigation application that provides different routes based on user preferences and conditions. The application can employ the Strategy Pattern to implement various routing algorithms, such as shortest path, fastest route, or scenic drive. Users can select their preferred routing strategy, and the application dynamically switches between strategies without modifying the core navigation functionality.

Explanation:

  • We define a RoutingStrategy interface that declares a method calculateRoute.
  • Concrete strategy classes (ShortestPathStrategy and FastestRouteStrategy) implement this interface, providing specific implementations of the calculateRoute method.
  • The NavigationApp class serves as the context, allowing clients to set and switch between different routing strategies.
  • Clients can set the routing strategy using the setRoutingStrategy method and plan routes using the planRoute method.

In summary, the Strategy Pattern allows for dynamic selection and interchangeability of algorithms at runtime. By encapsulating algorithmic behaviour within interchangeable strategy objects, this pattern promotes flexibility, reusability, and modular design, making it an invaluable tool in designing adaptable software systems.

State Pattern

image credit: refactoring.guru

The State Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. This pattern encapsulates state-specific behaviour within individual state objects and delegates the responsibility of managing state transitions to a context object. The State Pattern promotes a clean separation of concerns, making it easier to maintain and extend code by encapsulating state-related logic in separate classes. Let’s delve into the State Pattern with an explanation and an example.

Key Concepts of the State Pattern:

Context:

  • The Context is the class that contains the state object and delegates state-specific behavior to it.
  • It maintains a reference to the current state object and forwards requests to it.

State:

  • The State interface defines a set of methods or operations that encapsulate state-specific behavior.
  • Concrete state classes implement this interface, providing specific implementations of the behavior corresponding to each state.

Example Scenario: Consider a vending machine that dispenses different items based on its current state (e.g., in stock, out of stock). The vending machine can employ the State Pattern to manage its behavior:

  • Context: The VendingMachine class serves as the context and contains a reference to the current state (e.g., InStockState, OutOfStockState).
  • State: State objects encapsulate behavior related to specific states of the vending machine (e.g., dispenseItem(), refillStock()).

Explanation:

  • We define a VendingMachineState interface that declares methods insertCoin(), selectItem(), and dispenseItem().
  • Concrete state classes (InStockState and OutOfStockState) implement this interface, providing specific implementations of these methods based on the vending machine's state.
  • The VendingMachine class serves as the context and maintains a reference to the current state object.
  • Clients interact with the vending machine through methods like insertCoin(), selectItem(), and dispenseItem(), which delegate requests to the current state object.

In summary, the State Pattern allows objects to alter their behaviour dynamically as their internal state changes. By encapsulating state-specific behaviour within individual state classes, this pattern promotes modularity, encapsulation, and flexibility, making it a valuable tool in managing object behaviour in various scenarios.

Choosing the Right Design Pattern

In software development, choosing the right design pattern is crucial for creating robust, maintainable, and scalable software systems. Each design pattern has its own set of principles, advantages, and use cases, and selecting the appropriate pattern depends on various factors such as the problem domain, system requirements, and architectural considerations. Here are some key points to consider when choosing the right design pattern:

1 — Understanding the Problem Domain:

  • Before selecting a design pattern, it’s essential to thoroughly understand the problem domain and the requirements of the system.
  • Identify the recurring design problems, challenges, or constraints that need to be addressed.

2 — Matching Patterns to Problems:

  • Each design pattern is tailored to address specific design problems and challenges.
  • Choose a pattern that best fits the problem at hand, considering factors such as complexity, flexibility, and scalability.

3 — Considering Trade-offs:

  • Evaluate the trade-offs associated with each design pattern, including performance implications, code complexity, and maintenance overhead.
  • Consider factors such as runtime efficiency, memory usage, and ease of understanding and debugging.

4 — Adapting to Change:

  • Anticipate future changes and requirements and choose patterns that facilitate adaptability and extensibility.
  • Select patterns that allow for easy modification and evolution of the system over time.

5 — Applying Design Principles:

  • Apply fundamental design principles such as SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) when choosing and implementing design patterns.
  • Ensure that patterns adhere to principles of encapsulation, abstraction, and separation of concerns.

6 — Collaborative Decision-making:

  • Involve team members, architects, and domain experts in the decision-making process.
  • Foster collaboration and brainstorming sessions to explore different design options and select the most appropriate pattern collectively.

7 — Iterative Refinement:

  • Design patterns are not one-size-fits-all solutions and may require iterative refinement and adaptation based on feedback and real-world usage.
  • Continuously evaluate and refine the chosen patterns based on evolving requirements and insights gained from implementation.

In conclusion, choosing the right design pattern involves a thoughtful analysis of the problem domain, consideration of trade-offs, adherence to design principles, and collaborative decision-making. By carefully selecting and applying appropriate design patterns, developers can create software systems that are flexible, maintainable, and resilient to change, ultimately leading to higher-quality software products and enhanced developer productivity.

Benefits of Combining TypeScript and Design Patterns

The combination of TypeScript, a statically typed superset of JavaScript, with well-established design patterns offers numerous benefits for software development. By leveraging TypeScript’s strong typing, class-based syntax, and advanced language features alongside proven design patterns, developers can create more robust, maintainable, and scalable codebases. Here are some key benefits of combining TypeScript and design patterns:

Type Safety and Predictability:

  • TypeScript’s static typing system provides compile-time checks that catch common errors and prevent runtime failures.
  • When used in conjunction with design patterns, TypeScript ensures that objects, interfaces, and interactions adhere to predefined contracts and constraints, leading to more predictable and reliable code.

Enhanced Code Readability and Maintainability:

  • Design patterns offer standardized solutions to common design problems, making code easier to understand and maintain.
  • TypeScript’s class-based syntax and support for interfaces align well with design pattern implementations, resulting in clear, self-documenting code that is easier to comprehend and modify.

Encapsulation and Modularity:

  • TypeScript facilitates encapsulation and modularity through classes, modules, and namespaces, enabling the implementation of design patterns such as Singleton, Module, and Facade.
  • By encapsulating related functionality within classes and modules and applying design patterns to manage dependencies and interactions, developers can achieve cleaner, more modular codebases.

Flexibility and Extensibility:

  • Design patterns promote flexible and extensible software architectures by decoupling components, promoting code reuse, and enabling easy modifications.
  • TypeScript’s support for inheritance, interfaces, and generics complements design patterns such as Strategy, Decorator, and Observer, allowing developers to create highly adaptable systems that can accommodate changing requirements and evolving business needs.

Tooling and IDE Support:

  • TypeScript benefits from robust tooling and IDE support, including features such as code completion, refactoring, and static analysis.
  • With TypeScript, developers can leverage powerful development environments like Visual Studio Code to write, debug, and maintain code more efficiently, further enhancing productivity when working with design patterns.

Facilitated Collaboration and Team Productivity:

  • TypeScript’s static typing and clear syntax contribute to improved collaboration and team productivity by reducing ambiguity and facilitating communication.
  • By incorporating design patterns, teams can establish common design idioms and practices, enabling smoother collaboration and easier onboarding of new team members.

Combining TypeScript with design patterns offers a synergistic approach to software development, leveraging TypeScript’s language features and design patterns’ proven solutions to create high-quality, maintainable, and scalable software systems. By harnessing the benefits of type safety, encapsulation, modularity, and flexibility, developers can build robust applications that meet the challenges of modern software development effectively.

Further Exploration

This article just provides a starting point. There are numerous design patterns to explore. Consider these resources for further learning:

Thanks for reading!!

--

--