Design Patterns Analysis: Microsoft Cognitive Services Speech SDK (JavaScript)
The microsoft/cognitive-services-speech-sdk-js repository demonstrates several professional design patterns. Here’s a detailed breakdown:
1. Singleton Pattern
Pattern Description
The Singleton pattern ensures only one instance of a class exists throughout the application and provides global access to that instance.
Implementation in the Code
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { ArgumentNullError } from "./Error.js";
import { EventSource } from "./EventSource.js";
import { IEventSource } from "./IEventSource.js";
import { PlatformEvent } from "./PlatformEvent.js";
export class Events {
private static privInstance: IEventSource<PlatformEvent> = new EventSource<PlatformEvent>();
public static setEventSource(eventSource: IEventSource<PlatformEvent>): void {
if (!eventSource) {
throw new ArgumentNullError("eventSource");
}
Events.privInstance = eventSource;
}
public static get instance(): IEventSource<PlatformEvent> {
return Events.privInstance;
}
}How It Works
- Single Instance:
privInstanceis a static member initialized with a newEventSourceobject - Global Access: The static
instancegetter provides global access - Setter Control:
setEventSource()allows replacing the instance (useful for testing)
Why It’s Used
- Provides a centralized event management system across the entire SDK
- Ensures all components share the same event dispatcher
- Allows for dependency injection and testing
Simple Example for Your Repository
// Simple Logger Singleton Pattern
export class Logger {
private static privInstance: Logger = new Logger();
private logs: string[] = [];
private constructor() {
// Private constructor prevents instantiation
}
public static getInstance(): Logger {
return Logger.privInstance;
}
public log(message: string): void {
this.logs.push(message);
console.log(message);
}
public getLogs(): string[] {
return this.logs;
}
}
// Usage
const logger = Logger.getInstance();
logger.log("Application started");2. Factory Pattern
Pattern Description
The Factory pattern creates objects without specifying the exact classes to instantiate, allowing the system to determine which concrete class to instantiate.
Implementation in the Code
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { IConnection } from "../common/Exports.js";
import { AuthInfo } from "./IAuthentication.js";
import { RecognizerConfig } from "./RecognizerConfig.js";
export interface IConnectionFactory {
create(
config: RecognizerConfig,
authInfo: AuthInfo,
connectionId?: string): Promise<IConnection>;
}// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import {
ServicePropertiesPropertyName,
} from "../common.speech/Exports.js";
import { ConnectionRedirectEvent, Events, IConnection, IStringDictionary } from "../common/Exports.js";
import { PropertyId } from "../sdk/Exports.js";
import { AuthInfo, IConnectionFactory, RecognizerConfig } from "./Exports.js";
import { QueryParameterNames } from "./QueryParameterNames.js";
export abstract class ConnectionFactoryBase implements IConnectionFactory {
public static getHostSuffix(region: string): string {
/*...*/
}
public abstract create(
config: RecognizerConfig,
authInfo: AuthInfo,
connectionId?: string): Promise<IConnection>;
protected setCommonUrlParams(
config: RecognizerConfig,
queryParams: IStringDictionary<string>,
endpoint: string): void {
/*...*/
}
protected setUrlParameter(
propId: PropertyId,
parameterName: string,
config: RecognizerConfig,
queryParams: IStringDictionary<string>,
endpoint: string): void {
/*...*/
}How It Works
- Interface Definition:
IConnectionFactorydefines the contract for creating connections - Abstract Base:
ConnectionFactoryBaseprovides common functionality - Concrete Implementations: Specific factories (like
SpeechConnectionFactory,DialogConnectionFactory) implement the interface
Why It’s Used
- Allows different types of recognizers (speech, translation, dialog) to use their own connection factories
- Decouples the creation logic from the usage logic
- Makes the system extensible for new connection types
Simple Example for Your Repository
// Factory Pattern Example
interface IDatabase {
connect(): Promise<void>;
query(sql: string): Promise<any>;
}
class MySQLDatabase implements IDatabase {
async connect(): Promise<void> {
console.log("Connecting to MySQL...");
}
async query(sql: string): Promise<any> {
return { result: "MySQL result" };
}
}
class PostgresDatabase implements IDatabase {
async connect(): Promise<void> {
console.log("Connecting to PostgreSQL...");
}
async query(sql: string): Promise<any> {
return { result: "PostgreSQL result" };
}
}
class DatabaseFactory {
static create(type: "mysql" | "postgres"): IDatabase {
switch (type) {
case "mysql":
return new MySQLDatabase();
case "postgres":
return new PostgresDatabase();
default:
throw new Error("Unknown database type");
}
}
}
// Usage
const db = DatabaseFactory.create("mysql");
await db.connect();
const result = await db.query("SELECT * FROM users");3. Template Method Pattern
Pattern Description
The Template Method pattern defines the skeleton of an algorithm in a base class, allowing subclasses to override specific steps.
Implementation in the Code
//
// ################################################################################################################
// IMPLEMENTATION.
// Move to independent class
// ################################################################################################################
//
protected abstract createRecognizerConfig(speechConfig: SpeechServiceConfig): RecognizerConfig;
// Creates the correct service recognizer for the type
protected abstract createServiceRecognizer(
authentication: IAuthentication,
connectionFactory: IConnectionFactory,
audioConfig: AudioConfig,
recognizerConfig: RecognizerConfig): ServiceRecognizerBase;
// Does the generic recognizer setup that is common across all recognizer types.
protected implCommonRecognizerSetup(): void {
let osPlatform = (typeof window !== "undefined") ? "Browser" : "Node";
let osName = "unknown";
let osVersion = "unknown";
if (typeof navigator !== "undefined") {
osPlatform = osPlatform + "/" + navigator.platform;
osName = navigator.userAgent;
osVersion = navigator.appVersion;
}
const recognizerConfig = this.createRecognizerConfig(
new SpeechServiceConfig(
new Context(new OS(osPlatform, osName, osVersion))));
this.privReco = this.createServiceRecognizer(
Recognizer.getAuth(this.privProperties, this.tokenCredential),
this.privConnectionFactory,
this.audioConfig,
recognizerConfig);
}
protected async recognizeOnceAsyncImpl(recognitionMode: RecognitionMode): Promise<SpeechRecognitionResult> {
Contracts.throwIfDisposed(this.privDisposed);
const ret: Deferred<SpeechRecognitionResult> = new Deferred<SpeechRecognitionResult>();
await this.implRecognizerStop();
await this.privReco.recognize(recognitionMode, ret.resolve, ret.reject);
const result: SpeechRecognitionResult = await ret.promise;
await this.implRecognizerStop();
return result;
}
protected async startContinuousRecognitionAsyncImpl(recognitionMode: RecognitionMode): Promise<void> {
Contracts.throwIfDisposed(this.privDisposed);
await this.implRecognizerStop();
await this.privReco.recognize(recognitionMode, undefined, undefined);
}
protected async stopContinuousRecognitionAsyncImpl(): Promise<void> {
Contracts.throwIfDisposed(this.privDisposed);
await this.implRecognizerStop();
}
protected async implRecognizerStop(): Promise<void> {
if (this.privReco) {
// Get timeout property - undefined/empty means no timeout (existing behavior)
const timeoutProperty = this.privProperties.getProperty(
PropertyId.Recognizer_StopTimeoutMs,
undefined
); protected createRecognizerConfig(speechConfig: SpeechServiceConfig): RecognizerConfig {
return new RecognizerConfig(speechConfig, this.privProperties);
}
protected createServiceRecognizer(
authentication: IAuthentication,
connectionFactory: IConnectionFactory,
audioConfig: AudioConfig,
recognizerConfig: RecognizerConfig): ServiceRecognizerBase {
const configImpl: AudioConfigImpl = audioConfig as AudioConfigImpl;
return new SpeechServiceRecognizer(authentication, connectionFactory, configImpl, recognizerConfig, this);
}
}How It Works
- Base Class:
Recognizerdefines the algorithm skeleton (implCommonRecognizerSetup,recognizeOnceAsyncImpl) - Abstract Methods: Subclasses must implement
createRecognizerConfig()andcreateServiceRecognizer() - Subclass Implementations:
SpeechRecognizer,TranslationRecognizerprovide specific implementations
Why It’s Used
- Different recognizer types share common recognition logic but differ in configuration creation
- Reduces code duplication across different recognizer classes
- Makes it easy to add new recognizer types
Simple Example for Your Repository
// Template Method Pattern Example
abstract class DataProcessor {
// Template method - defines the algorithm skeleton
public process(data: string): void {
const validated = this.validate(data);
const transformed = this.transform(validated);
this.save(transformed);
}
protected validate(data: string): string {
if (!data || data.trim().length === 0) {
throw new Error("Data cannot be empty");
}
return data;
}
// Abstract method - subclasses must implement
protected abstract transform(data: string): string;
protected save(data: string): void {
console.log(`Saving: ${data}`);
}
}
class CSVProcessor extends DataProcessor {
protected transform(data: string): string {
return data.split(',').join('|');
}
}
class JSONProcessor extends DataProcessor {
protected transform(data: string): string {
return JSON.stringify(JSON.parse(data), null, 2);
}
}
// Usage
const csvProcessor = new CSVProcessor();
csvProcessor.process("name,age,city");
const jsonProcessor = new JSONProcessor();
jsonProcessor.process('{"name":"John","age":30}');4. Observer Pattern (Event-Driven)
Pattern Description
The Observer pattern defines a one-to-many dependency where when one object changes state, all its dependents are notified automatically.
Implementation in the Code
export class EventSource<TEvent extends PlatformEvent> implements IEventSource<TEvent> {
private privEventListeners: IStringDictionary<(event: TEvent) => void> = {};
private privMetadata: IStringDictionary<string>;
private privIsDisposed: boolean = false;
private privConsoleListener: IDetachable = undefined;
public constructor(metadata?: IStringDictionary<string>) {
this.privMetadata = metadata;
}
public onEvent(event: TEvent): void {
if (this.isDisposed()) {
throw (new ObjectDisposedError("EventSource"));
}
if (this.metadata) {
for (const paramName in this.metadata) {
if (paramName) {
if (event.metadata) {
if (!event.metadata[paramName]) {
event.metadata[paramName] = this.metadata[paramName];
}
}
}
}
}
for (const eventId in this.privEventListeners) {
if (eventId && this.privEventListeners[eventId]) {
this.privEventListeners[eventId](event);
}
}
}
public attach(onEventCallback: (event: TEvent) => void): IDetachable {
const id = createNoDashGuid();
this.privEventListeners[id] = onEventCallback;
return {
detach: (): Promise<void> => {
delete this.privEventListeners[id];
return Promise.resolve();
},
};
}
public attachListener(listener: IEventListener<TEvent>): IDetachable {
return this.attach((e: TEvent): void => listener.onEvent(e));
}
public attachConsoleListener(listener: IEventListener<TEvent>): IDetachable {
if (!!this.privConsoleListener) {
void this.privConsoleListener.detach();
}
this.privConsoleListener = this.attach((e: TEvent): void => listener.onEvent(e));
return this.privConsoleListener;
}
public isDisposed(): boolean {
return this.privIsDisposed;
}
public dispose(): void {
this.privEventListeners = null;
this.privIsDisposed = true;
}
public get metadata(): IStringDictionary<string> {
return this.privMetadata;
}
}How It Works
- Event Source:
EventSourcemanages a collection of event listeners - Attach Method: Listeners subscribe to events using
attach() - Notify:
onEvent()notifies all registered listeners when an event occurs - Detachable: Listeners can unsubscribe using the returned
IDetachableinterface
Why It’s Used
- Decouples event producers from consumers
- Enables loose coupling between components
- Allows multiple listeners to respond to the same event
Simple Example for Your Repository
// Observer/Event Pattern Example
interface IObserver {
onNotify(message: string): void;
}
class EmailNotifier implements IObserver {
onNotify(message: string): void {
console.log(`Email sent: ${message}`);
}
}
class SMSNotifier implements IObserver {
onNotify(message: string): void {
console.log(`SMS sent: ${message}`);
}
}
class EventEmitter {
private observers: IObserver[] = [];
subscribe(observer: IObserver): void {
this.observers.push(observer);
}
unsubscribe(observer: IObserver): void {
this.observers = this.observers.filter(o => o !== observer);
}
emit(message: string): void {
this.observers.forEach(observer => observer.onNotify(message));
}
}
// Usage
const emitter = new EventEmitter();
const email = new EmailNotifier();
const sms = new SMSNotifier();
emitter.subscribe(email);
emitter.subscribe(sms);
emitter.emit("Important notification");5. Strategy Pattern
Pattern Description
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Implementation in the Code
The SDK uses Strategy through different adapter implementations:
export abstract class SynthesisAdapterBase implements IDisposable {
protected privSynthesisTurn: SynthesisTurn;
protected privConnectionId: string;
protected privSynthesizerConfig: SynthesizerConfig;
protected privSynthesizer: Synthesizer;
protected privSuccessCallback: (e: SpeechSynthesisResult) => void;
protected privErrorCallback: (e: string) => void;
public get synthesizerConfig(): SynthesizerConfig {
return this.privSynthesizerConfig;
}
protected speakOverride: (ssml: string, requestId: string, sc: (e: SpeechSynthesisResult) => void, ec: (e: string) => void) => void = undefined;
// Called when telemetry data is sent to the service.
// Used for testing Telemetry capture.
public static telemetryData: (json: string) => void;
public static telemetryDataEnabled: boolean = true;
public set activityTemplate(messagePayload: string) {
this.privActivityTemplate = messagePayload;
}
public get activityTemplate(): string {
return this.privActivityTemplate;
}
protected receiveMessageOverride: () => void = undefined;
protected connectImplOverride: (isUnAuthorized: boolean) => void = undefined;
protected configConnectionOverride: (connection: IConnection) => Promise<IConnection> = undefined;
public set audioOutputFormat(format: AudioOutputFormatImpl) {
this.privAudioOutputFormat = format;
this.privSynthesisTurn.audioOutputFormat = format;
if (this.privSessionAudioDestination !== undefined) {
this.privSessionAudioDestination.format = format;
}
if (this.synthesisContext !== undefined) {
this.synthesisContext.audioOutputFormat = format;
}
}Why It’s Used
- Different synthesis strategies (REST vs WebSocket) can be swapped at runtime
- Allows customization through override properties
- Enables flexible behavior modification without changing core code
Simple Example for Your Repository
// Strategy Pattern Example
interface IPaymentStrategy {
pay(amount: number): Promise<boolean>;
}
class CreditCardPayment implements IPaymentStrategy {
async pay(amount: number): Promise<boolean> {
console.log(`Processing credit card payment of $${amount}`);
return true;
}
}
class PayPalPayment implements IPaymentStrategy {
async pay(amount: number): Promise<boolean> {
console.log(`Processing PayPal payment of $${amount}`);
return true;
}
}
class CryptoPayment implements IPaymentStrategy {
async pay(amount: number): Promise<boolean> {
console.log(`Processing crypto payment of $${amount}`);
return true;
}
}
class ShoppingCart {
private strategy: IPaymentStrategy;
setPaymentStrategy(strategy: IPaymentStrategy): void {
this.strategy = strategy;
}
async checkout(amount: number): Promise<void> {
if (!this.strategy) {
throw new Error("Payment strategy not set");
}
const success = await this.strategy.pay(amount);
if (success) {
console.log("Payment successful!");
}
}
}
// Usage
const cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment());
await cart.checkout(99.99);
cart.setPaymentStrategy(new PayPalPayment());
await cart.checkout(49.99);6. Deferred/Promise Pattern (Async Handling)
Implementation in the Code
export class Deferred<T> implements IDeferred<T> {
private privPromise: Promise<T>;
private privResolve: (value?: T | PromiseLike<T>) => void;
private privReject: (reason?: any) => void;
public constructor() {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
this.privPromise = new Promise<T>((resolve: (value: T) => void, reject: (reason: any) => void) => {
this.privResolve = resolve;
this.privReject = reject;
});
}
public get promise(): Promise<T> {
return this.privPromise;
}
public resolve = (result: T | Promise<T>): Deferred<T> => {
this.privResolve(result);
return this;
};
public reject = (error: string): Deferred<T> => {
this.privReject(error);
return this;
};
}Simple Example
// Deferred Pattern Example
class Deferred<T> {
promise: Promise<T>;
resolve!: (value: T) => void;
reject!: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
// Usage
async function fetchUser(id: number): Promise<{ id: number; name: string }> {
const deferred = new Deferred<{ id: number; name: string }>();
setTimeout(() => {
if (id > 0) {
deferred.resolve({ id, name: `User ${id}` });
} else {
deferred.reject("Invalid user ID");
}
}, 1000);
return deferred.promise;
}
const user = await fetchUser(1);
console.log(user);Now let me search for more specific patterns:Perfect! Now I have comprehensive information about the missing patterns. Let me provide you with a detailed analysis of the patterns you may have missed.
1. Decorator Pattern (Proxy/Wrapper)
Pattern Description
The Decorator pattern allows you to attach additional responsibilities to an object dynamically, wrapping it to add new functionality without modifying the original class.
Implementation in the Code
export class WebsocketConnection implements IConnection {
private privUri: string;
private privMessageFormatter: IWebsocketMessageFormatter;
private privConnectionMessageAdapter: WebsocketMessageAdapter; // ← Wraps another object
private privId: string;
private privIsDisposed: boolean = false;
public constructor(
uri: string,
queryParameters: IStringDictionary<string>,
headers: IStringDictionary<string>,
messageFormatter: IWebsocketMessageFormatter,
proxyInfo: ProxyInfo,
enableCompression: boolean = false,
connectionId?: string) {
/*...*/
this.privConnectionMessageAdapter = new WebsocketMessageAdapter(
this.privUri,
this.id,
this.privMessageFormatter,
proxyInfo,
headers,
enableCompression);
}
// Delegates to wrapped object
public async dispose(): Promise<void> {
this.privIsDisposed = true;
if (this.privConnectionMessageAdapter) {
await this.privConnectionMessageAdapter.close();
}
}
public open(): Promise<ConnectionOpenResponse> {
return this.privConnectionMessageAdapter.open();
}
public send(message: ConnectionMessage): Promise<void> {
return this.privConnectionMessageAdapter.send(message);
}
public read(): Promise<ConnectionMessage> {
return this.privConnectionMessageAdapter.read();
}
public get events(): EventSource<ConnectionEvent> {
return this.privConnectionMessageAdapter.events;
}
}How It Works
- Wrapper Class:
WebsocketConnectionwrapsWebsocketMessageAdapter - Delegation: All methods delegate to the wrapped adapter
- Added Functionality:
WebsocketConnectionadds ID management, disposal tracking, and URI handling - Same Interface: Both implement
IConnection
Why It’s Used
- Adds HTTP handling layer on top of WebSocket adapter
- Separates concerns: message formatting vs. connection management
- Allows easy swapping of implementations
- Maintains clean API while adding functionality
Simple Example for Your Repository
// Core functionality
interface IUserService {
getUser(id: number): User;
}
class BasicUserService implements IUserService {
getUser(id: number): User {
return { id, name: "John" };
}
}
// Decorator/Wrapper - adds caching without modifying original
class CachedUserService implements IUserService {
private cache = new Map<number, User>();
constructor(private userService: IUserService) {}
getUser(id: number): User {
if (this.cache.has(id)) {
console.log(`Getting user ${id} from cache`);
return this.cache.get(id)!;
}
const user = this.userService.getUser(id);
this.cache.set(id, user);
return user;
}
}
// Another Decorator - adds logging
class LoggingUserService implements IUserService {
constructor(private userService: IUserService) {}
getUser(id: number): User {
console.log(`Fetching user ${id}`);
const user = this.userService.getUser(id);
console.log(`User fetched: ${user.name}`);
return user;
}
}
// Usage - stack decorators
let service: IUserService = new BasicUserService();
service = new CachedUserService(service);
service = new LoggingUserService(service);
const user = service.getUser(1); // Logs, checks cache, fetches2. Builder Pattern
Pattern Description
The Builder pattern separates the construction of a complex object from its representation, allowing step-by-step object creation.
Implementation in the Code
export class DynamicGrammarBuilder {
private privPhrases: string[];
private privGrammars: string[];
private privWeight: number = 1.0;
// Step 1: Add phrases
public addPhrase(phrase: string | string[]): void {
if (!this.privPhrases) {
this.privPhrases = [];
}
if (phrase instanceof Array) {
this.privPhrases = this.privPhrases.concat(phrase);
} else {
this.privPhrases.push(phrase);
}
}
// Step 2: Clear phrases
public clearPhrases(): void {
this.privPhrases = undefined;
}
// Step 3: Add reference grammars
public addReferenceGrammar(grammar: string | string[]): void {
if (!this.privGrammars) {
this.privGrammars = [];
}
if (grammar instanceof Array) {
this.privGrammars = this.privGrammars.concat(grammar);
} else {
this.privGrammars.push(grammar);
}
}
// Step 4: Clear grammars
public clearGrammars(): void {
this.privGrammars = undefined;
}
// Step 5: Set weight
public setWeight(weight: number): void {
this.privWeight = weight;
}
// Step 6: Build the final object
public generateGrammarObject(): Dgi {
if (this.privGrammars === undefined && this.privPhrases === undefined) {
return undefined;
}
const retObj: Dgi = {};
retObj.referenceGrammars = this.privGrammars;
if (undefined !== this.privPhrases && 0 !== this.privPhrases.length) {
const retPhrases: Item[] = [];
this.privPhrases.forEach((value: string): void => {
retPhrases.push({
text: value,
});
});
retObj.groups = [{ type: GroupType.Generic, items: retPhrases }];
retObj.bias = this.privWeight;
}
return retObj;
}
}How It Works
- Fluent Interface: Methods return
voidbut modify internal state - Multi-Step Construction: Add phrases → add grammars → set weight → generate
- Final Build:
generateGrammarObject()returns the constructed object - Chainable Operations: Each method can be called in any order
Why It’s Used
- Complex grammar objects require multiple configuration steps
- Avoids constructor parameter explosion
- Allows partial configuration
- Makes code more readable and maintainable
Simple Example for Your Repository
// Builder Pattern Example
interface Query {
select: string[];
from: string;
where?: string;
orderBy?: string;
limit?: number;
}
class SQLQueryBuilder {
private query: Query = {
select: [],
from: ""
};
// Step 1: Select columns
select(...columns: string[]): this {
this.query.select = columns;
return this;
}
// Step 2: From table
from(table: string): this {
this.query.from = table;
return this;
}
// Step 3: Where condition
where(condition: string): this {
this.query.where = condition;
return this;
}
// Step 4: Order by
orderBy(column: string): this {
this.query.orderBy = column;
return this;
}
// Step 5: Limit
limit(count: number): this {
this.query.limit = count;
return this;
}
// Step 6: Build the final query
build(): string {
let sql = `SELECT ${this.query.select.join(", ")} FROM ${this.query.from}`;
if (this.query.where) sql += ` WHERE ${this.query.where}`;
if (this.query.orderBy) sql += ` ORDER BY ${this.query.orderBy}`;
if (this.query.limit) sql += ` LIMIT ${this.query.limit}`;
return sql;
}
}
// Usage - fluent interface
const query = new SQLQueryBuilder()
.select("id", "name", "email")
.from("users")
.where("age > 18")
.orderBy("name")
.limit(10)
.build();
console.log(query);
// SELECT id, name, email FROM users WHERE age > 18 ORDER BY name LIMIT 103. Disposable Pattern (Resource Management)
Pattern Description
The Disposable pattern ensures proper cleanup of resources by implementing a standardized cleanup interface.
Implementation in the Code
export interface IDisposable {
/**
* @returns {boolean}
* @memberOf IDisposable
*/
isDisposed(): boolean;
/**
* Performs cleanup operations on this instance
* @param {string} [reason] - optional reason for disposing the instance.
* @memberOf IDisposable
*/
dispose(reason?: string): void;
}export class EventSource<TEvent extends PlatformEvent> implements IEventSource<TEvent> {
private privEventListeners: IStringDictionary<(event: TEvent) => void> = {};
private privIsDisposed: boolean = false;
// Check if disposed
public isDisposed(): boolean {
return this.privIsDisposed;
}
// Cleanup resources
public dispose(): void {
this.privEventListeners = null;
this.privIsDisposed = true;
}
// Prevent operations on disposed objects
public onEvent(event: TEvent): void {
if (this.isDisposed()) {
throw (new ObjectDisposedError("EventSource"));
}
// ... rest of implementation
}
}How It Works
- Interface Contract:
IDisposabledefines the cleanup contract - Tracking State:
privIsDisposedtracks disposal status - Guard Clauses: Methods check
isDisposed()before operations - Resource Cleanup:
dispose()releases all resources - Error Handling: Throws
ObjectDisposedErrorif used after disposal
Why It’s Used
- Prevents memory leaks and resource exhaustion
- Standardizes cleanup across the SDK
- Ensures proper event listener cleanup
- Follows .NET/C# conventions developers recognize
Simple Example for Your Repository
// Disposable Pattern Example
interface IConnection {
isDisposed(): boolean;
dispose(): void;
query(sql: string): Promise<any>;
}
class DatabaseConnection implements IConnection {
private connection: any; // Internal connection
private privIsDisposed: boolean = false;
constructor(connectionString: string) {
// Initialize connection
this.connection = { isOpen: true };
}
isDisposed(): boolean {
return this.privIsDisposed;
}
dispose(): void {
if (this.privIsDisposed) return;
// Clean up resources
if (this.connection) {
this.connection.isOpen = false;
this.connection = null;
}
this.privIsDisposed = true;
console.log("Database connection disposed");
}
async query(sql: string): Promise<any> {
if (this.privIsDisposed) {
throw new Error("Cannot use disposed connection");
}
if (!this.connection.isOpen) {
throw new Error("Connection is not open");
}
// Execute query
return { result: "data" };
}
}
// Usage
const db = new DatabaseConnection("Server=localhost;Database=mydb");
try {
const result = await db.query("SELECT * FROM users");
console.log(result);
} finally {
db.dispose(); // Always cleanup
}4. Inheritance Hierarchy / Template Method Refinement
Pattern Description
Uses deep inheritance hierarchies to create specialized subclasses that inherit behavior from base classes.
Implementation in the Code
ServiceRecognizerBase (Abstract)
├── SpeechServiceRecognizer
├── TranslationServiceRecognizer
├── ConversationServiceRecognizer
└── ConversationTranscriptionServiceRecognizer
Recognizer (Abstract)
├── SpeechRecognizer
├── TranslationRecognizer
└── ConversationTranscriber
export class SpeechServiceRecognizer extends ServiceRecognizerBase {
private privSpeechRecognizer: SpeechRecognizer;
public constructor(
authentication: IAuthentication,
connectionFactory: IConnectionFactory,
audioSource: IAudioSource,
recognizerConfig: RecognizerConfig,
speechRecognizer: SpeechRecognizer) {
super(authentication, connectionFactory, audioSource, recognizerConfig, speechRecognizer);
this.privSpeechRecognizer = speechRecognizer;
}
// Specialized implementation
protected async processTypeSpecificMessages(connectionMessage: SpeechConnectionMessage): Promise<boolean> {
// ... speech-specific message processing
}
}Why It’s Used
- Specialized behavior for different recognizer types
- Code reuse through inheritance
- Each subclass implements only what’s different
- Common functionality in base class
5. Proxy Pattern (Access Control)
Pattern Description
The Proxy pattern provides a surrogate or placeholder object that controls access to another real object.
Implementation in the Code
export class Connection {
private privInternalData: ServiceRecognizerBase | SynthesisAdapterBase;
private privEventListener: IDetachable;
private privServiceEventListener: IDetachable;
/**
* Gets the Connection instance from the specified recognizer.
*/
public static fromRecognizer(recognizer: Recognizer | ConversationTranscriber): Connection {
const recoBase = recognizer.internalData as ServiceRecognizerBase;
const ret: Connection = new Connection();
ret.privInternalData = recoBase;
ret.setupEvents();
return ret;
}
/**
* Starts to set up connection to the service.
*/
public openConnection(cb?: () => void, err?: (error: string) => void): void {
marshalPromiseToCallbacks(this.privInternalData.connect(), cb, err);
}
/**
* Closes the connection the service.
*/
public closeConnection(cb?: () => void, err?: (error: string) => void): void {
// Controlled access to internal connection
}
}How It Works
- Controlled Access:
Connectioncontrols access to internalServiceRecognizerBase - Static Factory:
fromRecognizer()creates proxy instances - Wrapping Calls: Methods wrap internal calls with additional logic
Why It’s Used
- Users interact with public
ConnectionAPI - Internal
ServiceRecognizerBaseis hidden - Prevents direct access to internal implementation
- Provides cleaner public interface
Simple Example
// Proxy Pattern Example
interface IBankAccount {
deposit(amount: number): void;
withdraw(amount: number): void;
getBalance(): number;
}
class RealBankAccount implements IBankAccount {
private balance: number = 0;
deposit(amount: number): void {
this.balance += amount;
console.log(`Deposited: $${amount}`);
}
withdraw(amount: number): void {
this.balance -= amount;
console.log(`Withdrew: $${amount}`);
}
getBalance(): number {
return this.balance;
}
}
class BankAccountProxy implements IBankAccount {
private realAccount: RealBankAccount;
private pin: string;
private pinEntered: boolean = false;
constructor(pin: string) {
this.pin = pin;
this.realAccount = new RealBankAccount();
}
private validatePin(pin: string): boolean {
return this.pin === pin && (this.pinEntered = true);
}
deposit(amount: number): void {
if (this.pinEntered) {
this.realAccount.deposit(amount);
} else {
console.log("Access denied: PIN required");
}
}
withdraw(amount: number): void {
if (this.pinEntered) {
this.realAccount.withdraw(amount);
} else {
console.log("Access denied: PIN required");
}
}
getBalance(): number {
return this.pinEntered ? this.realAccount.getBalance() : -1;
}
login(pin: string): boolean {
return this.validatePin(pin);
}
}
// Usage
const account = new BankAccountProxy("1234");
account.deposit(100); // Access denied
account.login("1234");
account.deposit(100); // Deposited: $1006. Configuration/Property Pattern
Pattern Description
Uses a centralized configuration object to manage all settings and properties.
Implementation in the Code
export class PropertyCollection {
private privKeys: string[] = [] as string[];
private privValues: string[] = [] as string[];
// Get property with default fallback
public getProperty(key: PropertyId | string, def?: string | number | boolean): string {
let keyToUse: string;
if (typeof key === "string") {
keyToUse = key;
} else {
keyToUse = PropertyId[key];
}
for (let n = 0; n < this.privKeys.length; n++) {
if (this.privKeys[n] === keyToUse) {
return this.privValues[n];
}
}
if (def === undefined) {
return undefined;
}
return String(def);
}
// Set property
public setProperty(key: string | PropertyId, value: string): void {
let keyToUse: string;
if (typeof key === "string") {
keyToUse = key;
} else {
keyToUse = PropertyId[key];
}
for (let n = 0; n < this.privKeys.length; n++) {
if (this.privKeys[n] === keyToUse) {
this.privValues[n] = value;
return;
}
}
this.privKeys.push(keyToUse);
this.privValues.push(value);
}
// Clone configuration
public clone(): PropertyCollection {
const clonedMap = new PropertyCollection();
for (let n = 0; n < this.privKeys.length; n++) {
clonedMap.privKeys.push(this.privKeys[n]);
clonedMap.privValues.push(this.privValues[n]);
}
return clonedMap;
}
// Merge configurations
public mergeTo(destinationCollection: PropertyCollection): void {
this.privKeys.forEach((key: string | PropertyId): void => {
if (destinationCollection.getProperty(key, undefined) === undefined) {
const value = this.getProperty(key);
destinationCollection.setProperty(key, value);
}
});
}
}How It Works
- Key-Value Storage: Properties stored as parallel arrays
- Type-Safe Keys:
PropertyIdenum ensures valid property names - Default Values:
getProperty()returns defaults if not set - Cloning:
clone()creates independent copies - Merging:
mergeTo()combines configurations
Why It’s Used
- Centralized configuration management
- Type-safe property access
- Easy to clone and merge configurations
- Avoids constructor parameter explosion
Simple Example
// Configuration Pattern Example
enum ConfigKey {
DatabaseUrl = "DATABASE_URL",
ApiPort = "API_PORT",
LogLevel = "LOG_LEVEL",
}
class Config {
private properties = new Map<string, string>();
get(key: ConfigKey, defaultValue?: string): string {
return this.properties.get(key) || defaultValue || "";
}
set(key: ConfigKey, value: string): void {
this.properties.set(key, value);
}
clone(): Config {
const cloned = new Config();
this.properties.forEach((value, key) => {
cloned.set(key as ConfigKey, value);
});
return cloned;
}
}
// Usage
const config = new Config();
config.set(ConfigKey.DatabaseUrl, "mongodb://localhost");
config.set(ConfigKey.ApiPort, "3000");
config.set(ConfigKey.LogLevel, "debug");
console.log(config.get(ConfigKey.DatabaseUrl));
console.log(config.get(ConfigKey.ApiPort, "8080"));7. Utility/Helper Pattern (Static Utility Methods)
Pattern Description
Creates utility classes with static methods that provide common functionality without requiring instantiation.
Implementation in the Code
export class StringUtils {
public static formatString(format: string, ...args: any[]): string {
// ... formatting logic
}
public static isEmpty(str: string): boolean {
return !str || str.trim().length === 0;
}
}import { v4 as uuid } from "uuid";
const createGuid: () => string = (): string => uuid();
const createNoDashGuid: () => string = (): string => createGuid().replace(new RegExp("-", "g"), "").toUpperCase();
export { createGuid, createNoDashGuid };export class Contracts {
public static throwIfNullOrUndefined(param: any, name: string): void {
if (param === undefined || param === null) {
throw new Error("throwIfNullOrUndefined:" + name);
}
}
public static throwIfNull(param: any, name: string): void {
if (param === null) {
throw new Error("throwIfNull:" + name);
}
}
public static throwIfNullOrWhitespace(param: string, name: string): void {
this.throwIfNullOrUndefined(param, name);
if (("" + param).trim().length < 1) {
throw new Error("throwIfNullOrWhitespace:" + name);
}
}
}How It Works
- Static Methods Only: No instance creation needed
- Reusable Functions: Common functionality in one place
- Parameter Validation: Guard clause helpers prevent errors
- No State: Pure utility functions
Why It’s Used
- Eliminates need for instantiation
- Groups related utility functions
- Consistent error handling
- Easy to test and use throughout codebase
Simple Example
// Utility Pattern Example
class ValidationHelper {
static isEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static isPhoneNumber(phone: string): boolean {
const phoneRegex = /^\d{10}$/;
return phoneRegex.test(phone);
}
static isStrongPassword(password: string): boolean {
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[0-9]/.test(password) &&
/[!@#$%^&*]/.test(password);
}
static validateAndThrow(value: any, fieldName: string): void {
if (value === null || value === undefined) {
throw new Error(`${fieldName} is required`);
}
}
}
// Usage
console.log(ValidationHelper.isEmail("john@example.com")); // true
console.log(ValidationHelper.isPhoneNumber("5551234567")); // true
ValidationHelper.validateAndThrow(null, "username"); // throws