Appearance
Several design patterns are described below. A design pattern is a description of a common usage of language tools to solve structural problems in code. The purpose of a design pattern is not to classify code (e.g. to say code is or is not a particular pattern) but to equip you with more complex ways of using language features to structure code. Each design pattern interacts with parts of analytical code design (i.e. coupling, cohesion, and testability).
Patterns for establishing interfaces
Adapter: Simplifying interactions with incompatible types.
The Adapter pattern is widely used, especially in the context of legacy systems that cannot be easily modified, to enable objects to more easily interact with each other. Adapters often act as translators, enabling the objects and implementations used in one design to be converted to a format that is more amenable to another design. While adapters are often relatively straightforward, if the differences between the two designs is large, they can become more complex.
In the most common cases, Adapter objects simply act as a wrapper for another object. Concretely: the adapter contains a field of the wrapped type, and exposes a set of methods that make sense for a given design. Any requests to these methods are then adapted to the interface required of the adapted object. This often involves transforming both the parameters to invoke the wrapped object as well as transforming any returned values to match the exposed interface.
The main benefit of the Adapter pattern is that it allows clients of the adapters to remain oblivious of the design of the wrapped object, while still taking advantage of its functionality. Although client objects could do these translation steps themselves, they would then be coupled to the wrapped object and would have to take on the responsibilities necessary to perform the translation themselves.

In the example above, the Client needs to use functionality from the MP3Player and AACMedia frameworks, neither of which they are able to directly modify. But they would like them to have a consistent interface, despite the fact that they both have different requirements for actually performing the action (executing play(fName: string) that the Client actually wants). The adapter objects each only know how to deal with their adapted type to provide the desired functionality. While in this example both Adapters implement FormatAdapter, this is not strictly required by the pattern.
Analysis
Consider the client code without the adapter pattern. Any updates to how the client wanted to interact with the song (for example, if they wanted to implement a "skip" button) would need to be duplicated for however each media type handled it. This indicates a tight implicit coupling, with a connascence of algorithm. By adding an adapter class, the client now only has a connascence of type with any Players: each player only has to adhere to a type interface for a client to use it.
Factory: Creating objects.
The design advice depend on abstractions, not implementations is widely used, but it is impossible to instantiate an abstraction. An object must be created before it can be used, and when an object is created we must reference (and be coupled to) the exact concrete implementation that we want to have a reference to. Creational design patterns provide a means for enabling the creation of objects to be encapsulated within a specific object. While this object will have to know about the concrete types they are creating, they allow their callers to depend on their abstractions (assuming the instantiated objects have a more meaningful supertype). Providing a means for client programs to remain oblivious of the concrete types they are using is crucial to enable the open/closed principle to be applied fully within a design.

In the class diagram above, we can see the shortcomings of the factory-less design as Bank is coupled to all three subtypes of Account so that it can instantiate the kind of object it needs, despite maintaining a reference to Account itself.

The above design has been improved by having the Bank depend on a BankFactory instance. In this way the Bank remains oblivious of the concrete implementation of the Account they are using. This design does have some drawbacks though: every time a new Account is added the AccountFactory, which all clients depend upon, will need to be modified.

This final design is called an Abstract Factory. In this design the client code depends on a factory that itself implements an AccountFactory interface. This means that the client can be specialized with the kind of factory that is relevant to them. It also means that as new types of Account are added, only the factories that the new Account is relevant for need to be modified.
typescript
// Product Interface
interface Account {
generateInterest(): number;
}
// Concrete Products
class DailyAccount implements Account {
generateInterest(): number {
return 0.01; // 1% interest rate
}
}
class RRSPAccount implements Account {
generateInterest(): number {
return 0.04; // 4% interest rate
}
}
class TFSAAccount implements Account {
generateInterest(): number {
return 0.03; // 3% interest rate
}
}
// Abstract Factory
abstract class AccountFactory {
abstract createAccount(): Account;
}
// Concrete Factories
class InvestmentBankFactory extends AccountFactory {
createAccount(): Account {
return new TFSAAccount();
}
}
class CreditUnionFactory extends AccountFactory {
createAccount(): Account {
return new DailyAccount();
}
}
// Client Context
class Bank {
private factory?: AccountFactory;
setFactory(factory: AccountFactory): void {
this.factory = factory;
}
openAccount(): Account {
if (!this.factory) {
throw new Error("No AccountFactory set.");
}
return this.factory.createAccount();
}
}
// Usage Example
const bank = new Bank();
// Configure with InvestmentBankFactory
bank.setFactory(new InvestmentBankFactory());
const investmentAccount = bank.openAccount();
console.log(`Interest Rate: ${investmentAccount.generateInterest() * 100}%`); // Output: Interest Rate: 3%
// Switch to CreditUnionFactory
bank.setFactory(new CreditUnionFactory());
const creditUnionAccount = bank.openAccount();
console.log(`Interest Rate: ${creditUnionAccount.generateInterest() * 100}%`); // Output: Interest Rate: 1%Analysis
Without the factory pattern, we would need a method in Bank that looked like this:
typescript
openAccount(): Account {
// Direct coupling and conditional logic to create concrete objects
if (this.institutionType === "InvestmentBank") {
return new TFSAAccount();
} else if (this.institutionType === "CreditUnion") {
return new DailyAccount();
}
throw new Error("Invalid or unselected institution type.");
}This introduces a tight coupling between Bank and concrete Account instances. This primarily impacts our testability: it becomes difficult to create test fakes for Accounts since our code violates the dependency inversion principle.
Strategy: Encapsulating algorithms.
The Strategy design pattern enables encapsulation of algorithms. This lets client programs depend on the algorithmic interface without having to depend (or know about) the concrete underlying implementation being used. This allows new algorithms to be easily defined and added to a system without changing any client code.
The strategy pattern is often used to avoid subclassing the client. In our example below, you could imagine Client being extended by CelsiusStrategy, KelvinStrategy, and FahrenheitStrategy. While this would work, it would mean that Client would have to be changed to add a new form of temperature conversion. The pattern also supplants the even simpler approach whereby the code would have a series of conditional statements to choose the right temperature multiplier (which would also require Client changes to extend):
typescript
if (tempScheme === 'C') {
...
} else if (tempScheme === 'F') {
...
} else if (tempScheme === 'K') {
...
} else {
...
}Analysis
In this code, any edits to the usage in the client would exhibit Scattered Changes across each of the conditional bodies. This is indicative of code that is tightly and implicitly coupled, with a connascence of algorithm (because the client code duplicates a processing algorithm). By implementing a Strategy interface, the client now only depends on a type (achieving the weaker connascence of type), and future changes including new strategies, and to the processing code, now only happen in one place. This also improves testability: instead of having to test the whole code by repeating tests except by varying tempScheme, we can individually test each strategy, and then write just one test that tests the integration of just one strategy and the client's processing code, more easily controllable if we use a FakeStrategy. This could improve either observability or controllability or both!

Patterns for delegation
State: Dynamically changing behaviour based on internal state.
The state design pattern provides a composition-based approach for clients to manage their behaviour dynamically as their internal state changes. The current state of the system is dictated by a reference to a state object; the reference is dynamically updated as conditions change. Rather than having one large if or switch statement controlling state transitions, transition decisions are left to the state objects which only need to reason about their valid transitions, not all global transitions.
In the diagram below, TCPState objects use their reference to TCPConnection to call setState(TCPState) as the state of the system changes. In this way the client (TCPConnection) always knows its current state without being responsible for making sure it is correct. As the client performs actions on its state object, that object can itself update the client's state in response to any action. In this way the client delegates the responsibility for managing state transitions to the state hierarchy.
The state pattern isolates state decisions which makes reasoning about how or why these transitions took place much easier (for example because one could add logging to setState(..) in a way that would be opaque if the state was determined by examining values in fields within the system). This typically simplifies state management as well as from any given state there is a subset of valid other states that the program could transition to; this means the transition code is much simpler than a global block which must consider all possible transitions.

Analysis
Without the state pattern, the code could look something like this:
typescript
if (last === null || last === '') {
handleClosed();
} else if (last === 'listen' && isOpen()) {
handleOpen()
} else if (last === 'established' && isClosed()) {
handleClosed();
} else if (last === 'listen' && isClosed() {
close();
}The main problem with this code is low cohesion: the parent class must deal with all its normal responsibilities in addition to managing the logic for each state and state transition. This may result in divergent changes: any edits for seemingly unrelated requests may end up touching code in similar locations in the parent class. Secondly, consider wanting to add something that affects each state transition (e.g. logging the current and next state for each transition). This results in scattered changes (and indicate a connascence of algorithm between state changes).
By implementing the state pattern, we solve both of these problems:
- by delegating all state-related logic to a separate class, we improve cohesion in the parent class.
- by centralizing the logic, we can utilize inheritance to implement shared changes exactly once.
These benefits can also be framed in terms of testability. Let's first consider controllability. In order to test the second conditional case that results in handleOpen(), we would need to get the parent class into a state where last === 'listen' and isOpen(). We would also need to do this for every other possible state and transition, no matter how complex it would be to get there. However, if we had a ListenState class, each test already assumes that it starts in the state of "listening" (with no setup required) so we just need to test each transition separately, and by testing each State class like this, we have fully tested our system by transitivity.
Decorator: Dynamically adding responsibilities to objects.
The Decorator pattern is another structural pattern that provides a means to dynamically augment an object's responsibilities. With the decorator pattern it is important to distinguish between an object and a class. A class is the structural template from which object instances are created. That is, an object is a single instance of a class and a class can have many different instances. Each object can have different field values, but the fields, methods, and parent types they have are all defined by the class they are instantiated from.
The decorator pattern exists to add new responsibilities to objects, instead of to their whole class. This means that two objects instantiated from the same type can be modified at runtime to behave differently. Decorators work by enabling objects to be wrapped in other objects and using composition to treat the wrapped object as if it were a single object.
For example, consider the following simple system where we can have a Car or three special versions of with additional features:

One day a new customer asks for a car with both nav and adaptive cruise control. Planning ahead, the team realizes it is only a matter of time before customers ask for any subset of these features and set out to extend their design in the way that best preserves their existing design:

While the above approach is conceptually consistent with the initial design, having seven subclasses of Car is not optimal and will surely cause extreme resistance to any new feature being added (for example CarAutoLights) as this will have to be mixed in with every existing subclass. Instead, the team decides to move to a system using a decorator, which enables a Car to be 'wrapped' in instances of CarDecorator to add additional features; this is great, because adding a new features means just adding a single extra class meaning the development team can go home for Christmas after all:

It can be hard to visualize what this means from the class diagram alone. To create a version of a car with Nav and AutoBrake, one only needs to do the following:
typescript
let car = new Nav(new AutoBrake(new BaseCar())));Even at runtime this could allow for additional features. For instance:
typescript
// create car with Nav off
let car = new AutoBrake(new BaseCar()));
// ... sometime later:
// turn on Nav, wrap existing object
car = new Nav(car);The decorator does have some downsides: it is impossible to control the 'order' of the wrappers with the pattern. This also means that the wrappers cannot interact with one another directly (e.g., above we could wrap a BaseCar with Nav twice, which doesn't make any sense). Also, decorator objects tend to be fairly small resulting in a large number of classes. Decorators also interfere with object identity, so code that relies on checking identity (e.g., with instanceof) will behave differently with wrapped and unwrapped objects.
Analysis
Ultimately the decorator pattern provides excellent support for maintaining the flexibility and extensibility of the system. Base classes can be kept simple focusing on their core responsibilities (single responsibility), while additional functionality can be implemented in decorators (open/close). This makes each class embody high cohesion. This also means adding new decorators is easy and does not change the base classes, meaning that the classes are more loosely coupled together. This is a textbook demonstration of the flexibility of composition over inheritance.
Composite: Consistent handling of part-whole relationships.
Composites provide a mechanism for treating groups of objects the same as individual objects (often known as part-whole hierarchies). Systems often start with individual objects, but over time gain the ability to group objects together. Adding logic to differentiate individual objects from group objects adds unnecessary complexity to code. The composite pattern, through the composite (Manager in the example below) uses composition to maintain a list of children while still itself being the parent component type (Employee below).
The introduction of the composite means any client can treat both managers and developers as employees (e.g., by asking for their names or ids uniformly), whether they have reports or not. This frees client code from checking if the Employee reference they have is a Manager or a Developer, and enabling a Manager to appropriately traverse all of their reports appropriately (even if some of their reports are themselves a Manager).

Analysis
Without the composite pattern, any changes to how the client wants to handle employees would have to be duplicated in each type of employee. Additionally, adding a new role such as TechLead which, like Manager, has direct reports to traverse, would mean a duplication of this traversal logic. Both of these issues result in the code smell of scattered changes, indicating a strong coupling (connascence of algorithm).
The composite pattern decouples these implmeentations by taking it down to a connascence of type (especially visible in the client code). In the example, the default implementation of Employee::getBudget() would just be:
typescript
public getBudget():number {
return this.salary;
}Meanwhile, the implementation of Manager::getBudget() would also capture the budget of their reports (some of whom could themselves be Managers):
typescript
public getBudget():number {
let budget = this.salary;
for (const report of this.directReports) {
budget += report.getBudget();
}
return budget;
}But to the client whether an employee is a Manager or Developer would be totally transparent.
typescript
// employee 1233 has no reports
const e1 = getEmployee(1233);
Log.info(e1.getBudget());
// employee 1234 has 4 direct and 35 indirect reports
const e2 = getEmployee(1234);
Log.info(e2.getBudget());References
There are a vast set of resources about design patterns, the following are only a rough starting point:
Great overview of most design patterns with concrete examples.
Repository of many design patterns implemented in TypeScript.
Interesting article on language-specific support for decorators.
Nice state pattern article.
Design Patterns: Elements of Object-Oriented Software (Gang of Four Book).