Design Patterns with Go and TypeScript
Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, this book introduced software design patterns. This book is a pioneer and influenced much content of design patterns out there. I think it is a must-read book.
Many content creators out there re-explained those theories or patterns. They tried to explain them in their way. I agree with that. Because the book, the source, is old. The book was released in 1994. All examples are written in C++. Not many kids understand the C++ syntaxes. But trying to re-explain the patterns maybe will cause or create another theory. I wouldn't do that here. So, I suggest you read the book.
In short, design patterns are common patterns in software design or architecture. This book name and classify them. Using those named patterns while discussing and designing with other developers can be helpful.
From the book, we know that they worked hard to write it. They wrote it a long time. The book is not to be read once. I think the most important section is Design Pattern Catalog. We will read or open that section again and again. The case study from the book, Designing a Document Editor, is also the best part. It showed us how those design patterns solved it. So, we can make good software. However, before we go any further, I suggest you have some Object Oriented Programming (OOP) knowledge first. Feel free to read OOP in Go or OOP in TypeScript.
Design patterns have been classified into three types; Creational, Structural, and Behavioral. This article provides all examples written in GoLang and TypeScript. The goal is that we can understand design patterns through examples or a simple way just from a single article.
I was getting help from ChatGPT while making these examples. About a week after GPT-4 was just released. ChatGPT understands Design Patterns, but I still guided her to produce simple examples until met my expectation. In some cases we did refactoring.
I still have some ideas to improve this article. I will put them here or update this article in the future to bring good content quality about design patterns.
In my opinion, knowledge about design patterns is optional. I discussed this with other seniors and most of them agreed. But SOLID is a must.
Creational Patterns
Creational design patterns abstract the instantiation process. They help make a system independent of how its objects are created, composed, and represented.
In short, patterns for object creation.
Abstract Factory
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
In the example below, we have an AbstractFactory
interface with two methods; CreateProductA
and CreateProductB
. This interface could have many clients or implementations. Let's say ConcreteFactory1
and ConcreteFactory2
. They have their details for CreateProductA
and CreateProductB
.
classDiagram class AbstractFactory{ <<interface>> CreateProductA() ProductA CreateProductB() ProductB } class ConcreteFactory1{ CreateProductA() ProductA CreateProductB() ProductB } class ConcreteFactory2{ CreateProductA() ProductA CreateProductB() ProductB } AbstractFactory <|.. ConcreteFactory1 AbstractFactory <|.. ConcreteFactory2
Go
type ProductA interface {
DoSomething()
}
type ProductB interface {
DoSomethingElse()
}
type ConcreteProductA1 struct{}
func (p ConcreteProductA1) DoSomething() {
fmt.Println("Doing something in ConcreteProductA1")
}
type ConcreteProductB1 struct{}
func (p ConcreteProductB1) DoSomethingElse() {
fmt.Println("Doing something else in ConcreteProductB1")
}
type ConcreteProductA2 struct{}
func (p ConcreteProductA2) DoSomething() {
fmt.Println("Doing something in ConcreteProductA2")
}
type ConcreteProductB2 struct{}
func (p ConcreteProductB2) DoSomethingElse() {
fmt.Println("Doing something else in ConcreteProductB2")
}
type AbstractFactory interface {
CreateProductA() ProductA
CreateProductB() ProductB
}
type ConcreteFactory1 struct{}
func (c ConcreteFactory1) CreateProductA() ProductA {
return ConcreteProductA1{}
}
func (c ConcreteFactory1) CreateProductB() ProductB {
return ConcreteProductB1{}
}
type ConcreteFactory2 struct{}
func (c ConcreteFactory2) CreateProductA() ProductA {
return ConcreteProductA2{}
}
func (c ConcreteFactory2) CreateProductB() ProductB {
return ConcreteProductB2{}
}
func GetFactory(factoryType string) AbstractFactory {
switch factoryType {
case "Factory1":
return ConcreteFactory1{}
case "Factory2":
return ConcreteFactory2{}
default:
return nil
}
}
TypeScript
interface ProductA {
doSomething(): void;
}
interface ProductB {
doSomethingElse(): void;
}
class ConcreteProductA1 implements ProductA {
doSomething(): void {
console.log("Doing something in ConcreteProductA1");
}
}
class ConcreteProductB1 implements ProductB {
doSomethingElse(): void {
console.log("Doing something else in ConcreteProductB1");
}
}
class ConcreteProductA2 implements ProductA {
doSomething(): void {
console.log("Doing something in ConcreteProductA2");
}
}
class ConcreteProductB2 implements ProductB {
doSomethingElse(): void {
console.log("Doing something else in ConcreteProductB2");
}
}
interface AbstractFactory {
createProductA(): ProductA;
createProductB(): ProductB;
}
class ConcreteFactory1 implements AbstractFactory {
createProductA(): ProductA {
return new ConcreteProductA1();
}
createProductB(): ProductB {
return new ConcreteProductB1();
}
}
class ConcreteFactory2 implements AbstractFactory {
createProductA(): ProductA {
return new ConcreteProductA2();
}
createProductB(): ProductB {
return new ConcreteProductB2();
}
}
function getFactory(factoryType: string): AbstractFactory | null {
switch (factoryType) {
case "Factory1":
return new ConcreteFactory1();
case "Factory2":
return new ConcreteFactory2();
default:
return null;
}
}
Builder
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
You have an interface, let's say, Builder
. It has multiple methods of building parts and a method to return a result.
classDiagram class Builder{ <<interface>> BuildPart1() BuildPart2() BuildPart3() GetResult() Product } class ConcreteBuilder1{ BuildPart1() BuildPart2() BuildPart3() GetResult() Product } class ConcreteBuilder2{ BuildPart1() BuildPart2() BuildPart3() GetResult() Product } Builder <|.. ConcreteBuilder1 Builder <|.. ConcreteBuilder2
Go
type Builder interface {
BuildPart1()
BuildPart2()
BuildPart3()
GetResult() Product
}
type Product struct {
parts []string
}
func (p *Product) AddPart(part string) {
p.parts = append(p.parts, part)
}
type ConcreteBuilder1 struct {
product Product
}
func (b *ConcreteBuilder1) BuildPart1() {
b.product.AddPart("Part 1 of ConcreteBuilder1")
}
func (b *ConcreteBuilder1) BuildPart2() {
b.product.AddPart("Part 2 of ConcreteBuilder1")
}
func (b *ConcreteBuilder1) BuildPart3() {
b.product.AddPart("Part 3 of ConcreteBuilder1")
}
func (b *ConcreteBuilder1) GetResult() Product {
return b.product
}
type ConcreteBuilder2 struct {
product Product
}
func (b *ConcreteBuilder2) BuildPart1() {
b.product.AddPart("Part 1 of ConcreteBuilder2")
}
func (b *ConcreteBuilder2) BuildPart2() {
b.product.AddPart("Part 2 of ConcreteBuilder2")
}
func (b *ConcreteBuilder2) BuildPart3() {
b.product.AddPart("Part 3 of ConcreteBuilder2")
}
func (b *ConcreteBuilder2) GetResult() Product {
return b.product
}
type Director struct{}
func (d Director) Construct(builder Builder) Product {
builder.BuildPart1()
builder.BuildPart2()
builder.BuildPart3()
return builder.GetResult()
}
TypeScript
class Product {
private parts: string[];
constructor() {
this.parts = [];
}
addPart(part: string): void {
this.parts.push(part);
}
getParts(): string[] {
return this.parts;
}
}
interface Builder {
buildPart1(): void;
buildPart2(): void;
buildPart3(): void;
getResult(): Product;
}
class ConcreteBuilder1 implements Builder {
private product: Product;
constructor() {
this.product = new Product();
}
buildPart1(): void {
this.product.addPart("Part 1 of ConcreteBuilder1");
}
buildPart2(): void {
this.product.addPart("Part 2 of ConcreteBuilder1");
}
buildPart3(): void {
this.product.addPart("Part 3 of ConcreteBuilder1");
}
getResult(): Product {
return this.product;
}
}
class ConcreteBuilder2 implements Builder {
private product: Product;
constructor() {
this.product = new Product();
}
buildPart1(): void {
this.product.addPart("Part 1 of ConcreteBuilder2");
}
buildPart2(): void {
this.product.addPart("Part 2 of ConcreteBuilder2");
}
buildPart3(): void {
this.product.addPart("Part 3 of ConcreteBuilder2");
}
getResult(): Product {
return this.product;
}
}
class Director {
construct(builder: Builder): Product {
builder.buildPart1();
builder.buildPart2();
builder.buildPart3();
return builder.getResult();
}
}
Factory Method
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
Almost similar to AbstractFactory
. The difference is, AbstractFactory
is an entire object for creation, while FactoryMethod
is a just method.
classDiagram class Creator{ <<interface>> FactoryMethod() Product } class ConcreteCreator1{ FactoryMethod() Product } class ConcreteCreator2{ FactoryMethod() Product } Creator <|.. ConcreteCreator1 Creator <|.. ConcreteCreator2
Go
type Creator interface {
FactoryMethod() Product
}
type Product interface {
Use()
}
type ConcreteProduct1 struct{}
func (p *ConcreteProduct1) Use() {
fmt.Println("Using ConcreteProduct1")
}
type ConcreteProduct2 struct{}
func (p *ConcreteProduct2) Use() {
fmt.Println("Using ConcreteProduct2")
}
type ConcreteCreator1 struct{}
func (c *ConcreteCreator1) FactoryMethod() Product {
return &ConcreteProduct1{}
}
type ConcreteCreator2 struct{}
func (c *ConcreteCreator2) FactoryMethod() Product {
return &ConcreteProduct2{}
}
TypeScript
interface Product {
use(): void;
}
class ConcreteProduct1 implements Product {
use(): void {
console.log("Using ConcreteProduct1");
}
}
class ConcreteProduct2 implements Product {
use(): void {
console.log("Using ConcreteProduct2");
}
}
interface Creator {
factoryMethod(): Product;
}
class ConcreteCreator1 implements Creator {
factoryMethod(): Product {
return new ConcreteProduct1();
}
}
class ConcreteCreator2 implements Creator {
factoryMethod(): Product {
return new ConcreteProduct2();
}
}
Prototype
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
The Prototype
can clone itself.
classDiagram class Prototype{ <<interface>> Clone() Prototype } class ConcretePrototype1{ property1 string property2 int Clone() Prototype } class ConcretePrototype2{ property1 int property2 int Clone() Prototype } Prototype <|.. ConcretePrototype1 Prototype <|.. ConcretePrototype2
Go
type Prototype interface {
Clone() Prototype
}
type ConcretePrototype1 struct {
property1 string
property2 int
}
func (p *ConcretePrototype1) Clone() Prototype {
return &ConcretePrototype1{p.property1, p.property2}
}
type ConcretePrototype2 struct {
property1 int
property2 int
}
func (p *ConcretePrototype2) Clone() Prototype {
return &ConcretePrototype2{p.property1, p.property2}
}
type Client struct{}
func (c *Client) Operation(prototype Prototype) {
clone := prototype.Clone()
fmt.Printf("Cloned object: %+v\n", clone)
}
func main() {
prototype1 := &ConcretePrototype1{"hello", 123}
client := &Client{}
client.Operation(prototype1)
prototype2 := &ConcretePrototype2{456, 789}
client.Operation(prototype2)
}
TypeScript
interface Prototype {
clone(): Prototype;
}
class ConcretePrototype1 implements Prototype {
constructor(public property1: string, public property2: number) {}
public clone(): Prototype {
return new ConcretePrototype1(this.property1, this.property2);
}
}
class ConcretePrototype2 implements Prototype {
constructor(public property1: number, public property2: number) {}
public clone(): Prototype {
return new ConcretePrototype2(this.property1, this.property2);
}
}
class Client {
public static operation(prototype: Prototype): void {
const clone = prototype.clone();
console.log(`Cloned object: ${JSON.stringify(clone)}`);
}
}
const prototype1 = new ConcretePrototype1("hello", 123);
Client.operation(prototype1);
const prototype2 = new ConcretePrototype2(456, 789);
Client.operation(prototype2);
Singleton
Ensure a class only has one instance, and provide a global point of access to it.
The point of Singleton or these examples below is using one instance only for all its consumers.
Go
type Singleton struct {
data string
}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{data: "Hello, World!"}
})
return instance
}
func main() {
s1 := GetInstance()
s2 := GetInstance()
if s1 == s2 {
fmt.Println("s1 and s2 are the same instance")
} else {
fmt.Println("s1 and s2 are different instances")
}
fmt.Printf("s1 data: %s\n", s1.data)
fmt.Printf("s2 data: %s\n", s2.data)
}
TypeScript
class Singleton {
private static instance: Singleton;
private data: string;
private constructor() {
this.data = "Hello, World!";
}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
public getData(): string {
return this.data;
}
}
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
if (s1 === s2) {
console.log("s1 and s2 are the same instance");
} else {
console.log("s1 and s2 are different instances");
}
console.log(`s1 data: ${s1.getData()}`);
console.log(`s2 data: ${s2.getData()}`);
Structural Patterns
Structural patterns are concerned with how classes and objects are composed to form larger structures. Structural class patterns use inheritance to compose interfaces or implementations.
Adapter
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
Let's say you have multiple adapters. Each adapter has different properties for its implementation. That's why we need a Target
interface that has a Request
method that has many implementations.
classDiagram class Target{ <<interface>> Request() } class Adapter{ adaptee Adaptee Request() } class Adaptee{ SpecificRequest() } Target <|.. Adapter Adapter --> Adaptee note for Adapter "adaptee.SpecificRequest()"
Go
type Target interface {
Request()
}
type Adaptee struct{}
func (a *Adaptee) SpecificRequest() {
fmt.Println("Adaptee's specific request")
}
type Adapter struct {
adaptee *Adaptee
}
func (a *Adapter) Request() {
fmt.Println("Adapter's request")
a.adaptee.SpecificRequest()
}
func NewAdapter(adaptee *Adaptee) Target {
return &Adapter{adaptee: adaptee}
}
func main() {
adaptee := &Adaptee{}
adapter := NewAdapter(adaptee)
adapter.Request()
}
TypeScript
interface Target {
request(): void;
}
class Adaptee {
public specificRequest(): void {
console.log("Adaptee's specific request");
}
}
class Adapter implements Target {
constructor(private adaptee: Adaptee) {}
public request(): void {
console.log("Adapter's request");
this.adaptee.specificRequest();
}
}
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
adapter.request();
Bridge
Decouple an abstraction from its implementation so that the two can vary independently.
Let's say you want to avoid permanent binding between an abstraction and its implementation. That is what Bridge is for.
classDiagram class Abstraction{ <<interface>> Operation() } class ConcreteAbstractionA{ implementation Implementation Operation() } class ConcreteAbstractionB{ implementation Implementation Operation() } class Implementation{ <<interface>> OperationImplementation() } class ConcreteImplementationA{ OperationImplementation() } class ConcreteImplementationB{ OperationImplementation() } Abstraction o--> Implementation note for Abstraction "implementation.OperationImplementation()" Abstraction <|-- ConcreteAbstractionA Abstraction <|-- ConcreteAbstractionB Implementation <|-- ConcreteImplementationA Implementation <|-- ConcreteImplementationB
Go
type Abstraction interface {
Operation()
}
type ConcreteAbstractionA struct {
implementation Implementation
}
func (aa *ConcreteAbstractionA) Operation() {
aa.implementation.OperationImplementation()
}
type ConcreteAbstractionB struct {
implementation Implementation
}
func (ab *ConcreteAbstractionB) Operation() {
ab.implementation.OperationImplementation()
}
type Implementation interface {
OperationImplementation()
}
type ConcreteImplementationA struct{}
func (cia *ConcreteImplementationA) OperationImplementation() {
fmt.Println("ConcreteImplementationA operation")
}
type ConcreteImplementationB struct{}
func (cib *ConcreteImplementationB) OperationImplementation() {
fmt.Println("ConcreteImplementationB operation")
}
func main() {
concreteImplementationA := &ConcreteImplementationA{}
concreteImplementationB := &ConcreteImplementationB{}
concreteAbstractionA := &ConcreteAbstractionA{implementation: concreteImplementationA}
concreteAbstractionB := &ConcreteAbstractionB{implementation: concreteImplementationB}
concreteAbstractionA.Operation()
concreteAbstractionB.Operation()
}
TypeScript
interface Abstraction {
operation(): void;
}
class ConcreteAbstractionA implements Abstraction {
constructor(private implementation: Implementation) {}
public operation(): void {
this.implementation.operationImplementation();
}
}
class ConcreteAbstractionB implements Abstraction {
constructor(private implementation: Implementation) {}
public operation(): void {
this.implementation.operationImplementation();
}
}
interface Implementation {
operationImplementation(): void;
}
class ConcreteImplementationA implements Implementation {
public operationImplementation(): void {
console.log("ConcreteImplementationA operation");
}
}
class ConcreteImplementationB implements Implementation {
public operationImplementation(): void {
console.log("ConcreteImplementationB operation");
}
}
const concreteImplementationA = new ConcreteImplementationA();
const concreteImplementationB = new ConcreteImplementationB();
const concreteAbstractionA = new ConcreteAbstractionA(concreteImplementationA);
const concreteAbstractionB = new ConcreteAbstractionB(concreteImplementationB);
concreteAbstractionA.operation();
concreteAbstractionB.operation();
Composite
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
Just like tree data structure. The parent leaf operates its children's operations.
classDiagram class Component{ <<interface>> Operation() } class Leaf{ Operation() } class Composite{ children []Component Add(Component) Remove(Component) Operation() } Component <|-- Leaf Composite o--> Component note for Composite "for child range children\nchild.Operation()"
Go
type Component interface {
Operation()
}
type Leaf struct{}
func (leaf *Leaf) Operation() {
fmt.Println("Leaf Operation")
}
type Composite struct {
children []Component
}
func (composite *Composite) Add(child Component) {
composite.children = append(composite.children, child)
}
func (composite *Composite) Remove(child Component) {
for i, c := range composite.children {
if c == child {
composite.children = append(composite.children[:i], composite.children[i+1:]...)
return
}
}
}
func (composite *Composite) Operation() {
fmt.Println("Composite Operation")
for _, child := range composite.children {
child.Operation()
}
}
func main() {
root := &Composite{}
root.Add(&Leaf{})
branch := &Composite{}
branch.Add(&Leaf{})
branch.Add(&Leaf{})
root.Add(branch)
root.Operation()
}
TypeScript
interface Component {
operation(): void;
}
class Leaf implements Component {
operation() {
console.log("Leaf Operation");
}
}
class Composite implements Component {
private children: Component[] = [];
add(child: Component) {
this.children.push(child);
}
remove(child: Component) {
const index = this.children.indexOf(child);
if (index > -1) {
this.children.splice(index, 1);
}
}
operation() {
console.log("Composite Operation");
this.children.forEach(child => child.operation());
}
}
const root: Composite = new Composite();
root.add(new Leaf());
const branch: Composite = new Composite();
branch.add(new Leaf());
branch.add(new Leaf());
root.add(branch);
root.operation();
Decorator
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
There is a Decorator to wrap a component, just like a wrapper.
classDiagram class Component{ <<interface>> Operation() string } class ConcreteComponent{ Operation() string } class Decorator{ <<interface>> SetComponent(Component) } class ConcreteDecoratorA{ component Component SetComponent(Component) Operation() string } class ConcreteDecoratorB{ component Component SetComponent(Component) Operation() string } Component <|-- ConcreteComponent Decorator <|-- ConcreteDecoratorA Decorator <|-- ConcreteDecoratorB Decorator o--> Component note for Decorator "component.Operation()"
Go
type Component interface {
Operation() string
}
type ConcreteComponent struct{}
func (cc *ConcreteComponent) Operation() string {
return "ConcreteComponent"
}
type Decorator interface {
Component
SetComponent(Component)
}
type ConcreteDecoratorA struct {
component Component
}
func (cda *ConcreteDecoratorA) SetComponent(component Component) {
cda.component = component
}
func (cda *ConcreteDecoratorA) Operation() string {
return fmt.Sprintf("ConcreteDecoratorA(%s)", cda.component.Operation())
}
type ConcreteDecoratorB struct {
component Component
}
func (cdb *ConcreteDecoratorB) SetComponent(component Component) {
cdb.component = component
}
func (cdb *ConcreteDecoratorB) Operation() string {
return fmt.Sprintf("ConcreteDecoratorB(%s)", cdb.component.Operation())
}
func main() {
component := &ConcreteComponent{}
decoratorA := &ConcreteDecoratorA{}
decoratorB := &ConcreteDecoratorB{}
decoratorA.SetComponent(component)
decoratorB.SetComponent(decoratorA)
result := decoratorB.Operation()
fmt.Println(result)
}
TypeScript
interface Component {
operation(): string;
}
class ConcreteComponent implements Component {
operation() {
return "ConcreteComponent";
}
}
interface Decorator extends Component {
setComponent(component: Component): void;
}
class ConcreteDecoratorA implements Decorator {
private component: Component;
setComponent(component: Component) {
this.component = component;
}
operation() {
return `ConcreteDecoratorA(${this.component.operation()})`;
}
}
class ConcreteDecoratorB implements Decorator {
private component: Component;
setComponent(component: Component) {
this.component = component;
}
operation() {
return `ConcreteDecoratorB(${this.component.operation()})`;
}
}
const component: Component = new ConcreteComponent();
const decoratorA: Decorator = new ConcreteDecoratorA();
const decoratorB: Decorator = new ConcreteDecoratorB();
decoratorA.setComponent(component);
decoratorB.setComponent(decoratorA);
const result: string = decoratorB.operation();
console.log(result);
Facade
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
A Facade depends on some objects or abstractions.
classDiagram PaymentFacade ..> Account PaymentFacade ..> Wallet PaymentFacade ..> Security
Go
type PaymentFacade struct {
acc *Account
wallet *Wallet
security *Security
}
func (p *PaymentFacade) MakePayment(amount float64) error {
p.acc.checkBalance(amount)
p.wallet.debit(amount)
p.security.verify()
fmt.Printf("Payment of $%.2f has been made\n", amount)
return nil
}
type Account struct {
balance float64
}
func (a *Account) checkBalance(amount float64) error {
if a.balance < amount {
return fmt.Errorf("not enough balance")
}
return nil
}
type Wallet struct {
balance float64
}
func (w *Wallet) debit(amount float64) error {
if w.balance < amount {
return fmt.Errorf("not enough balance")
}
w.balance -= amount
return nil
}
type Security struct{}
func (s *Security) verify() error {
fmt.Println("Security verification complete")
return nil
}
func main() {
acc := &Account{1000.0}
wallet := &Wallet{500.0}
security := &Security{}
payment := &PaymentFacade{acc, wallet, security}
err := payment.MakePayment(750.0)
if err != nil {
fmt.Printf("Error: %s\n", err)
}
}
TypeScript
class PaymentFacade {
private account: Account;
private wallet: Wallet;
private security: Security;
constructor() {
this.account = new Account(1000);
this.wallet = new Wallet(500);
this.security = new Security();
}
public makePayment(amount: number): void {
try {
this.account.checkBalance(amount);
this.wallet.debit(amount);
this.security.verify();
console.log(`Payment of $${amount.toFixed(2)} has been made`);
} catch (err) {
console.error(`Error: ${err}`);
}
}
}
class Account {
private balance: number;
constructor(balance: number) {
this.balance = balance;
}
public checkBalance(amount: number): void {
if (this.balance < amount) {
throw new Error('not enough balance');
}
}
}
class Wallet {
private balance: number;
constructor(balance: number) {
this.balance = balance;
}
public debit(amount: number): void {
if (this.balance < amount) {
throw new Error('not enough balance');
}
this.balance -= amount;
}
}
class Security {
public verify(): void {
console.log('Security verification complete');
}
}
const payment = new PaymentFacade();
payment.makePayment(750.0);
Flyweight
Use sharing to support large numbers of fine-grained objects efficiently.
A Flyweight could be effective when an application uses a large number of objects and doesn't depend on object identity because it uses shared flyweight objects.
classDiagram class Flyweight{ <<interface>> Operation() } class ConcreteFlyweight{ state string Operation() } class FlyweightFactory{ flyweights Flyweights GetFlyweight(key string) Flyweight } Flyweight <|-- ConcreteFlyweight FlyweightFactory --> Flyweight
Go
type Flyweight interface {
Operation()
}
type ConcreteFlyweight struct {
state string
}
func (f *ConcreteFlyweight) Operation() {
fmt.Printf("ConcreteFlyweight: %s\n", f.state)
}
type FlyweightFactory struct {
flyweights map[string]Flyweight
}
func (f *FlyweightFactory) GetFlyweight(key string) Flyweight {
if flyweight, ok := f.flyweights[key]; ok {
return flyweight
}
flyweight := &ConcreteFlyweight{state: key}
f.flyweights[key] = flyweight
return flyweight
}
func NewFlyweightFactory() *FlyweightFactory {
return &FlyweightFactory{
flyweights: make(map[string]Flyweight),
}
}
func main() {
factory := NewFlyweightFactory()
flyweight1 := factory.GetFlyweight("key1")
flyweight2 := factory.GetFlyweight("key2")
flyweight3 := factory.GetFlyweight("key1")
flyweight1.Operation()
flyweight2.Operation()
flyweight3.Operation()
fmt.Println(len(factory.flyweights))
}
TypeScript
interface Flyweight {
operation(): void;
}
class ConcreteFlyweight implements Flyweight {
private state: string;
constructor(state: string) {
this.state = state;
}
public operation(): void {
console.log(`ConcreteFlyweight: ${this.state}`);
}
}
class FlyweightFactory {
private flyweights: { [key: string]: Flyweight } = {};
public getFlyweight(key: string): Flyweight {
if (key in this.flyweights) {
return this.flyweights[key];
}
const flyweight = new ConcreteFlyweight(key);
this.flyweights[key] = flyweight;
return flyweight;
}
public getFlyweightCount(): number {
return Object.keys(this.flyweights).length;
}
}
const factory = new FlyweightFactory();
const flyweight1 = factory.getFlyweight("key1");
const flyweight2 = factory.getFlyweight("key2");
const flyweight3 = factory.getFlyweight("key1");
flyweight1.operation();
flyweight2.operation();
flyweight3.operation();
console.log(factory.getFlyweightCount());
Proxy
Provide a surrogate or placeholder for another object to control access to it.
We need to pass a proxy before the real subject.
classDiagram class Subject{ <<interface>> Do() string } class RealSubject{ Do() string } class Proxy{ realSubject RealSubject Do() string } Subject <|-- RealSubject Proxy --> RealSubject
Go
type Subject interface {
Do() string
}
type RealSubject struct{}
func (s *RealSubject) Do() string {
return "RealSubject: doing something"
}
type Proxy struct {
realSubject *RealSubject
}
func (p *Proxy) Do() string {
if p.realSubject == nil {
p.realSubject = &RealSubject{}
}
result := "Proxy: calling the real subject to execute a task\n"
result += p.realSubject.Do()
return result
}
func main() {
var subject Subject = &Proxy{}
result := subject.Do()
fmt.Println(result)
}
TypeScript
interface Subject {
do(): string;
}
class RealSubject implements Subject {
public do(): string {
return "RealSubject: doing something";
}
}
class _Proxy implements Subject {
private realSubject: RealSubject | null = null;
public do(): string {
if (!this.realSubject) {
this.realSubject = new RealSubject();
}
let result = "Proxy: calling the real subject to execute a task\n";
result += this.realSubject.do();
return result;
}
}
const subject: Subject = new _Proxy();
const result = subject.do();
console.log(result);
Behavioral Patterns
Behavioral patterns are concerned with algorithms and the assignment of responsibili- ties between objects. Behavioral patterns describe not just patterns of objects or classes but also the patterns of communication between them. These patterns characterize complex control flow that's difficult to follow at run-time. They shift your focus away from flow of control to let you concentratejust on the way objects are interconnected.
Chain of Responsibility
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
We can see from the example below that ConcreteHandler1
and ConcreteHandler2
are chained. The ConcreteHandler2
would handle the request if ConcreteHandler1
couldn't, and so on.
classDiagram class Handler{ <<interface>> HandleRequest(request int) SetNextHandler(handler Handler) } class ConcreteHandler1{ nextHandler Handler HandleRequest(request int) SetNextHandler(handler Handler) } class ConcreteHandler2{ nextHandler Handler HandleRequest(request int) SetNextHandler(handler Handler) } class Client{ handler Handler MakeRequest(request int) } Handler <|-- ConcreteHandler1 Handler <|-- ConcreteHandler2 Client --> Handler Handler --> Handler : successor
Go
type Handler interface {
HandleRequest(request int)
SetNextHandler(handler Handler)
}
type ConcreteHandler1 struct {
nextHandler Handler
}
func (c *ConcreteHandler1) HandleRequest(request int) {
switch {
case request >= 0 && request < 10:
fmt.Printf("Request %d handled by ConcreteHandler1\n", request)
case c.nextHandler != nil:
c.nextHandler.HandleRequest(request)
default:
fmt.Println("Request cannot be handled by any handler")
}
}
func (c *ConcreteHandler1) SetNextHandler(handler Handler) {
c.nextHandler = handler
}
type ConcreteHandler2 struct {
nextHandler Handler
}
func (c *ConcreteHandler2) HandleRequest(request int) {
switch {
case request >= 10 && request < 20:
fmt.Printf("Request %d handled by ConcreteHandler2\n", request)
case c.nextHandler != nil:
c.nextHandler.HandleRequest(request)
default:
fmt.Println("Request cannot be handled by any handler")
}
}
func (c *ConcreteHandler2) SetNextHandler(handler Handler) {
c.nextHandler = handler
}
type Client struct {
handler Handler
}
func (c *Client) MakeRequest(request int) {
c.handler.HandleRequest(request)
}
func main() {
handler1 := &ConcreteHandler1{}
handler2 := &ConcreteHandler2{}
handler1.SetNextHandler(handler2)
client := &Client{handler: handler1}
client.MakeRequest(5)
client.MakeRequest(15)
client.MakeRequest(25)
}
TypeScript
interface Handler {
handleRequest(request: number): void;
setNextHandler(handler: Handler): void;
}
class ConcreteHandler1 implements Handler {
private nextHandler: Handler;
public handleRequest(request: number): void {
if (request >= 0 && request < 10) {
console.log(`Request ${request} handled by ConcreteHandler1`);
} else if (this.nextHandler != null) {
this.nextHandler.handleRequest(request);
} else {
console.log("Request cannot be handled by any handler");
}
}
public setNextHandler(handler: Handler): void {
this.nextHandler = handler;
}
}
class ConcreteHandler2 implements Handler {
private nextHandler: Handler;
public handleRequest(request: number): void {
if (request >= 10 && request < 20) {
console.log(`Request ${request} handled by ConcreteHandler2`);
} else if (this.nextHandler != null) {
this.nextHandler.handleRequest(request);
} else {
console.log("Request cannot be handled by any handler");
}
}
public setNextHandler(handler: Handler): void {
this.nextHandler = handler;
}
}
class Client {
private handler: Handler;
constructor(handler: Handler) {
this.handler = handler;
}
public makeRequest(request: number): void {
this.handler.handleRequest(request);
}
}
const handler1: Handler = new ConcreteHandler1();
const handler2: Handler = new ConcreteHandler2();
handler1.setNextHandler(handler2);
const client: Client = new Client(handler1);
client.makeRequest(5);
client.makeRequest(15);
client.makeRequest(25);
Command
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
classDiagram class Command{ <<interface>> Execute() } class ConcreteCommand{ message string Execute() } class Invoker{ command Command SetCommand(Command) ExecuteCommand() } Command <|-- ConcreteCommand Command o--> Invoker
Go
type Command interface {
Execute()
}
type ConcreteCommand struct {
message string
}
func (c *ConcreteCommand) Execute() {
fmt.Println(c.message)
}
type Invoker struct {
command Command
}
func (i *Invoker) SetCommand(c Command) {
i.command = c
}
func (i *Invoker) ExecuteCommand() {
i.command.Execute()
}
func main() {
concreteCommand := &ConcreteCommand{message: "Hello World!"}
invoker := &Invoker{}
invoker.SetCommand(concreteCommand)
invoker.ExecuteCommand()
}
TypeScript
interface Command {
execute(): void;
}
class ConcreteCommand implements Command {
constructor(private message: string) {}
execute(): void {
console.log(this.message);
}
}
class Invoker {
private command: Command;
setCommand(c: Command) {
this.command = c;
}
executeCommand() {
this.command.execute();
}
}
const concreteCommand = new ConcreteCommand('Hello World!');
const invoker = new Invoker();
invoker.setCommand(concreteCommand);
invoker.executeCommand();
Interpreter
Given a language, define a represention for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
The example below is about a calculator with basic calculations. I think that was a good example of an Interpreter.
classDiagram class Expression{ <<interface>> Interpret() int } class Number{ value int Interpret() int } class Add{ left Expression right Expression Interpret() int } class Subtract{ left Expression right Expression Interpret() int } class Multiply{ left Expression right Expression Interpret() int } class Divide{ left Expression right Expression Interpret() int } Expression <|-- Number Expression <|-- Add Expression <|-- Subtract Expression <|-- Multiply Expression <|-- Divide
Go
type Expression interface {
Interpret() int
}
type Number struct {
value int
}
func (n *Number) Interpret() int {
return n.value
}
type Add struct {
left Expression
right Expression
}
func (a *Add) Interpret() int {
return a.left.Interpret() + a.right.Interpret()
}
type Subtract struct {
left Expression
right Expression
}
func (s *Subtract) Interpret() int {
return s.left.Interpret() - s.right.Interpret()
}
type Multiply struct {
left Expression
right Expression
}
func (m *Multiply) Interpret() int {
return m.left.Interpret() * m.right.Interpret()
}
type Divide struct {
left Expression
right Expression
}
func (d *Divide) Interpret() int {
return d.left.Interpret() / d.right.Interpret()
}
func main() {
expression := &Add{
left: &Number{value: 10},
right: &Multiply{
left: &Number{value: 2},
right: &Subtract{left: &Number{value: 5}, right: &Number{value: 3}},
},
}
result := expression.Interpret()
fmt.Printf("Result: %d\n", result)
}
TypeScript
interface Expression {
interpret(): number;
}
class Value implements Expression {
value: number;
constructor(value: number) {
this.value = value;
}
interpret(): number {
return this.value;
}
}
class Add implements Expression {
left: Expression;
right: Expression;
constructor(left: Expression, right: Expression) {
this.left = left;
this.right = right;
}
interpret(): number {
return this.left.interpret() + this.right.interpret();
}
}
class Subtract implements Expression {
left: Expression;
right: Expression;
constructor(left: Expression, right: Expression) {
this.left = left;
this.right = right;
}
interpret(): number {
return this.left.interpret() - this.right.interpret();
}
}
class Multiply implements Expression {
left: Expression;
right: Expression;
constructor(left: Expression, right: Expression) {
this.left = left;
this.right = right;
}
interpret(): number {
return this.left.interpret() * this.right.interpret();
}
}
class Divide implements Expression {
left: Expression;
right: Expression;
constructor(left: Expression, right: Expression) {
this.left = left;
this.right = right;
}
interpret(): number {
return this.left.interpret() / this.right.interpret();
}
}
const expression: Expression = new Add(
new Value(10),
new Multiply(
new Value(2),
new Subtract(new Value(5), new Value(3)),
),
);
const result: number = expression.interpret();
console.log(`Result: ${result}`);
Iterator
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
It is an abstraction to iterate objects.
classDiagram class Iterator{ <<interface>> HasNext() bool Next() any } class ConcreteAggregate{ items []any CreateIterator() Iterator } class ConcreteIterator{ aggregate ConcreteAggregate index int HasNext() bool Next() any } Iterator <|-- ConcreteIterator ConcreteAggregate <-- ConcreteIterator
Go
type Iterator interface {
HasNext() bool
Next() interface{}
}
type ConcreteIterator struct {
aggregate *ConcreteAggregate
index int
}
func (i *ConcreteIterator) HasNext() bool {
return i.index < len(i.aggregate.items)
}
func (i *ConcreteIterator) Next() interface{} {
if !i.HasNext() {
return nil
}
item := i.aggregate.items[i.index]
i.index++
return item
}
type ConcreteAggregate struct {
items []interface{}
}
func (a *ConcreteAggregate) CreateIterator() Iterator {
return &ConcreteIterator{
aggregate: a,
index: 0,
}
}
func main() {
aggregate := &ConcreteAggregate{
items: []interface{}{"A", "B", "C", "D", "E"},
}
iterator := aggregate.CreateIterator()
for iterator.HasNext() {
item := iterator.Next()
fmt.Printf("%s ", item)
}
}
TypeScript
interface Iterator<T> {
hasNext(): boolean;
next(): T;
}
interface Aggregate<T> {
createIterator(): Iterator<T>;
}
class ConcreteAggregate implements Aggregate<number> {
private numbers: number[];
constructor(numbers: number[]) {
this.numbers = numbers;
}
createIterator(): Iterator<number> {
return new ConcreteIterator(this);
}
getNumbers(): number[] {
return this.numbers;
}
}
class ConcreteIterator implements Iterator<number> {
private collection: ConcreteAggregate;
private index: number;
constructor(collection: ConcreteAggregate) {
this.collection = collection;
this.index = 0;
}
hasNext(): boolean {
return this.index < this.collection.getNumbers().length;
}
next(): number {
if (!this.hasNext()) {
return null;
}
const num = this.collection.getNumbers()[this.index];
this.index++;
return num;
}
}
const concreteAggregate = new ConcreteAggregate([1, 2, 3, 4, 5]);
const iterator = concreteAggregate.createIterator();
while (iterator.hasNext()) {
const num = iterator.next();
console.log(num);
}
Mediator
Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
A Mediator mediates object interaction.
classDiagram class Mediator{ <<interface>> SendMessage(sender Colleague, message string) } class ConcreteMediator{ colleague1 Colleague colleague2 Colleague SendMessage(sender Colleague, message string) } class Colleague{ <<interface>> SendMessage(message string) Notify(message string) } class ConcreteColleague1{ mediator Mediator SendMessage(message string) Notify(message string) } class ConcreteColleague2{ mediator Mediator SendMessage(message string) Notify(message string) } Colleague <|-- ConcreteColleague1 Colleague <|-- ConcreteColleague2 Mediator <|-- ConcreteMediator Mediator <-- Colleague ConcreteMediator --> ConcreteColleague1 ConcreteMediator --> ConcreteColleague2
Go
type Mediator interface {
SendMessage(sender Colleague, message string)
}
type ConcreteMediator struct {
colleague1 Colleague
colleague2 Colleague
}
func (m *ConcreteMediator) SendMessage(sender Colleague, message string) {
if sender == m.colleague1 {
m.colleague2.Notify(message)
} else {
m.colleague1.Notify(message)
}
}
type Colleague interface {
SendMessage(message string)
Notify(message string)
}
type ConcreteColleague1 struct {
mediator Mediator
}
func (c *ConcreteColleague1) SendMessage(message string) {
c.mediator.SendMessage(c, message)
}
func (c *ConcreteColleague1) Notify(message string) {
fmt.Printf("ConcreteColleague1 received message: %s\n", message)
}
type ConcreteColleague2 struct {
mediator Mediator
}
func (c *ConcreteColleague2) SendMessage(message string) {
c.mediator.SendMessage(c, message)
}
func (c *ConcreteColleague2) Notify(message string) {
fmt.Printf("ConcreteColleague2 received message: %s\n", message)
}
func main() {
mediator := &ConcreteMediator{}
colleague1 := &ConcreteColleague1{mediator: mediator}
colleague2 := &ConcreteColleague2{mediator: mediator}
mediator.colleague1 = colleague1
mediator.colleague2 = colleague2
colleague1.SendMessage("Hello, colleague2!")
colleague2.SendMessage("Hi there, colleague1!")
}
TypeScript
interface Mediator {
sendMessage(sender: Colleague, message: string): void;
}
class ConcreteMediator implements Mediator {
private colleague1!: Colleague;
private colleague2!: Colleague;
setColleague1(colleague: Colleague): void {
this.colleague1 = colleague;
}
setColleague2(colleague: Colleague): void {
this.colleague2 = colleague;
}
sendMessage(sender: Colleague, message: string): void {
if (sender === this.colleague1) {
this.colleague2.notify(message);
} else {
this.colleague1.notify(message);
}
}
}
interface Colleague {
sendMessage(message: string): void;
notify(message: string): void;
}
class ConcreteColleague1 implements Colleague {
private mediator!: Mediator;
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
sendMessage(message: string): void {
this.mediator.sendMessage(this, message);
}
notify(message: string): void {
console.log(`ConcreteColleague1 received message: ${message}`);
}
}
class ConcreteColleague2 implements Colleague {
private mediator!: Mediator;
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
sendMessage(message: string): void {
this.mediator.sendMessage(this, message);
}
notify(message: string): void {
console.log(`ConcreteColleague2 received message: ${message}`);
}
}
const mediator: Mediator = new ConcreteMediator();
const colleague1: Colleague = new ConcreteColleague1();
const colleague2: Colleague = new ConcreteColleague2();
mediator.setColleague1(colleague1);
mediator.setColleague2(colleague2);
colleague1.setMediator(mediator);
colleague2.setMediator(mediator);
colleague1.sendMessage("Hello, colleague2!");
colleague2.sendMessage("Hi there, colleague1!");
Memento
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
In the example below, the Memento
has one property, Text
, to store the internal state of the Editor
. So, the Caretaker
could restore the state of the Editor
later.
classDiagram class Editor{ text string Write(text string) Read() string } class Memento{ text string Text() string } class Caretaker{ memento Memento Save(Editor) Restor(Editor) } Memento <--o Caretaker Editor ..> Memento
Go
type Editor struct {
text string
}
func (e *Editor) Write(text string) {
e.text += text
}
func (e *Editor) Read() string {
return e.text
}
type Memento struct {
text string
}
func (m *Memento) Text() string {
return m.text
}
type Caretaker struct {
memento *Memento
}
func (c *Caretaker) Save(editor *Editor) {
c.memento = &Memento{text: editor.Read()}
}
func (c *Caretaker) Restore(editor *Editor) {
editor.Write(c.memento.Text())
}
func main() {
editor := &Editor{}
caretaker := &Caretaker{}
editor.Write("Hello, ")
caretaker.Save(editor)
editor.Write("world!")
fmt.Println(editor.Read())
caretaker.Restore(editor)
fmt.Println(editor.Read())
}
TypeScript
class Editor {
private text: string = '';
write(text: string) {
this.text += text;
}
read(): string {
return this.text;
}
}
class Memento {
constructor(private text: string) {}
getText(): string {
return this.text;
}
}
class Caretaker {
private memento: Memento | null = null;
save(editor: Editor) {
this.memento = new Memento(editor.read());
}
restore(editor: Editor) {
if (this.memento) {
editor.write(this.memento.getText());
}
}
}
const editor = new Editor();
const caretaker = new Caretaker();
editor.write('Hello, ');
caretaker.save(editor);
editor.write('world!');
console.log(editor.read());
caretaker.restore(editor);
console.log(editor.read());
Observer
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
You can see that MessagePublisher
has many observers. They will be updated on Notify()
.
classDiagram class Subject{ <<interface>> Attach(Observer) Detach(Observer) Notify(message string) } class MessagePublisher{ observers []Observer Attach(Observer) Detach(Observer) Notify(message string) } class Observer{ <<interface>> Update(message string) } class MessageSubscriber{ name string Update(message string) } Subject <|-- MessagePublisher Observer <|-- MessageSubscriber Subject o--> Observer note for Subject "for observer range observers\nobserver.Update()"
Go
type Subject interface {
Attach(observer Observer)
Detach(observer Observer)
Notify(message string)
}
type Observer interface {
Update(message string)
}
type MessagePublisher struct {
observers []Observer
}
func (mp *MessagePublisher) Attach(observer Observer) {
mp.observers = append(mp.observers, observer)
}
func (mp *MessagePublisher) Detach(observer Observer) {
for i, obs := range mp.observers {
if obs == observer {
mp.observers = append(mp.observers[:i], mp.observers[i+1:]...)
break
}
}
}
func (mp *MessagePublisher) Notify(message string) {
for _, observer := range mp.observers {
observer.Update(message)
}
}
type MessageSubscriber struct {
name string
}
func (ms *MessageSubscriber) Update(message string) {
fmt.Printf("%s received message: %s\n", ms.name, message)
}
func main() {
publisher := &MessagePublisher{}
subscriber1 := &MessageSubscriber{name: "Subscriber 1"}
subscriber2 := &MessageSubscriber{name: "Subscriber 2"}
publisher.Attach(subscriber1)
publisher.Attach(subscriber2)
publisher.Notify("Hello, world!")
publisher.Detach(subscriber2)
publisher.Notify("How are you doing?")
}
TypeScript
interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(message: string): void;
}
interface Observer {
update(message: string): void;
}
class MessagePublisher implements Subject {
private observers: Observer[] = [];
attach(observer: Observer) {
this.observers.push(observer);
}
detach(observer: Observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notify(message: string) {
this.observers.forEach(observer => observer.update(message));
}
}
class MessageSubscriber implements Observer {
constructor(private name: string) {}
update(message: string) {
console.log(`${this.name} received message: ${message}`);
}
}
const publisher = new MessagePublisher();
const subscriber1 = new MessageSubscriber('Subscriber 1');
const subscriber2 = new MessageSubscriber('Subscriber 2');
publisher.attach(subscriber1);
publisher.attach(subscriber2);
publisher.notify('Hello, world!');
publisher.detach(subscriber2);
publisher.notify('How are you doing?');
State
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
classDiagram class State{ <<interface>> Handle() } class ConcreteStateA{ Handle() } class ConcreteStateB{ Handle() } class Context{ state State SetState(State) Request() } State <|-- ConcreteStateA State <|-- ConcreteStateB Context o--> State
Go
type State interface {
Handle()
}
type ConcreteStateA struct{}
func (s *ConcreteStateA) Handle() {
fmt.Println("Handle request with ConcreteStateA")
}
type ConcreteStateB struct{}
func (s *ConcreteStateB) Handle() {
fmt.Println("Handle request with ConcreteStateB")
}
type Context struct {
state State
}
func (c *Context) SetState(state State) {
c.state = state
}
func (c *Context) Request() {
c.state.Handle()
}
func main() {
context := &Context{}
stateA := &ConcreteStateA{}
stateB := &ConcreteStateB{}
context.SetState(stateA)
context.Request()
context.SetState(stateB)
context.Request()
}
TypeScript
interface State {
handle(): void;
}
class ConcreteStateA implements State {
handle(): void {
console.log("Handle request with ConcreteStateA");
}
}
class ConcreteStateB implements State {
handle(): void {
console.log("Handle request with ConcreteStateB");
}
}
class Context {
private state: State;
public setState(state: State) {
this.state = state;
}
public request() {
this.state.handle();
}
}
const context = new Context();
const stateA = new ConcreteStateA();
const stateB = new ConcreteStateB();
context.setState(stateA);
context.request();
context.setState(stateB);
context.request();
Strategy
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Let's say you want to execute something and have several algorithms. That is when you need a Strategy pattern.
classDiagram class Strategy{ <<interface>> Execute() } class ConcreteStrategyA{ Execute() } class ConcreteStrategyB{ Execute() } class Context{ strategy Strategy SetStrategy(strategy Strategy) ExecuteStrategy() } Strategy <|-- ConcreteStrategyA Strategy <|-- ConcreteStrategyB Context o--> Strategy
Go
type Strategy interface {
Execute()
}
type ConcreteStrategyA struct{}
func (s *ConcreteStrategyA) Execute() {
fmt.Println("Execute strategy A")
}
type ConcreteStrategyB struct{}
func (s *ConcreteStrategyB) Execute() {
fmt.Println("Execute strategy B")
}
type Context struct {
strategy Strategy
}
func (c *Context) SetStrategy(strategy Strategy) {
c.strategy = strategy
}
func (c *Context) ExecuteStrategy() {
c.strategy.Execute()
}
func main() {
context := &Context{}
strategyA := &ConcreteStrategyA{}
strategyB := &ConcreteStrategyB{}
context.SetStrategy(strategyA)
context.ExecuteStrategy()
context.SetStrategy(strategyB)
context.ExecuteStrategy()
}
TypeScript
interface Strategy {
execute(): void;
}
class ConcreteStrategyA implements Strategy {
execute(): void {
console.log("Execute strategy A");
}
}
class ConcreteStrategyB implements Strategy {
execute(): void {
console.log("Execute strategy B");
}
}
class Context {
private strategy: Strategy;
public setStrategy(strategy: Strategy) {
this.strategy = strategy;
}
public executeStrategy() {
this.strategy.execute();
}
}
const context = new Context();
const strategyA = new ConcreteStrategyA();
const strategyB = new ConcreteStrategyB();
context.setStrategy(strategyA);
context.executeStrategy();
context.setStrategy(strategyB);
context.executeStrategy();
Template Method
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
classDiagram class AbstractClass{ <<interface>> TemplateMethod() PrimitiveOperation1() PrimitiveOperation2() } class ConcreteClass{ TemplateMethod() PrimitiveOperation1() PrimitiveOperation2() } AbstractClass <|-- ConcreteClass note for AbstractClass "..\nPrimitiveOperation1()\nPrimitiveOperation2()\n.."
Go
type AbstractClass interface {
TemplateMethod()
PrimitiveOperation1()
PrimitiveOperation2()
}
type ConcreteClass struct{}
func (c *ConcreteClass) TemplateMethod() {
fmt.Println("ConcreteClass: template method starts")
c.PrimitiveOperation1()
c.PrimitiveOperation2()
fmt.Println("ConcreteClass: template method ends")
}
func (c *ConcreteClass) PrimitiveOperation1() {
fmt.Println("ConcreteClass: primitive operation 1")
}
func (c *ConcreteClass) PrimitiveOperation2() {
fmt.Println("ConcreteClass: primitive operation 2")
}
func main() {
concrete := &ConcreteClass{}
concrete.TemplateMethod()
}
TypeScript
abstract class AbstractClass {
public TemplateMethod(): void {
console.log("AbstractClass: template method starts");
this.PrimitiveOperation1();
this.PrimitiveOperation2();
console.log("AbstractClass: template method ends");
}
protected abstract PrimitiveOperation1(): void;
protected abstract PrimitiveOperation2(): void;
}
class ConcreteClass extends AbstractClass {
protected PrimitiveOperation1(): void {
console.log("ConcreteClass: primitive operation 1");
}
protected PrimitiveOperation2(): void {
console.log("ConcreteClass: primitive operation 2");
}
}
const concrete = new ConcreteClass();
concrete.TemplateMethod();
Visitor
Represent an operation to be performed on the elements of an object structure.Visitor lets you define a new operation without changing the classes of the elements on which it operates.
In the example below, ConcreteElementA
and ConcreteElementB
accept a Visitor
. They called visitors to visit themselves. Then the Visitor
does some stuff and operates the element.
classDiagram class Visitor{ <<interface>> VisitConcreteElementA(ConcreteElementA) VisitConcreteElementB(ConcreteElementB) } class Element{ <<interface>> Accept(Visitor) } class ConcreteElementA{ Accept(Visitor) OperationA() } class ConcreteElementB{ Accept(Visitor) OperationB() } class ConcreteVisitor1{ VisitConcreteElementA(ConcreteElementA) VisitConcreteElementB(ConcreteElementB) } class ConcreteVisitor2{ VisitConcreteElementA(ConcreteElementA) VisitConcreteElementB(ConcreteElementB) } Visitor <|-- ConcreteVisitor1 Visitor <|-- ConcreteVisitor2 Element <|-- ConcreteElementA Element <|-- ConcreteElementB
Go
type Visitor interface {
VisitConcreteElementA(element *ConcreteElementA)
VisitConcreteElementB(element *ConcreteElementB)
}
type Element interface {
Accept(visitor Visitor)
}
type ConcreteElementA struct{}
func (e *ConcreteElementA) Accept(visitor Visitor) {
visitor.VisitConcreteElementA(e)
}
func (e *ConcreteElementA) OperationA() {
fmt.Println("ConcreteElementA: operation A")
}
type ConcreteElementB struct{}
func (e *ConcreteElementB) Accept(visitor Visitor) {
visitor.VisitConcreteElementB(e)
}
func (e *ConcreteElementB) OperationB() {
fmt.Println("ConcreteElementB: operation B")
}
type ConcreteVisitor1 struct{}
func (v *ConcreteVisitor1) VisitConcreteElementA(element *ConcreteElementA) {
fmt.Println("ConcreteVisitor1: visiting ConcreteElementA")
element.OperationA()
}
func (v *ConcreteVisitor1) VisitConcreteElementB(element *ConcreteElementB) {
fmt.Println("ConcreteVisitor1: visiting ConcreteElementB")
element.OperationB()
}
type ConcreteVisitor2 struct{}
func (v *ConcreteVisitor2) VisitConcreteElementA(element *ConcreteElementA) {
fmt.Println("ConcreteVisitor2: visiting ConcreteElementA")
element.OperationA()
}
func (v *ConcreteVisitor2) VisitConcreteElementB(element *ConcreteElementB) {
fmt.Println("ConcreteVisitor2: visiting ConcreteElementB")
element.OperationB()
}
func main() {
elements := []Element{&ConcreteElementA{}, &ConcreteElementB{}}
visitor1 := &ConcreteVisitor1{}
visitor2 := &ConcreteVisitor2{}
for _, element := range elements {
element.Accept(visitor1)
element.Accept(visitor2)
}
}
TypeScript
interface Visitor {
visitConcreteElementA(element: ConcreteElementA): void;
visitConcreteElementB(element: ConcreteElementB): void;
}
interface Element {
accept(visitor: Visitor): void;
}
class ConcreteElementA implements Element {
accept(visitor: Visitor): void {
visitor.visitConcreteElementA(this);
}
operationA(): void {
console.log("ConcreteElementA: operation A");
}
}
class ConcreteElementB implements Element {
accept(visitor: Visitor): void {
visitor.visitConcreteElementB(this);
}
operationB(): void {
console.log("ConcreteElementB: operation B");
}
}
class ConcreteVisitor1 implements Visitor {
visitConcreteElementA(element: ConcreteElementA): void {
console.log("ConcreteVisitor1: visiting ConcreteElementA");
element.operationA();
}
visitConcreteElementB(element: ConcreteElementB): void {
console.log("ConcreteVisitor1: visiting ConcreteElementB");
element.operationB();
}
}
class ConcreteVisitor2 implements Visitor {
visitConcreteElementA(element: ConcreteElementA): void {
console.log("ConcreteVisitor2: visiting ConcreteElementA");
element.operationA();
}
visitConcreteElementB(element: ConcreteElementB): void {
console.log("ConcreteVisitor2: visiting ConcreteElementB");
element.operationB();
}
}
const elements: Element[] = [new ConcreteElementA(), new ConcreteElementB()];
const visitor1: Visitor = new ConcreteVisitor1();
const visitor2: Visitor = new ConcreteVisitor2();
for (const element of elements) {
element.accept(visitor1);
element.accept(visitor2);
}
Related Articles
- SOLID
- Next.js vs Hugo
- React Context with TypeScript
- Convert Any Images to JPEG and WebP, Compress, and Resize on the Client Side
- Build, Test, and Run Go Microservices with Bazel