Something About Design Pattern In Javascript

忽然,就想用英语来写写技术相关的东西哈:),水平有限,不过还得硬写(模仿老外也行),不写口语铁定退化。

The article is divided into several sections:
1.The Factory Design Pattern in Javascript
2.Editor’s Commands
3.Protect Your Class’s Properties or Methods
4.How To Use Super in Javascript / Typescript
5.In a Class, How To Add a Clone Method without using “New” keyword
6.How To Use static in Javascript / Typescript

The Factory Design Pattern in Javascript

The Factory Design Pattern is probably the most used design pattern in modern programming languages.
Here, we’ll describe a flavor of factory pattern commonly used nowdays. You can also check the original Factory Method pattern which is very similar.

Intent
– creates objects without exposing the instantiation logic to the client.
– refers to the newly created object through a common interface

Learn more about it.

Let’s start with the fine example from jsplumb’s Endpoint Factory:
It create a factory that is used to create / clone / manipulate all kinds of Endpoints.

Endpoint’s Factory

import {EndpointRepresentation} from "../endpoint/endpoints"
import {Endpoint} from "../endpoint/endpoint"
import {Orientation} from "../factory/anchor-record-factory"
import {Constructable, log} from "@jsplumb/util"
import {AnchorPlacement} from "@jsplumb/common"

const endpointMap:Record<string, Constructable<EndpointRepresentation<any>>> = {}
const endpointComputers:Record<string, EndpointComputeFunction<any>> = {}
const handlers:Record<string, EndpointHandler<any, any>> = {}

export type EndpointComputeFunction<T> = (endpoint:EndpointRepresentation<T>, anchorPoint:AnchorPlacement, orientation:Orientation, endpointStyle:any) => T

export const EndpointFactory = {
    get:(ep:Endpoint, name:string, params:any):EndpointRepresentation<any> => {

        let e:Constructable<EndpointRepresentation<any>> = endpointMap[name]
        if (!e) {
            throw {message:"jsPlumb: unknown endpoint type '" + name + "'"}
        } else {
            return new e(ep, params) as EndpointRepresentation<any>
        }
    },

    clone:<C>(epr:EndpointRepresentation<C>):EndpointRepresentation<C> => {
        const handler = handlers[epr.type]
        return EndpointFactory.get(epr.endpoint, epr.type, handler.getParams(epr))
    },

    compute:<T>(endpoint:EndpointRepresentation<T>, anchorPoint:AnchorPlacement, orientation:Orientation, endpointStyle:any):T => {
      const c = endpointComputers[endpoint.type]
      if (c != null) {
          return c(endpoint, anchorPoint, orientation, endpointStyle)
      } else {
          log("jsPlumb: cannot find endpoint calculator for endpoint of type ", endpoint.type)
      }
    },

    registerHandler:<E,T>(eph:EndpointHandler<E, T>) => {
        handlers[eph.type] = eph
        endpointMap[eph.type] = eph.cls
        endpointComputers[eph.type] = eph.compute
    }
}

export interface EndpointHandler<E, T> {
    type:string
    compute:EndpointComputeFunction<T>
    getParams(endpoint:E):Record<string, any>
    cls:Constructable<EndpointRepresentation<T>>
}

One kind of the Endpoint

import {EndpointRepresentation } from "./endpoints"
import {Orientation} from "../factory/anchor-record-factory"
import {Endpoint} from "./endpoint"
import {EndpointHandler} from "../factory/endpoint-factory"
import {AnchorPlacement, DotEndpointParams} from "@jsplumb/common"

export type ComputedDotEndpoint = [ number, number, number, number, number ]

export class DotEndpoint extends EndpointRepresentation<ComputedDotEndpoint> {

    radius:number
    defaultOffset:number
    defaultInnerRadius:number

    constructor(endpoint:Endpoint, params?:DotEndpointParams) {
        
        super(endpoint, params)
        
        params = params || {}
        this.radius = params.radius || 5
        this.defaultOffset = 0.5 * this.radius
        this.defaultInnerRadius = this.radius / 3
    }

    static type = "Dot"
    type = DotEndpoint.type
}


export const DotEndpointHandler:EndpointHandler<DotEndpoint, ComputedDotEndpoint> = {

    type:DotEndpoint.type,

    cls:DotEndpoint,

    compute:(ep:DotEndpoint, anchorPoint:AnchorPlacement, orientation:Orientation, endpointStyle:any):ComputedDotEndpoint => {
        let x = anchorPoint.curX - ep.radius,
            y = anchorPoint.curY - ep.radius,
            w = ep.radius * 2,
            h = ep.radius * 2

        if (endpointStyle && endpointStyle.stroke) {
            let lw = endpointStyle.strokeWidth || 1
            x -= lw
            y -= lw
            w += (lw * 2)
            h += (lw * 2)
        }

        ep.x = x
        ep.y = y
        ep.w = w
        ep.h = h

        return [ x, y, w, h, ep.radius ]
    },

    getParams:(ep:DotEndpoint): Record<string, any> => {
        return { radius: ep.radius }
    }
}

Editor’s Commands

Let’s take a look at the Threejs Editor’s Commands.
Every command is defined as a class. The class is used to create a new command instance. The command is executed by calling the execute() method. The command is undoable by calling the undo() method. The command is redoable by calling the redo() method.
But in some situations, you can’t use this design pattern. For example, if one command is relying on another command to be executed first, or some commands are executed automatically when another command is executed. In these cases, you can use the queue to store the snapshot of the state difference according to the timeline of the command execution, and redo / undo them as necessary.

Threejs Editor’s Commands

export { AddObjectCommand } from './AddObjectCommand.js';
export { AddScriptCommand } from './AddScriptCommand.js';
export { MoveObjectCommand } from './MoveObjectCommand.js';
export { MultiCmdsCommand } from './MultiCmdsCommand.js';
export { RemoveObjectCommand } from './RemoveObjectCommand.js';
export { RemoveScriptCommand } from './RemoveScriptCommand.js';
export { SetColorCommand } from './SetColorCommand.js';
export { SetGeometryCommand } from './SetGeometryCommand.js';
export { SetGeometryValueCommand } from './SetGeometryValueCommand.js';
export { SetMaterialColorCommand } from './SetMaterialColorCommand.js';
export { SetMaterialCommand } from './SetMaterialCommand.js';
export { SetMaterialMapCommand } from './SetMaterialMapCommand.js';
export { SetMaterialValueCommand } from './SetMaterialValueCommand.js';
export { SetMaterialVectorCommand } from './SetMaterialVectorCommand.js';
export { SetPositionCommand } from './SetPositionCommand.js';
export { SetRotationCommand } from './SetRotationCommand.js';
export { SetScaleCommand } from './SetScaleCommand.js';
export { SetSceneCommand } from './SetSceneCommand.js';
export { SetScriptValueCommand } from './SetScriptValueCommand.js';
export { SetUuidCommand } from './SetUuidCommand.js';
export { SetValueCommand } from './SetValueCommand.js';

Protect Your Class’s Properties or Methods

Some properties or methods’s structure of a class are not meant to be changed by the user. For example, the position’s structure of an object is not meant to be changed by the user. In these cases, you can use Object.defineProperty() to protect the property or method.

Protect Your Class’s Properties

// other code...
// ...
class Object3D extends EventDispatcher {

	constructor() {

		super();

		Object.defineProperty( this, 'id', { value: _object3DId ++ } );

		// other code...
		// ...

		const position = new Vector3();
		const rotation = new Euler();
		const quaternion = new Quaternion();
		const scale = new Vector3( 1, 1, 1 );

		function onRotationChange() {

			quaternion.setFromEuler( rotation, false );

		}

		function onQuaternionChange() {

			rotation.setFromQuaternion( quaternion, undefined, false );

		}

		rotation._onChange( onRotationChange );
		quaternion._onChange( onQuaternionChange );

		Object.defineProperties( this, {
			position: {
				configurable: true,
				enumerable: true,
				value: position
			},
			rotation: {
				configurable: true,
				enumerable: true,
				value: rotation
			},
			quaternion: {
				configurable: true,
				enumerable: true,
				value: quaternion
			},
			scale: {
				configurable: true,
				enumerable: true,
				value: scale
			},
			modelViewMatrix: {
				value: new Matrix4()
			},
			normalMatrix: {
				value: new Matrix3()
			}
		} );

		// other code...
		// ...

	}

	// other functions ...
  // ...
}

Object3D.DefaultUp = new Vector3( 0, 1, 0 );
Object3D.DefaultMatrixAutoUpdate = true;

Object3D.prototype.isObject3D = true;

export { Object3D };

How To Use Super in Javascript / Typescript

Firstly, sometimes you need to call the super constructor in a constructor. In this case, you can use the super keyword. The super keyword is used to call the constructor of the super class. Secondly, you can use the super keyword to call the methods of the super class. Thirdly, you can use the super keyword to call the properties of the super class. Finally, you can use the super keyword to call the getters and setters of the super class.

Jsplumb’s Endpoint Class

// other code...
// ...

export class Endpoint<E = any> extends Component {

    // other code...
    // ...

    constructor(public instance:JsPlumbInstance, params:InternalEndpointOptions<E>) {
        super(instance, params)

	// other code...
	// ...
    }

    // other code...
    // ...

    setVisible(v:boolean, doNotChangeConnections?:boolean, doNotNotifyOtherEndpoint?:boolean) {

        super.setVisible(v)

        // other code...
        // ...
    }

    applyType(t:any, typeMap:any):void {

        super.applyType(t, typeMap)

        // other code...
        // ...

    }

    destroy():void {

        super.destroy()

        // other code...
        // ...

    }

    // other code...
    // ...

    addClass(clazz: string, cascade?:boolean): void {
        super.addClass(clazz, cascade)
        if (this.endpoint != null) {
            this.endpoint.addClass(clazz)
        }
    }

    removeClass(clazz: string, cascade?:boolean): void {
        super.removeClass(clazz, cascade)
        if (this.endpoint != null) {
            this.endpoint.removeClass(clazz)
        }
    }
}

In a Class, How To Add a Clone Method without using “New” keyword

What’s going on when we use a “new” keyword to create a new instance of a class?

When the code new Foo(...) is executed, the following things happen:

– A new object is created, inheriting from Foo.prototype.
– The constructor function Foo is called with the specified arguments, and with this bound to the newly created object. new Foo is equivalent to new Foo(), i.e. if no argument list is specified, Foo is called without arguments.
– The object (not null, false, 3.1415 or other primitive types) returned by the constructor function becomes the result of the whole new expression. If the constructor function doesn’t explicitly return an object, the object created in step 1 is used instead (normally constructors don’t return a value, but they can choose to do so if they want to override the normal object creation process).

Read More in the Web technology for developers 》JavaScript 》JavaScript reference 》Expressions and operators 》new operator

What if we want to create a new instance of a class without using “new“?
We can use Object.create()!

Let’s check out the following example:

class Foo {
    clone2 = () => {}

    constructor(a, b) {
        this._constructor(a,b)
    }
    
    _constructor(a, b) { // protected constructor
        this.a = a
        this.b = b

        this.c = this.a + this.b

        this.clone2 = () => { // Add a Clone Method without using "New" keyword by Object.create()
            console.log('clone2 called');
            let o = Object.create(this.constructor.prototype) // clone the prototype first
            this._constructor.apply(o, [a, b]) // then call the constructor method to clone the properties
            return o
        }
    }

    method() {
        console.log('method called');
    }

    clone1() { // Add a Clone Method using "New" keyword
        console.log('clone1 called');
        return new Foo(this.a, this.b)
    }
}

or use Typescript directly just like jsplumb’s Component class, using Object.create() in a protected constructor method:

export abstract class Component extends EventGenerator {
    // other code...
    clone: () => Component
    protected constructor(public instance:JsPlumbInstance, params?:ComponentOptions) {
        
        super()
        // other code...

        this.clone = ():Component => { // Add a Clone Method without using "New" keyword by Object.create()
            let o = Object.create(this.constructor.prototype) // clone the prototype first
            this.constructor.apply(o, [instance, params]) // then call the constructor method to clone the properties
            return o
        }

        // other code...
    }

    // other code...
}

How To Use static in Javascript / Typescript

Sometimes, we need to use static properties and methods in a class. What’s the difference between static and instance properties and methods?

If you are familiar with Java, you know that static properties and methods are similar to static members in Java. In Java, static members are accessed using the class name, and in Javascript, static members are accessed using the class name too.

What’s the difference between keyword “this” in a static method and keyword “this” in an instance method?
In a static method, “this” refers to the class, but in an instance method, “this” refers to the instance.

class Foo {
    constructor(a, b) {
        this.a = a
        this.b = b
    }

    method() {
        console.log('method called');
        console.log('this.a = ' + this.a);
        console.log('this.b = ' + this.b);
        console.log(this);
    }

    static staticMethod() {
        console.log('staticMethod called');
        console.log('this.a = ' + this.a);
        console.log('this.b = ' + this.b);
        console.log(this);
        console.log(new this(3, 4));
    }
}

const foo = new Foo(1, 2)

foo.method()
// method called
// this.a = 1
// this.b = 2
// Foo { a: 1, b: 2 }  => this is the instance

foo.staticMethod()
// VM396:1 Uncaught TypeError: foo.staticMethod is not a function at <anonymous>:1:5

Foo.staticMethod()
// staticMethod called
// this.a = undefined
// this.b = undefined
// class Foo {...}    => this is a class, not an instance
// Foo {a: 3, b: 4}   => new this(3, 4) is a new instance

Last but Not least

Reading ths source code of some excellent open source projects, we can learn quite a lot about the different types of awesome design patterns that can be used to construct our applications.
As the saying goes, “The more that you read, the more things you will know. The more that you learn, the more places you’ll go. You’ll miss the best things if you keep your eyes shut.” -Dr. Seuss

作者: 博主

Talk is cheap, show me the code!