Reader small image

You're reading from  TypeScript 4 Design Patterns and Best Practices

Product typeBook
Published inSep 2021
Reading LevelIntermediate
PublisherPackt
ISBN-139781800563421
Edition1st Edition
Right arrow
Author (1)
 Theofanis Despoudis
Theofanis Despoudis
author image
Theofanis Despoudis

Theo Despoudis lives in Ireland, where he works as a Software Engineer for WP Engine and as a part-time tech practitioner for Fixate. He is the co-author of The React Workshop and Advanced Go Programming in 7 Days, Dzone Core Member, and maintains some open source projects on GitHub. Theo is available for conference talks, independent consulting, and corporate training services opportunities.
Read more about Theofanis Despoudis

Right arrow

Introducing Unified Modeling Language (UML)

You now know how to work with VSCode and have a firm understanding of its code base and some examples. We will complete this chapter by learning about UML and how we can utilize it to study design patterns. We will focus on a limited set of UML, specifically class diagrams, since this is the traditional way to depict design patterns; plus, they are straightforward to comprehend.

What is UML?

UML is a standardized way of modeling software architecture concepts, as well as interactions between systems or deployment configurations. Nowadays, UML covers more areas, and it's fairly comprehensive. It came as a result of a consolidation of similar tools and modeling techniques, such as use cases, the Object Modeling Technique (OMT), and the Booch Method.

You don't really need to know all the ins and outs of UML, but it is really helpful when you're learning about design patterns. When you first learn about design patterns, you want to have a holistic overview of the patterns, irrespective of the implementation part, which will differ from language to language. Using UML class diagrams is a perfect choice for modeling our patterns in a design language that everyone can understand with minimal training.

Let's delve into more practical examples using TypeScript.

Note

Although UML diagrams have a long history in software engineering, you should use them carefully. Generally, they should only be used to demonstrate a specific use case or sub-system, together with a short explanation of the architecture decisions. UML is not very suitable for capturing the dynamic requirements of very complex systems because, as a visual language, it is only suitable for representing high-level overviews.

Learning UML class diagrams

UML class diagrams consist of static representations of the classes or objects of a system. TypeScript supports classes and interfaces, as well as visibility modifiers (public, protected, or private) so that we can leverage those types to describe them with class diagrams. Here are some of the most fundamental concepts when studying class diagrams:

  • A class represents a collection of objects with a specific structure and features. For example, the following Product class looks like this:
    class Product {}

    This corresponds to the following diagram:

Figure 1.7 – Class representation

Figure 1.7 – Class representation

  • An interface is usually attached to a class and represents a contract that the class adheres to. This means that the class implements this interface:
    interface Identifiable<T extends string | number>{
        id: T
    }
    class Product implements Identifiable<string> {
        id: string
        constructor(id: string) {
            this.id = id;
        }
    }

    This corresponds to the following diagram. Notice the placement of the interface clause on top of the class name within the left shift (<<) and right shift (>>) symbols:

Figure 1.8 – Interface representation

Figure 1.8 – Interface representation

  • An abstract class represents an object that can't be directly instantiated:
    abstract class BaseApiClient {}

    This corresponds to the following diagram. The name of the class is in italics:

Figure 1.9 – Abstract class representation

Figure 1.9 – Abstract class representation

  • An association represents a basic relationship between classes, interfaces, or similar types. We use associations to show how they are linked with each other, and this can be direct or indirect. For example, we have the following models for Blog and Author:
    class Blog implements Identifiable<string> {
        id: string;
        authorId: string;
        constructor(id: string, authorId: string) {
            this.id = id;
            this.authorId = authorId;
        }
    }
    class Author {}

    This corresponds to the following diagram. Blog is connected to Author with a line:

Figure 1.10 – Association representation

Figure 1.10 – Association representation

Notice that because the Author class here is not being passed as a parameter, it is referenced from the authorId parameter instead. This is an example of indirect association.

  • An aggregation is a special case of association when we have two entities that can exist when one is missing or not available. For example, let's say we have a SearchService that accepts a QueryBuilder parameter and performs API requests on a different system:
    class QueryBuilder {}
    class EmptyQueryBuilder extends QueryBuilder {}
    interface SearchParams {
      qb?: QueryBuilder;
      path: string;
    }
     
    class SearchService {
      queryBuilder?: QueryBuilder;
      path: string;
     
      constructor({ qb = EmptyQueryBuilder, path }: 
        SearchParams) {
        this.queryBuilder = qb;
        this.path = path;
      }
    }

    This corresponds to the following diagram. SearchService is connected to QueryBuilder with a line and a white rhombus:

Figure 1.11 – Aggregation representation

Figure 1.11 – Aggregation representation

In this case, when we don't have a QueryBuilder or the class itself has no queries to perform, then SearchService will still exist, although it will not actually perform any requests. QueryBuilder can also exist without SearchService.

Composition is a stricter version of aggregation, where we have a parent component or class that will control the lifetime of its children. If the parent is removed from the system, then all the children will be removed as well. Here is an example with Directory and File:

class Directory {
  files: File[];
  directories: Directory[];
  constructor(files: File[], directories: Directory[]) {
    this.files = files;
    this.directories = directories;
  }
 
  addFile(file: File): void {
      this.files.push(file);
  }
  addDir(directory: Directory): void {
    this.directories.push(directory);
  }
}

This corresponds to the following diagram. Directory is connected to File with a line and a black or filled rhombus:

Figure 1.12 – Composition representation

Figure 1.12 – Composition representation

  • Inheritance represents a parent-child relationship when there is one or more sub-classes that inherit from base classes (also known as a superclass):
    class BaseClient {} 
    class UsersApiClient extends BaseClient {}  

    This corresponds to the following diagram. UsersApiClient is connected to BaseClient with a line and a white pointed arrow:

Figure 1.13 – Inheritance representation

Figure 1.13 – Inheritance representation

  • Visibility is related to attributes that the class contains and how they are accessed. For example, we have an SSHUser class that accepts a private key and a public key:
    class SSHUser {
      private privateKey: string;
      public publicKey: string;
     
      constructor(prvKey: string, pubKey: string) {
        this.privateKey = prvKey;
        this.publicKey = pubKey;
      }
     
      public getBase64(): string {
        return Buffer.from(this.publicKey).toString
          ("base64");
      }
    }

    This corresponds to the following diagram. SSHUser contains two properties and one method. We use a minus (-) for private visibility and a plus (+) for public visibility:

Figure 1.14 – Visibility

Figure 1.14 – Visibility

Here, we can see that the methods are separated by a horizontal bar for visibility.

We can also add notes or comments to class diagrams, although it's not very clear if they should be included in the code:

Figure 1.15 – Comments representation

Figure 1.15 – Comments representation

The main difficulty when using class diagrams is not drawing them on a piece of paper, but rather how to properly model the domain classes and relationships in a sound manner. This process is often iterative and involves interacting with several domain experts or knowledgeable stakeholders. In Chapter 8, Developing Modern and Robust TypeScript Applications, we are going to learn how domain-driven design can help us with modeling business rules.

Previous PageNext Page
You have been reading a chapter from
TypeScript 4 Design Patterns and Best Practices
Published in: Sep 2021Publisher: PacktISBN-13: 9781800563421
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Theofanis Despoudis

Theo Despoudis lives in Ireland, where he works as a Software Engineer for WP Engine and as a part-time tech practitioner for Fixate. He is the co-author of The React Workshop and Advanced Go Programming in 7 Days, Dzone Core Member, and maintains some open source projects on GitHub. Theo is available for conference talks, independent consulting, and corporate training services opportunities.
Read more about Theofanis Despoudis