Chapter 4. Structural Design Patterns

While creational patterns play the part of flexibly creating objects, structural patterns, on the other hand, are patterns about composing objects. In this chapter, we are going to talk about structural patterns that fit different scenarios.

If we take a closer look at structural patterns, they can be divided into structural class patterns and structural object patterns. Structural class patterns are patterns that play with "interested parties" themselves, while structural object patterns are patterns that weave pieces together (like Composite Pattern). These two kinds of structural patterns complement each other to some degree.

Here are the patterns we'll walk through in this chapter:

  • Composite: Builds tree-like structures using primitive and composite objects. A good example would be the DOM tree that forms a complete page.
  • Decorator: Adds functionality to classes or objects dynamically.
  • Adapter: Provides a general interface and work with different adaptees by implementing different concrete adapters. Consider providing different database choices for a single content management system.
  • Bridge: Decouples the abstraction from its implementation, and make both of them interchangeable.
  • Façade: Provides a simplified interface for the combination of complex subsystems.
  • Flyweight: Shares stateless objects that are being used many times to improve memory efficiency and performance.
  • Proxy: Acts as the surrogate that takes extra responsibilities when accessing objects it manages.

Composite Pattern

Objects under the same class could vary from their properties or even specific subclasses, but a complex object can have more than just normal properties. Taking DOM elements, for example, all the elements are instances of class Node. These nodes form tree structures to represent different pages, but every node in these trees is complete and uniform compared to the node at the root:

<html> 
  <head> 
    <title>TypeScript</title> 
  </head> 
  <body> 
    <h1>TypeScript</h1> 
    <img /> 
  </body> 
</html> 

The preceding HTML represents a DOM structure like this:

Composite Pattern

All of the preceding objects are instances of Node, they implement the interface of a component in Composite Pattern. Some of these nodes like HTML elements (except for HTMLImageElement) in this example have child nodes (components) while others don't.

Participants

The participants of Composite Pattern implementation include:

  • Component: Node

    Defines the interface and implement the default behavior for objects of the composite. It should also include an interface to access and manage the child components of an instance, and optionally a reference to its parent.

  • Composite: Includes some HTML elements, like HTMLHeadElement and HTMLBodyElement

    Stores child components and implements related operations, and of course its own behaviors.

  • Leaf: TextNode, HTMLImageElement

    Defines behaviors of a primitive component.

  • Client:

    Manipulates the composite and its components.

Pattern scope

Composite Pattern applies when objects can and should be abstracted recursively as components that form tree structures. Usually, it would be a natural choice when a certain structure needs to be formed as a tree, such as trees of view components, abstract syntax trees, or trees that represent file structures.

Implementation

We are going to create a composite that represents simple file structures and has limited kinds of components.

First of all, let's import related node modules:

import * as Path from 'path'; 
import * as FS from 'fs'; 

Note

Module path and fs are built-in modules of Node.js, please refer to Node.js documentation for more information: https://nodejs.org/api/.

Note

It is my personal preference to have the first letter of a namespace (if it's not a function at the same time) in uppercase, which reduces the chance of conflicts with local variables. But a more popular naming style for namespace in JavaScript does not.

Now we need to make abstraction of the components, say FileSystemObject:

abstract class FileSystemObject { 
  constructor( 
    public path: string, 
    public parent?: FileSystemObject 
  ) { } 
 
  get basename(): string { 
    return Path.basename(this.path); 
  } 
} 

We are using abstract class because we are not expecting to use FileSystemObject directly. An optional parent property is defined to allow us to visit the upper component of a specific object. And the basename property is added as a helper for getting the basename of the path.

The FileSystemObject is expected to have subclasses, FolderObject and FileObject. For FolderObject, which is a composite that may contain other folders and files, we are going to add an items property (getter) that returns other FileSystemObject it contains:

class FolderObject extends FileSystemObject { 
  items: FileSystemObject[]; 
 
  constructor(path: string, parent?: FileSystemObject) { 
    super(path, parent); 
  } 
} 

We can initialize the items property in the constructor with actual files and folders existing at given path:

this.items = FS 
  .readdirSync(this.path) 
  .map(path => { 
    let stats = FS.statSync(path); 
 
    if (stats.isFile()) { 
      return new FileObject(path, this); 
    } else if (stats.isDirectory()) { 
      return new FolderObject(path, this); 
    } else { 
      throw new Error('Not supported');
    } 
  }); 

You may have noticed we are forming items with different kinds of objects, and we are also passing this as the parent of newly created child components.

And for FileObject, we'll add a simple readAll method that reads all bytes of the file:

class FileObject extends FileSystemObject { 
  readAll(): Buffer { 
    return FS.readFileSync(this.path); 
  } 
} 

Currently, we are reading the child items inside a folder from the actual filesystem when a folder object gets initiated. This might not be necessary if we want to access this structure on demand. We may actually create a getter that calls readdir only when it's accessed, thus the object would act like a proxy to the real filesystem.

Consequences

Both the primitive object and composite object in Composite Pattern share the component interface, which makes it easy for developers to build a composite structure with fewer things to remember.

It also enables the possibility of using markup languages like XML and HTML to represent a really complex object with extreme flexibility. Composite Pattern can also make the rendering easier by having components rendered recursively.

As most components are compatible with having child components or being child components of their parents themselves, we can easily create new components that work great with existing ones.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.186.46