Design patterns: summary of 11 Behaviroal behavioral design patterns

Design patterns: summary of 11 Behaviroal behavioral design patterns

Related series of articles

preface

General categoryCreate typeStructural typeBehavioral type
classFactory MethodAdapterInterpreter interpreter
Template Method template method
objectAbstract Factory
Builder builder
Prototype prototype
Singleton singleton
Adapter
Bridge bridging
Composite composition
Decorator decorator
Facade appearance
Flyweight yuan
Proxy proxy
Chain of Responsibility
Command command
Iterator iterator
Mediator
Memento memo
Observer observer
State state
Strategy strategy
Visitor visitor

Today, let's talk about the last part: 11 behavioral design patterns

text

0. Overview of behavioral design pattern

Creative mode is responsible for the creation of presentation objects

Structural pattern expresses the organizational structure, association and combination of objects

The behavioral pattern to be introduced today is a method description of interaction, call and delegation between objects. Let's discuss the usage scenarios and mode comparison of 11 modes respectively.

1. Chain of Responsibility

1.1 applicable scenarios

  • Multiple objects have the ability to process requests
  • When the receiver is not clear, the processing object decides who will handle it
  • The collection of request processing objects can be specified dynamically

1.2 mode structure

  • Handler Abstract request handler: defines the request processing interface
  • ConcreteHandler: implement specific request logic or forward requests

1.3 code example

1.3.1 handler definition

  • /src/behavioral/chain_of_responsibility/handlers.ts

First define the handler interface

export abstract class Handler {
  successor?: Handler

  constructor(successor?: Handler) {
    this.successor = successor
  }

  abstract handleRequest(target: RequestTargetType): void
}

Next are two specific handlers

export type RequestTargetType = 'A' | 'B'

export class ConcreteHandlerA extends Handler {
  constructor(successor?: Handler) {
    super(successor)
  }

  handleRequest(target: RequestTargetType) {
    if (target === 'A') {
      log('ConcreteHandlerA handle request')
    } else {
      log('ConcreteHandlerA pass')
      this.successor?.handleRequest(target)
    }
  }
}

export class ConcreteHandlerB extends Handler {
  constructor(successor?: Handler) {
    super(successor)
  }

  handleRequest(target: RequestTargetType) {
    if (target === 'B') {
      log('ConcreteHandlerB handle request')
    } else {
      log('ConcreteHandlerA pass')
      this.successor?.handleRequest(target)
    }
  }
}

The specific processing logic of two processors can choose to process by themselves or forward to subsequent processors

1.3.2 test & output

  • /src/behavioral/chain_of_responsibility/index.ts

Next, we create three types of handler objects

const handlerA = new ConcreteHandlerA()
const handlerB = new ConcreteHandlerB()
const handlerC = new ConcreteHandlerA(new ConcreteHandlerB())

Next, let's look at the processing of three objects for A and B requests

group("handlerA.handleRequest('A')", () => {
  handlerA.handleRequest('A')
})

group("handlerB.handleRequest('A')", () => {
  handlerB.handleRequest('A')
})

group("handlerC.handleRequest('A')", () => {
  handlerC.handleRequest('A')
})

group("handlerA.handleRequest('B')", () => {
  handlerA.handleRequest('B')
})

group("handlerB.handleRequest('B')", () => {
  handlerB.handleRequest('B')
})

group("handlerC.handleRequest('B')", () => {
  handlerC.handleRequest('B')
})

1.4 summary of effect and characteristics

  • Decoupling between requester and handler: the requester does not know who the specific handler is and the processing flow. It is good to decouple the two objects
  • Enhance the flexibility of object assignment: processing objects can be combined in any order to increase the flexibility of processing flow programming
  • Uncertain request processing: there is no way to ensure that the request must be processed because the request may be passed on indefinitely in the responsibility chain

2. Command mode

2.1 applicable scenarios

  • Suitable for the implementation of callback mechanism: first define, and then specify the function call location
  • It supports specifying, arranging and executing requests, that is, managing the request behavior itself
  • Undo operation is supported as the basis of command / transaction rollback
  • Support the modification log, abstract the behavior itself, and help memory
  • Create high-level primitives

2.2 mode structure

  • Command Abstract command interface: defines the command execution interface
  • ConcreteCommand: implements specific command operations
  • Receiver command receiver: it helps to complete the command or provides the interface required by the side effects of the command, that is, it can operate external objects
  • Invoker command trigger: execute the command after the command object is triggered according to a certain mechanism

2.3 code examples

2.3.1 Commands command definition

  • /src/behavioral/command/commands.ts

The first is the command interface

export interface Command {
  execute(): void
}

The first defines a simple command type

export class SimpleCommand implements Command {
  receiver: Application
  func: keyof ApplicationReceiver

  constructor(
    receiver: Application,
    func: keyof ApplicationReceiver
  ) {
    this.receiver = receiver
    this.func = func
  }

  execute() {
    log('SimpleCommand.execute')
    this.receiver[this.func]()
  }
}

The second is commands that use other context objects

export class CommandUsingContext implements Command {
  receiver: Application

  constructor(receiver: Application) {
    this.receiver = receiver
    this.receiver.context = new Context()
  }

  execute() {
    log('CommandUsingContext.execute')
    this.receiver.context?.commandRealize()
  }
}

The last is to define the type of composite command

export class MacroCommand implements Command {
  receiver: Application
  commands: Command[]

  constructor(receiver: Application, ...commands: Command[]) {
    this.receiver = receiver
    this.commands = commands
  }

  execute() {
    log('MacroCommand.execute')
    this.commands.forEach((command) => command.execute())
  }
}

2.3.2 Receiver command receiver

  • /src/behavioral/command/receivers.ts

Here, we use an Application type as the command receiver, and abstract the Application method interface ApplicationReceiver and the internal Context object Context

export class Context {
  commandRealize() {
    log('invoke Context.commandRealize')
  }
}

export interface ApplicationReceiver {
  commandRealize(): void
}

export class Application implements ApplicationReceiver {
  context?: Context

  commandRealize() {
    log('invoke Application.commandRealize')
  }
}

2.3.2 Invoker command trigger & Test & output

  • /src/behavioral/command/index.ts

Finally, our Invoker is responsible for issuing the command object

class Invoker {
  receiver: Application
  commands: Command[] = []

  constructor(receiver: Application) {
    this.receiver = receiver
  }

  simple() {
    const command = new SimpleCommand(this.receiver, 'commandRealize')
    this.commands.push(command)
    command.execute()
  }

  usingContext() {
    const command = new CommandUsingContext(this.receiver)
    this.commands.push(command)
    command.execute()
  }
}

And the final test code

const app = new Application()
const invoker = new Invoker(app)

invoker.simple()
invoker.usingContext()

2.4 summary of effect and characteristics

  • The calling operation object is decoupled from the calling implementation object: the Command encapsulates the description of the Command behavior, while the Receiver encapsulates the specific data change interface, just like the creator pattern of the behavior pattern version
  • Easy to extend, combine and compound commands: since the issued command itself is also an assembleable class, the reusability of command objects is improved

3. Interpreter mode

3.1 applicable scenarios

  • Analysis of simple grammar
  • Encapsulate mode conversion to improve interpretation efficiency

3.2 mode structure

  • AbstractExpression abstract expression: defines a common interface for expressions
  • NonterminalExpression: a high-level expression that indicates that there may be sub expressions
  • TerminalExpression end expression: as the most basic expression of AST leaf node
  • Context context object: it can record the traversal and parsing operations of AST, and convert the traversal of tree into structured sequential operations

3.3 code example

3.3.1 Expressions expression definition

  • /src/behavioral/interpreter/expressions.ts

First, the expression needs to be defined. The first is an abstract expression that exposes an interpretable interface

export interface Expression {
  interpret(context: Context): void
}

Next, we simulate a binary operation expression with Terminator (number) respectively

function record(msg: string, context: Context) {
  context.log.push(msg)
  log(`meet ${msg}`)
}

export class TerminalToken implements Expression {
  num: number

  constructor(num: number) {
    this.num = num
  }

  interpret(context: Context): void {
    record(`token(${this.num})`, context)
  }
}

And nonterminal expressions (binary expressions)

export class BinaryExpression implements Expression {
  x: TerminalToken
  y: TerminalToken
  sign: string

  constructor(x: TerminalToken, y: TerminalToken, sign: string) {
    this.x = x
    this.y = y
    this.sign = sign
  }

  interpret(context: Context) {
    const exp = `${this.x.num} ${this.sign} ${this.y.num}`
    const msg = `binaryExpression(${exp})`
    record(msg, context)

    this.x.interpret(context)
    log(`sign ${this.sign}`)
    this.y.interpret(context)
  }
}

3.3.2 Context object

  • /src/behavioral/interpreter/Context.ts

Next is the context object, which can be used to retain traversal information or records

export default class Context {
  log: string[] = []
}

3.3.3 test & output

  • /src/behavioral/interpreter/index.ts

Finally, we first create a method for constructing binary expressions (this part should normally be completed by syntax parsing)

function createBinaryExpression(s: string) {
  const [num1, sign, num2] = s.split(' ')
  const x = new TerminalToken(Number(num1))
  const y = new TerminalToken(Number(num2))
  const be = new BinaryExpression(x, y, sign)
  return be
}

Next, we test the two expressions and see the output

group('interpret expression1', () => {
  const context = new Context()
  const expression1 = createBinaryExpression('1 + 1')
  expression1.interpret(context)
  log('context:', context)
})

group('interpret expression2', () => {
  const context = new Context()
  const expression2 = createBinaryExpression('123 + 456')
  expression2.interpret(context)
  log('context:', context)
})

3.4 summary of effect and characteristics

  • Open to syntax change and extension: since the client is programmed through the abstract expression interface, the specific syntax and order are shielded from the client
  • Easy to implement Grammar: the expression object directly corresponds to the syntax object, which is easy to implement syntax parsing
  • Difficult to maintain complex grammar: it is difficult to maintain complex grammar

4. Iterator iterator mode

4.1 applicable scenarios

  • Access aggregate object content without exposing internal representations
  • Support multiple traversal modes
  • Provide a unified traversal interface for different aggregation structures

4.2 mode structure

  • Iterator iterator interface: declares the common interface of iterators
  • ConcreteIterator concrete iterator: it really traverses the aggregate object and implements the iterator interface. Usually, each aggregate object has an exclusive corresponding iterator
  • Aggregate Abstract aggregate object: declares the interface that creates the iterator
  • ConcreteAggregate concrete aggregate object: implements and returns an iterator object created from itself

4.3 code examples

4.3.1 Iterator Abstract iterator interface

  • /src/behavioral/iterator/Iterator.ts

First, we define the common iterative interface needed to traverse the object. Here, we use the paradigm feature of TS

export default interface Iterator<Data> {
  first(): Data | null
  current(): Data | null
  next(): Data | null
  isDone(): boolean
}

4.3.2 Aggregate aggregate object interface

  • /src/behavioral/iterator/Aggregate.ts

Next, define the interface of the aggregate object. Because it is used for testing, we only implement the add method. In fact, we can separate the interface of the aggregate object from the iterator interface and combine different interfaces through multiple implementations

export default interface Aggregate<Data> {
  add(data: Data): void
  iterator(): Iterator<Data>
}

4.3.3 ArrayList array implementation list

  • /src/behavioral/iterator/ArrayList.ts

Next, we implement an array implementation list, just like the ArrayList in Java (of course, in JS, the array itself has dynamically expanded its size, so it's really unnecessary hh)

export default class ArrayList<Data> implements Aggregate<Data> {
  dataList: Data[] = []

  add(data: Data) {
    this.dataList.push(data)
  }

  iterator() {
    return new ArrayListIterator<Data>(this.dataList)
  }
}

After the basic array list is completed, you need to implement a special iterator for this class, which can understand the internal representation of the aggregate object (that is, encapsulate the traversal of the internal representation)

export class ArrayListIterator<Data> implements Iterator<Data> {
  dataList: Data[]
  currentIdx: number

  constructor(dataList: Data[]) {
    this.dataList = dataList
    this.currentIdx = 0
  }

  first() {
    return this.dataList.length ? this.dataList[0] : null
  }

  current() {
    return this.isDone() ? null : this.dataList[this.currentIdx]
  }

  next() {
    if (!this.isDone()) {
      this.currentIdx++
      return this.dataList[this.currentIdx]
    }
    return null
  }

  isDone() {
    return this.currentIdx >= this.dataList.length
  }
}

4.3.4 LinkedList linked list implementation list

  • /src/behavioral/iterator/LinkedList.ts

The second aggregation object is a linked list, so we will not repeat it here and directly add the code

class Node<Data> {
  data: Data
  next: Node<Data> | null

  constructor(data: Data) {
    this.data = data
    this.next = null
  }
}

export default class LinkedList<Data> implements Aggregate<Data> {
  head: Node<Data> | null = null
  tail: Node<Data> | null = null

  add(data: Data) {
    if (!this.head) {
      this.tail = this.head = new Node(data)
    } else {
      this.tail!.next = new Node(data)
      this.tail = this.tail!.next
    }
  }

  iterator() {
    return new LinkedListIterator(this.head)
  }
}

And the iterator class specific to the linked list

class LinkedListIterator<Data> implements Iterator<Data> {
  head: Node<Data> | null
  currentNode: Node<Data> | null

  constructor(head: Node<Data> | null) {
    this.head = head
    this.currentNode = head
  }

  first() {
    return this.head ? this.head.data : null
  }

  current() {
    return this.currentNode ? this.currentNode.data : null
  }

  next() {
    if (!this.isDone()) {
      this.currentNode = this.currentNode!.next
    }
    return this.current()
  }

  isDone() {
    return !this.currentNode
  }
}

4.3.5 test & output

  • /src/behavioral/iterator/index.ts

Finally, we define a method to traverse the aggregate object through the interface

function traversal<Data>(iterator: Iterator<Data>) {
  let i = 0
  while (!iterator.isDone()) {
    log(`${i++}:`, iterator.current())
    iterator.next()
  }
}

Then we create two objects and call the method traversal

const arrayList: Aggregate<number> = new ArrayList<number>()
arrayList.add(1)
arrayList.add(3)
arrayList.add(5)
arrayList.add(7)
arrayList.add(9)

const linkedList: Aggregate<number> = new LinkedList<number>()
linkedList.add(2)
linkedList.add(4)
linkedList.add(6)
linkedList.add(8)
linkedList.add(10)

group('traversal arrayList', () => {
  traversal(arrayList.iterator())
})
group('traversal linkedList', () => {
  traversal(linkedList.iterator())
})

4.4 summary of effect and characteristics

  • Multiple traversal methods are supported: the specific traversal operations are encapsulated in the iterator and shielded from the user. Different traversal operations can be implemented by exposing different traversal interfaces
  • Simplify aggregate object interface: after independence from the iterator interface for traversal, you can simplify the interface used by the aggregate object itself to access content
  • Realize multiple traversal at the same time: since the return is a specific traversal "object", each object maintains its own traversal state and is independent of each other

5. Mediator model

5.1 applicable scenarios

  • Multiple components have complex interaction and close coupling
  • Direct communication between multiple objects leads to low component reusability
  • Customize the behavior of accessing components

5.2 mode structure

  • Collague cooperative object interface: in the mediator mode, we call components cooperative objects because we actually need multiple cooperative objects to complete tasks together, and we don't want the objects to be directly over coupled
  • Concrete collage specific cooperation objects: the communication between specific cooperation objects must pass through the intermediary object, which makes the interface and interface of each specific cooperation object realize operation, improve reusability, and reduce the coupling to communication targets
  • Mediator interface: defines the mediator interface, which is also the consumption target of the partner
  • ConcreteMediator specific mediator object: in fact, mediator objects often provide many interfaces for cooperative objects and are closely related to multiple cooperative objects. Therefore, the ease of use of abstract mediators is not necessarily very high when applying, and there is no need to separate an abstract mediator interface

5.3 code examples

5.3.1 collages partner definition

  • /src/behavioral/mediator/colleagues.ts

First of all, the most important thing in the mediator model is the specific cooperation object and its interface at the bottom. The first is the common interface of the cooperation object

export abstract class Colleague {
  mediator?: Mediator

  changed(): void {
    this.mediator?.colleagueChanged(this)
  }
}

Maintain the reference to the mediator through the abstract cooperation object. For the specific cooperation object, you only need to call the interface to notify the mediator object at a specific time

Here are three specific partners

export class Colleague1 extends Colleague {
  receive(invoker: string) {
    log(`Colleague1.receive from ${invoker}`)
  }

  broadCast() {
    this.changed()
  }
}

export class Colleague2 extends Colleague {
  receive(invoker: string) {
    log(`Colleague2.receive from ${invoker}`)
  }

  broadCast() {
    this.changed()
  }
}

export class Colleague3 extends Colleague {
  receive(invoker: string) {
    log(`Colleague3.receive from ${invoker}`)
  }

  broadCast() {
    this.changed()
  }
}

5.3.2 definition of mediator

  • /src/behavioral/mediator/Mediator.ts

The next is our protagonist, who is responsible for maintaining the cooperation of multiple partners. At the same time, it also means that the intermediary needs to know more information about the actual partners; Therefore, in fact, for the mediator object, it is meaningless to abstract the common interface for the cooperative object

export default class Mediator {
  colleague1: Colleague1
  colleague2: Colleague2
  colleague3: Colleague3

  constructor(c1: Colleague1, c2: Colleague2, c3: Colleague3) {
    this.colleague1 = c1
    this.colleague2 = c2
    this.colleague3 = c3
  }

  colleagueChanged(colleague: Colleague): void {
    log('passing message by mediator')
    if (colleague instanceof Colleague1) {
      const invoker = 'colleague1'
      this.colleague2.receive(invoker)
      this.colleague3.receive(invoker)
    } else if (colleague instanceof Colleague2) {
      const invoker = 'colleague2'
      this.colleague1.receive(invoker)
      this.colleague3.receive(invoker)
    } else if (colleague instanceof Colleague3) {
      const invoker = 'colleague3'
      this.colleague1.receive(invoker)
      this.colleague2.receive(invoker)
    } else {
      throw new Error('unknown Colleague type')
    }
  }
}

5.3.3 test & output

  • /src/behavioral/mediator/index.ts

Finally, we first combine a mediator object containing three partners

const c1 = new Colleague1()
const c2 = new Colleague2()
const c3 = new Colleague3()
const mediator = new Mediator(c1, c2, c3)
c1.mediator = mediator
c2.mediator = mediator
c3.mediator = mediator

Then trigger the additional interfaces of the three objects respectively to observe the information transmission between the objects

group('colleague1.broacast', () => {
  c1.broadCast()
})

group('colleague2.broacast', () => {
  c2.broadCast()
})

group('colleague3.broacast', () => {
  c3.broadCast()
})

5.4 summary of effect and characteristics

  • Reduce subclass generation: the communication and high-level logic of component objects are encapsulated in mediator objects, so the definition of cooperative objects can focus on component design with high reusability and high cohesion
  • Cooperation object decoupling: it shields the specific links between cooperation objects. All cooperation objects rely on intermediaries rather than interdependence
  • Simplified object protocol: instead of coupling between cooperative objects, the interface exposed by the mediator to specific cooperative objects greatly simplifies the communication width between objects to a certain extent
  • Centralization of control: the mediator object takes charge of all cooperative objects and the interaction between objects, that is, centralizing the control logic of the object into a single mediator object

6. Memento memo mode

6.1 applicable scenarios

  • Record object status and restore required (production snapshot, backup)
  • Encapsulate the internal state of the object to ensure data integrity and security

6.2 mode structure

  • Original object: an object that has its own internal state
  • Memento memo: an object that records a snapshot of the internal state of an object
  • CareTaker object manager: the manager responsible for retaining stored objects

6.3 code examples

6.3.1 original object definition of originator

  • /src/behavioral/memento/originators.ts

The first, of course, is our internal object. Here, we use a version attribute to symbolize the internal state of the object

export interface State {
  version: string
}

export class Originator {
  state: State = { version: 'v1.0.0' }

  upgrade(level: 'S' | 'M' | 'L' = 'S') {
    const [a, b, c] = this.state.version.split('.')
    let newVersion = this.state.version
    switch (level) {
      case 'S':
        newVersion = `${a}.${b}.${Number(c) + 1}`
        break
      case 'M':
        newVersion = `${a}.${Number(b) + 1}.${c}`
        break
      case 'L':
        newVersion = `${a[0]}${Number(a.substring(1)) + 1}.${b}.${c}`
        break
    }
    this.state.version = newVersion
  }

  createMemento(): Memento {
    log('memorized:', this.state)
    return new Memento(this.state)
  }

  restoreMemento(memento: Memento) {
    this.state = memento.state
  }
}

6.3.2 Memento memo & caretaker memo manager definition

  • /src/behavioral/memento/mementos.ts

Next is the memo object, which keeps the internal state record / snapshot of the original object

export class Memento {
  state: State

  constructor(state: State) {
    this.state = { ...state }
  }

  setState(state: State) {
    this.state = { ...state }
  }
}

And a manager object to save the state

export class CareTaker {
  memento: Memento | null = null

  getMemento() {
    return this.memento
  }

  setMemento(memento: Memento) {
    this.memento = memento
  }
}

In fact, the attributes in JS are all public, so the actual application scenario can be implemented by using closures

6.3.3 test & output

  • /src/behavioral/memento/index.ts

Next, we will simulate an application with iterative versions and revert to the version of the last snapshot at a certain time

const careTaker = new CareTaker()
const originator = new Originator()

log(originator)

originator.upgrade()
log(originator)
careTaker.setMemento(originator.createMemento())

originator.upgrade()
log(originator)

originator.upgrade()
log(originator)

const memo = careTaker.getMemento()
memo && originator.restoreMemento(memo)
log(originator)

6.4 summary of effect and characteristics

  • Keep encapsulation boundary: the purpose of memo mode is to shield the internal state from external objects and ensure the integrity and privacy of the internal state
  • There may be a huge cost: memos do not limit the size and number of states. You need to be very careful to save too many states / snapshots. There may be storage overhead problems (you can use the shared mode to improve)

7. Observer mode

7.1 applicable scenarios

  • The observer object depends on the target object, and you want to automatically notify all observers when the target object changes
  • The target object does not know how many objects depend on itself

7.2 mode structure

  • Observer observer object: it is responsible for maintaining the pointer to the target object and defining the common interface for observer update
  • ConcreteObserver: implements the response logic when the state of the observation target changes
  • Subject observable object interface: maintains an observer object queue and defines the notification method to be used when the state changes
  • ConcreteSubject concrete observable object: the concrete object logic is decoupled from the observer, and only needs to call the notification method at a specific time

7.3 code examples

7.3.1 Observers definition

  • /src/behavioral/observer/observers.ts

First of all, the protagonist of the observer pattern is our observer, which defines the interface and specific implementation in response to the change of the target object

export abstract class Observer {
  subject: Subject | null = null

  observe(subject: Subject) {
    if (this.subject) {
      this.subject.detach(this)
    }
    this.subject = subject
    this.subject.attach(this)
  }

  abstract update(): void
}

export class ConcreteObserver extends Observer {
  static count = 0
  id = ++ConcreteObserver.count

  update() {
    log(`ConcreteObserver(${this.id}) update`)
  }
}

7.3.2 Subjects observation target definition

  • /src/behavioral/observer/subjects.ts

The next step is the definition of observable objects. We need to carefully design the interface of observable objects to ensure the reusability of observer objects

First, abstract classes are used to automatically maintain the observer queue and notification model

export class Subject {
  observers: Observer[] = []

  attach(observer: Observer) {
    if (!this.observers.includes(observer)) {
      this.observers.push(observer)
    }
  }

  detach(observer: Observer) {
    if (this.observers.includes(observer)) {
      this.observers.splice(this.observers.indexOf(observer), 1)
    }
  }

  notify() {
    this.observers.forEach((observer) => observer.update())
  }
}

Next is the implementation of concrete observable objects

export class ConcreteSubject extends Subject {
  static count = 0
  id = ++ConcreteSubject.count

  change() {
    log(`ConcreteSubject(${this.id}) changed`)
    this.notify()
  }
}

The observer pattern is implemented by inheritance. In fact, composition can provide better flexibility and scalability

7.3.3 test & output

  • /src/behavioral/observer/index.ts

Finally, we define two observation targets and three observer objects respectively

const subject1 = new ConcreteSubject()
const subject2 = new ConcreteSubject()

const observer1 = new ConcreteObserver()
observer1.observe(subject1)
const observer2 = new ConcreteObserver()
observer2.observe(subject2)
const observer3 = new ConcreteObserver()
observer3.observe(subject2)

Then see what happens when the observation target changes (the observer is notified and acts accordingly)

subject1.change()
subject2.change()

7.4 summary of effect and characteristics

  • Abstract coupling of target and observer: the method that the target notifies the observer is only based on the interface, while the observer uniformly exposes and updates the interface, which is independent of the specific implementation
  • Support Broadcasting: since the observer does not know other observers of the same target, the target object can easily broadcast to all observers
  • Redundant update: since the observer status is shielded from the observation target, unnecessary observer updates may be caused. At this time, it is necessary to make a more detailed distinction between the observer status and the observation target status

8. State mode

8.1 applicable scenarios

  • The behavior of an object depends on its state and needs to change the state dynamically at run time
  • Multiple behaviors of objects have similar conditional branching structures about states

8.2 mode structure

  • State interface: an interface that declares state related operations
  • ConcreteState object: defines the specific operation logic in different states
  • Context object: an object that needs to show different behaviors according to different states, that is, the method logic abstraction depends on the state object

8.3 code examples

8.3.1 Context object definition

  • /src/behavioral/state/index.ts

First, let's clarify the logic of the context object: according to the connection to a remote object, you need to indicate whether the request is successful or not according to the connection status

export default class Context {
  state: State = new Disconnected()

  connect() {
    this.state = new Connected()
    log('Context is Connected')
  }

  request() {
    log('invoke Context.requrest')
    this.state.handle()
  }
}

8.3.2 States object definition

  • /src/behavioral/state/states.ts

In order to avoid redundant conditional branching statements, we extract the connection state into an independent object to show different behaviors

export interface State {
  handle(): void
}

export class Connected implements State {
  handle() {
    log('[Connected] handle success')
  }
}

export class Disconnected implements State {
  handle() {
    log('[Disconnected] handle fail')
  }
}

8.3.3 test & output

  • /src/behavioral/state/index.ts
const context = new Context()

context.request()
context.connect()
context.request()

8.4 summary of effect and characteristics

  • Local encapsulation of state and related behavior: encapsulate state and related behavior into an independent object
  • Explicit state transition: after the state is abstracted into an object, the state transition logic is clearer (extracting and changing the state object)
  • Shared state: the state without instance variable can be shared and reused by multiple objects

9. Strategy mode

9.1 applicable scenarios

  • There are only "behavior differences" among multiple related objects, that is, different decisions
  • Implement different variants and algorithms for the same algorithm
  • The algorithm uses data and logic to shield customers

9.2 mode structure

  • Strategy policy class: defines the interface used to process and call a certain algorithm
  • ConcreteStrategy specific policy class: implement specific algorithm logic
  • Context object: completes business requirements by using algorithm policy objects

9.3 code example

9.3.1 context object definition

  • /src/behavioral/strategy/Context.ts

Similarly, let's first clarify what business the context object is going to accomplish

export default class Context {
  name = 'sUpErFrEe'
  strategy: Strategy = new Default()

  setStrategy(strategy: Strategy) {
    this.strategy = strategy
  }

  toString() {
    return `{ name: ${this.strategy.operation(this.name)} }`
  }
}

In this example, you want to complete a formatted output operation for the internal string

9.3.2 policy definition

  • /src/behavioral/strategy/strategys.ts

Next, we define different policy class implementations for different formatting policies

The first is the abstract interface definition

export interface Strategy {
  operation(s: string): void
}

The first strategy is to return the original string directly by default

export class Default implements Strategy {
  operation(s: string) {
    return s
  }
}

The second is to capitalize

export class UpperCase implements Strategy {
  operation(s: string) {
    return s.toUpperCase()
  }
}

The third is to convert to lowercase

export class LowerCase implements Strategy {
  operation(s: string) {
    return s.toLowerCase()
  }
}

The last one is to capitalize

export class Capitalize implements Strategy {
  operation(s: string) {
    const lower = s.toLowerCase()
    return `${lower[0].toUpperCase()}${lower.substring(1)}`
  }
}

9.3.3 test & output

  • /src/behavioral/strategy/index.ts

Finally, we can look at the output under different strategies

const upperCase = new UpperCase()
const lowerCase = new LowerCase()
const capitalize = new Capitalize()

const context = new Context()
log(`context: ${context}`)

context.setStrategy(upperCase)
log(`context: ${context}`)

context.setStrategy(lowerCase)
log(`context: ${context}`)

context.setStrategy(capitalize)
log(`context: ${context}`)

9.4 summary of effect and characteristics

  • Algorithm series: the context is only called according to the policy interface, so more different algorithm series can be implemented
  • Alternative inheritance method: different implementations of algorithms can also be met through inheritance, but the use of policy mode is an implementation method of using combination instead of inheritance
  • Eliminate conditional branching: the strategy that originally needed to be selected according to conditional branching is now directly completed by using polymorphism
  • Choice of implementation: since the policy is independent and becomes a specific object, it is necessary to create and manage the existing policy classes and the scheduling and selection of policies through other modes
  • Communication overhead: from the original single method to the delegation system for specific policy objects, which increases the communication overhead of objects
  • Object inflation: the policy logic is independent into new objects, which may produce redundant policy objects. For example, singleton mode and meta mode can be used to optimize the additional cost of policy objects

10. Template Method

10.1 applicable scenarios

  • Extract the invariant part of the algorithm, open the variable behavior to the subclass implementation, and avoid the repeated definition of common logic
  • Also known as hook pattern (hook), it is equivalent to exposing an optional hook function for subclass coverage

10.2 mode structure

  • Template abstract template class: refine and centralize common and invariant algorithm parts
  • ConcreteTemplate concrete template class: optional abstract interfaces / hooks exposed by the template class

10.3 code examples

10.3.1 Templates template definition

  • /src/behavioral/template_method/templates.ts

The first is the abstract template class that maintains common logic

export abstract class ProcessTemplate {
  process() {
    this.step1()
    this.step2()
    this.step3()
  }

  step1() {
    log('default step2')
  }

  abstract step2(): void

  step3() {
    log('default step3')
  }
}

Next, there are two different template implementations, which need to cover the required step 2, while the optional ones cover step 1 and step 3

export class ProcessA extends ProcessTemplate {
  step2() {
    log('Custom step2 from ProcessA')
  }
}

export class ProcessB extends ProcessTemplate {
  step1() {
    log('Custom step1 from ProcessB')
  }

  step2() {
    log('Custom step2 from ProcessB')
  }
}

10.3.2 test & output

  • /src/behavioral/template_method/index.ts

Finally, we can see different template implementations, and modify the internal flow of the algorithm by rewriting the hook function

const templateA: ProcessTemplate = new ProcessA()
const templateB: ProcessTemplate = new ProcessB()

group('templateA process', () => {
  templateA.process()
})

group('templateB process', () => {
  templateB.process()
})

10.4 summary of effect and characteristics

  • Reverse control structure: the parent class directly calls the implementation of the internal method, while the specific implementation of the internal method is handed over to the child class (the default implementation can also be provided as a hook)

11. Visitor visitor mode

11.1 applicable scenarios

  • You need to achieve more specific access to the operation target (depending on the specific target object type)
  • Access the unified object structure for different object types
  • Dual delegation model: to expose specific types of access operations

11.2 mode structure

  • ObjectStructure object structure: it may be an aggregate object or a composite object, providing an interface for accessing internal objects
  • Element internal object abstraction: declare an interface that accepts an accessor
  • Concrete element specific internal object: expose its specific type by calling the visitor's specific interface
  • Visitor Abstract visitor: declare interfaces that access different concrete types
  • ConcreteVisitor: optional to implement different access operations

11.3 code examples

11.3.1 Visitors visitor definition

  • /src/behavioral/visitor/visitors.ts

First of all, the most important thing is our visitors. Let's take a look at the visitor's interface

export abstract class Visitor {
  visitElementA(_: ElementA) {}
  visitElementB(_: ElementB) {}
}

The two methods accept different access target types

Then there are two actual visitor objects, each overriding a method

export class ConcreteVisitorA extends Visitor {
  visitElementA(elementA: ElementA) {
    log('ConcreteVisitorA visit', elementA)
  }
}

export class ConcreteVisitorB extends Visitor {
  visitElementB(elementB: ElementB) {
    log('ConcreteVisitorB visit', elementB)
  }
}

11.3.2 object structure definition

  • /src/behavioral/visitor/ObjectStructure.ts

Next, define an object composition structure. This example is a simple array list

export default class ObjectStructure {
  elements: Element[] = []

  accept(visitor: Visitor) {
    this.elements.forEach((element) => element.accept(visitor))
  }
}

11.3.3 Elements object definition

  • /src/behavioral/visitor/elements.ts

Finally, two concrete objects are defined, and the accept method calls different visitor methods respectively

export interface Element {
  accept(visitor: Visitor): void
}

export class ElementA implements Element {
  static count = 1
  id = ElementA.count++

  accept(visitor: Visitor) {
    visitor.visitElementA(this)
  }
}

export class ElementB implements Element {
  static count = 1
  id = ElementB.count++

  accept(visitor: Visitor) {
    visitor.visitElementB(this)
  }
}

11.3.4 test & output

  • /src/behavioral/visitor/index.ts

From the output, we can see that in fact, for each object, a visitor's method will be called to indicate that it has passed, but our visitors can provide default methods and method overrides to allow the specified visitor to traverse the specified object

const os = new ObjectStructure()
os.elements.push(new ElementA())
os.elements.push(new ElementA())
os.elements.push(new ElementB())
os.elements.push(new ElementA())
os.elements.push(new ElementB())
os.elements.push(new ElementB())
os.elements.push(new ElementB())

const visitorA = new ConcreteVisitorA()
const visitorB = new ConcreteVisitorB()

group('visitorA', () => {
  os.accept(visitorA)
})

group('visitorB', () => {
  os.accept(visitorB)
})

11.4 summary of effect and characteristics

  • Easy to add new operations: to add new access operations, you only need to add a new method or add a new inheritance class
  • Centralize access methods: centralize and cohere the related logic of access objects into one or more related objects
  • Access through class hierarchy: in fact, the visitor pattern is to leave many hook methods for the whole traversal operation, so that we can extend the traversal behavior; In other words, we can implement the iterator pattern in the traversal part of the object structure, which can be open to the extension of the traversal method
  • Cumulative state: by opening the traversal access extension, we can accumulate the traversal process and results outside the object
  • Breaking encapsulation: the specific visitor will actually see the actual object type directly through the object interface. In fact, it belongs to a kind of destruction of encapsulation privacy, but at the same time, this is the mode we expect to access specific types

epilogue

This article introduces the last part of design patterns: behavioral design patterns. However, design patterns are not limited to this. We do not need to choose patterns for patterns, but choose corresponding patterns according to needs for combination and expansion, so as to help us better clarify the interaction patterns between objects. The essence of design patterns is patterns

Other resources

Reference connection

TitleLink
Design Patterns - Elements of Reusable Object-Oriented Software

Complete code example

https://github.com/superfreeeee/Blog-code/tree/main/design_pattern_js/src/behavioral

Keywords: Design Pattern OOP

Added by pipe_girl on Sat, 22 Jan 2022 11:22:11 +0200