design patterns - saltict/Demo-Docs GitHub Wiki

Design Patterns

Comprehensive overview of the design patterns and architectural decisions implemented in the SubWallet Services SDK.

📖 Navigation


Table of Contents

Singleton Pattern

The SDK implements the Singleton pattern to ensure a single instance manages all services and configuration.

Implementation

export class SubWalletServiceSDK {
  private static instance: SubWalletServiceSDK | null = null;
  
  // Private constructor prevents external instantiation
  private constructor() {
    this.initializeServices();
  }
  
  // Static method to get the singleton instance
  static getInstance(): SubWalletServiceSDK {
    if (!SubWalletServiceSDK.instance) {
      SubWalletServiceSDK.instance = new SubWalletServiceSDK();
    }
    return SubWalletServiceSDK.instance;
  }
  
  // Prevent cloning
  private clone(): SubWalletServiceSDK {
    throw new Error('Cannot clone singleton instance');
  }
}

Benefits

  • Resource Management: Single instance prevents multiple HTTP clients
  • Configuration Consistency: Centralized configuration state
  • Memory Efficiency: Reduced memory footprint
  • Service Coordination: Single point of service orchestration

Pattern Diagram

%%{init: {'theme':'dark', 'themeVariables': { 
  'primaryColor': '#ff6b6b', 
  'primaryTextColor': '#fff', 
  'primaryBorderColor': '#ff6b6b', 
  'lineColor': '#ffa726', 
  'sectionBkColor': '#2d3748', 
  'altSectionBkColor': '#1a202c', 
  'gridColor': '#4a5568', 
  'secondaryColor': '#4a5568', 
  'tertiaryColor': '#2d3748'
}}}%%
classDiagram
    class SubWalletServiceSDK {
        -static instance: SubWalletServiceSDK
        -config: SDKConfig
        -serviceList: BaseApi[]
        
        -constructor()
        +static getInstance(): SubWalletServiceSDK
        +updateConfig(config: Partial<SDKOption>): void
    }
    
    class Client1 {
        +useSDK(): void
    }
    
    class Client2 {
        +useSDK(): void
    }
    
    Client1 --> SubWalletServiceSDK: getInstance()
    Client2 --> SubWalletServiceSDK: getInstance()
    
    note for SubWalletServiceSDK "Only one instance exists"
Loading

Factory Pattern

The SDK uses the Factory pattern for creating and configuring service instances.

Service Factory Implementation

class ServiceFactory {
  static createService<T extends BaseApi>(
    ServiceClass: new (config: ApiConfig) => T,
    config: ApiConfig
  ): T {
    // Validate configuration
    this.validateConfig(config);
    
    // Create service instance
    const service = new ServiceClass(config);
    
    // Apply any common service setup
    this.setupService(service);
    
    return service;
  }
  
  private static validateConfig(config: ApiConfig): void {
    if (!config.baseUrl) {
      throw new Error('Base URL is required');
    }
  }
  
  private static setupService(service: BaseApi): void {
    // Common service initialization
    service.setupErrorHandling();
    service.enableRequestLogging();
  }
}

// Usage in SDK constructor
private initializeServices(): void {
  const config = this.apiConfig;
  
  this.balanceDetectionApi = ServiceFactory.createService(BalanceDetectionApi, config);
  this.priceHistoryApi = ServiceFactory.createService(PriceHistoryApi, config);
  this.swapApi = ServiceFactory.createService(SwapApi, config);
  this.xcmApi = ServiceFactory.createService(XcmApi, config);
  this.cardanoApi = ServiceFactory.createService(CardanoApi, config);
}

Benefits

  • Consistent Service Creation: Standardized service instantiation
  • Configuration Validation: Centralized config validation
  • Service Setup: Common service initialization logic
  • Type Safety: Generic factory with type constraints

Strategy Pattern

The Strategy pattern is used for handling different platform configurations and request strategies.

Platform Strategy Implementation

interface PlatformStrategy {
  getHeaders(): Record<string, string>;
  getRequestDefaults(): RequestInit;
  handleSpecialCases(request: RequestOptions): RequestOptions;
}

class ExtensionStrategy implements PlatformStrategy {
  getHeaders(): Record<string, string> {
    return {
      'SW-SDK-Platform': 'extension',
      'User-Agent': 'SubWallet-Extension'
    };
  }
  
  getRequestDefaults(): RequestInit {
    return {
      credentials: 'omit', // Extensions don't send cookies
      mode: 'cors'
    };
  }
  
  handleSpecialCases(request: RequestOptions): RequestOptions {
    // Extension-specific request modifications
    return request;
  }
}

class MobileStrategy implements PlatformStrategy {
  getHeaders(): Record<string, string> {
    return {
      'SW-SDK-Platform': 'mobile',
      'User-Agent': 'SubWallet-Mobile'
    };
  }
  
  getRequestDefaults(): RequestInit {
    return {
      credentials: 'include',
      timeout: 30000 // Longer timeout for mobile networks
    };
  }
  
  handleSpecialCases(request: RequestOptions): RequestOptions {
    // Mobile-specific optimizations
    if (request.method === 'GET') {
      request.headers = {
        ...request.headers,
        'Cache-Control': 'max-age=300' // 5-minute cache for mobile
      };
    }
    return request;
  }
}

// Strategy context
class PlatformContext {
  private strategy: PlatformStrategy;
  
  constructor(platform: string) {
    this.strategy = this.createStrategy(platform);
  }
  
  private createStrategy(platform: string): PlatformStrategy {
    switch (platform) {
      case 'extension':
        return new ExtensionStrategy();
      case 'mobile':
        return new MobileStrategy();
      case 'webapp':
        return new WebAppStrategy();
      default:
        return new ExtensionStrategy(); // Default fallback
    }
  }
  
  executeRequest(request: RequestOptions): Promise<Response> {
    const headers = this.strategy.getHeaders();
    const defaults = this.strategy.getRequestDefaults();
    const processedRequest = this.strategy.handleSpecialCases(request);
    
    return fetch(processedRequest.url, {
      ...defaults,
      ...processedRequest,
      headers: { ...headers, ...processedRequest.headers }
    });
  }
}

Strategy Pattern Diagram

%%{init: {'theme':'dark', 'themeVariables': { 
  'primaryColor': '#ff6b6b', 
  'primaryTextColor': '#fff', 
  'primaryBorderColor': '#ff6b6b', 
  'lineColor': '#ffa726', 
  'sectionBkColor': '#2d3748', 
  'altSectionBkColor': '#1a202c', 
  'gridColor': '#4a5568', 
  'secondaryColor': '#4a5568', 
  'tertiaryColor': '#2d3748'
}}}%%
classDiagram
    class PlatformStrategy {
        <<interface>>
        +getHeaders(): Record~string, string~
        +getRequestDefaults(): RequestInit
        +handleSpecialCases(request): RequestOptions
    }
    
    class ExtensionStrategy {
        +getHeaders(): Record~string, string~
        +getRequestDefaults(): RequestInit
        +handleSpecialCases(request): RequestOptions
    }
    
    class MobileStrategy {
        +getHeaders(): Record~string, string~
        +getRequestDefaults(): RequestInit
        +handleSpecialCases(request): RequestOptions
    }
    
    class WebAppStrategy {
        +getHeaders(): Record~string, string~
        +getRequestDefaults(): RequestInit
        +handleSpecialCases(request): RequestOptions
    }
    
    class PlatformContext {
        -strategy: PlatformStrategy
        +executeRequest(request): Promise~Response~
    }
    
    PlatformStrategy <|-- ExtensionStrategy
    PlatformStrategy <|-- MobileStrategy
    PlatformStrategy <|-- WebAppStrategy
    PlatformContext --> PlatformStrategy
Loading

Observer Pattern

The Observer pattern is implemented for configuration updates and service notifications.

Configuration Observer Implementation

interface ConfigurationObserver {
  onConfigurationChanged(newConfig: ApiConfig): void;
}

class ConfigurationSubject {
  private observers: ConfigurationObserver[] = [];
  private config: ApiConfig;
  
  addObserver(observer: ConfigurationObserver): void {
    this.observers.push(observer);
  }
  
  removeObserver(observer: ConfigurationObserver): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  
  notifyObservers(): void {
    this.observers.forEach(observer => {
      observer.onConfigurationChanged(this.config);
    });
  }
  
  updateConfiguration(newConfig: Partial<ApiConfig>): void {
    this.config = { ...this.config, ...newConfig };
    this.notifyObservers();
  }
}

// Services implement the observer interface
export class BaseApi implements ConfigurationObserver {
  constructor(protected config: ApiConfig) {
    // Register as observer for configuration changes
    ConfigurationManager.addObserver(this);
  }
  
  onConfigurationChanged(newConfig: ApiConfig): void {
    this.config = newConfig;
    this.reinitializeConnections();
  }
  
  private reinitializeConnections(): void {
    // Reinitialize HTTP client with new configuration
    this.httpClient = new HttpClient(this.config);
  }
}

Event-Driven Updates

// SDK implementation with observer pattern
export class SubWalletServiceSDK extends ConfigurationSubject {
  updateConfig(newConfig: Partial<SDKOption>): void {
    // Update internal configuration
    this.config = { ...this.config, ...newConfig };
    
    // Generate new API configuration
    const apiConfig = this.apiConfig;
    
    // Notify all service observers
    this.updateConfiguration(apiConfig);
  }
  
  constructor() {
    super();
    this.initializeServices();
    
    // Register services as observers
    this.serviceList.forEach(service => {
      this.addObserver(service);
    });
  }
}

Template Method Pattern

The Template Method pattern is used in the BaseApi class to define the structure of API requests while allowing services to customize specific steps.

BaseApi Template Implementation

export abstract class BaseApi {
  // Template method defining the request flow
  async executeRequest<T>(options: RequestOptions): Promise<T> {
    // Step 1: Pre-process request (can be overridden)
    const processedOptions = this.preprocessRequest(options);
    
    // Step 2: Validate request (can be overridden)
    this.validateRequest(processedOptions);
    
    // Step 3: Execute HTTP request (fixed implementation)
    const response = await this.performHttpRequest(processedOptions);
    
    // Step 4: Process response (can be overridden)
    const processedResponse = this.processResponse<T>(response);
    
    // Step 5: Post-process result (can be overridden)
    return this.postprocessResult(processedResponse);
  }
  
  // Hook methods that can be overridden by subclasses
  protected preprocessRequest(options: RequestOptions): RequestOptions {
    // Default implementation
    return options;
  }
  
  protected validateRequest(options: RequestOptions): void {
    // Default validation
    if (!options.path) {
      throw new Error('Request path is required');
    }
  }
  
  protected processResponse<T>(response: Response): T {
    // Default response processing
    return response.json() as Promise<T>;
  }
  
  protected postprocessResult<T>(result: T): T {
    // Default post-processing
    return result;
  }
  
  // Fixed implementation that shouldn't be overridden
  private async performHttpRequest(options: RequestOptions): Promise<Response> {
    const url = `${this.config.baseUrl}/${options.path}`;
    return fetch(url, {
      method: options.method || 'GET',
      headers: {
        'Content-Type': 'application/json',
        ...this.config.headers,
        ...options.headers
      },
      body: options.data ? JSON.stringify(options.data) : undefined
    });
  }
}

// Service-specific implementations
export class SwapApi extends BaseApi {
  // Override preprocessing for swap-specific logic
  protected preprocessRequest(options: RequestOptions): RequestOptions {
    // Add swap-specific headers
    return {
      ...options,
      headers: {
        ...options.headers,
        'X-Swap-Version': '2.0'
      }
    };
  }
  
  // Override validation for swap-specific rules
  protected validateRequest(options: RequestOptions): void {
    super.validateRequest(options);
    
    if (options.method === 'POST' && !options.data) {
      throw new Error('Swap requests require data payload');
    }
  }
}

Dependency Injection

The SDK uses constructor-based dependency injection for managing service dependencies.

Dependency Injection Implementation

// Dependency injection container
class DIContainer {
  private services: Map<string, any> = new Map();
  private factories: Map<string, () => any> = new Map();
  
  register<T>(key: string, factory: () => T): void {
    this.factories.set(key, factory);
  }
  
  resolve<T>(key: string): T {
    if (this.services.has(key)) {
      return this.services.get(key);
    }
    
    const factory = this.factories.get(key);
    if (!factory) {
      throw new Error(`Service ${key} not registered`);
    }
    
    const service = factory();
    this.services.set(key, service);
    return service;
  }
}

// Service registration
const container = new DIContainer();

container.register('httpClient', () => new HttpClient(config));
container.register('errorHandler', () => new ErrorHandler());
container.register('logger', () => new Logger());

// Service with injected dependencies
export class BaseApi {
  constructor(
    protected config: ApiConfig,
    private httpClient = container.resolve<HttpClient>('httpClient'),
    private errorHandler = container.resolve<ErrorHandler>('errorHandler'),
    private logger = container.resolve<Logger>('logger')
  ) {
    this.initialize();
  }
}

Error Handling Patterns

Chain of Responsibility Pattern for Error Handling

abstract class ErrorHandler {
  protected next?: ErrorHandler;
  
  setNext(handler: ErrorHandler): ErrorHandler {
    this.next = handler;
    return handler;
  }
  
  handle(error: Error): Error | null {
    if (this.canHandle(error)) {
      return this.processError(error);
    }
    
    if (this.next) {
      return this.next.handle(error);
    }
    
    return error; // Unhandled error
  }
  
  protected abstract canHandle(error: Error): boolean;
  protected abstract processError(error: Error): Error;
}

class NetworkErrorHandler extends ErrorHandler {
  protected canHandle(error: Error): boolean {
    return error.message.includes('fetch') || error.message.includes('network');
  }
  
  protected processError(error: Error): Error {
    return new SdkError('Network connectivity issue', {
      type: ErrorType.NETWORK_ERROR,
      originalError: error,
      retryable: true
    });
  }
}

class ValidationErrorHandler extends ErrorHandler {
  protected canHandle(error: Error): boolean {
    return error.message.includes('validation') || error.message.includes('invalid');
  }
  
  protected processError(error: Error): Error {
    return new SdkError('Invalid request parameters', {
      type: ErrorType.VALIDATION_ERROR,
      originalError: error,
      retryable: false
    });
  }
}

// Error handling chain setup
const errorChain = new NetworkErrorHandler();
errorChain
  .setNext(new ValidationErrorHandler())
  .setNext(new APIErrorHandler())
  .setNext(new GenericErrorHandler());

Error Handling Flow

%%{init: {'theme':'dark', 'themeVariables': { 
  'primaryColor': '#ff6b6b', 
  'primaryTextColor': '#fff', 
  'primaryBorderColor': '#ff6b6b', 
  'lineColor': '#ffa726', 
  'sectionBkColor': '#2d3748', 
  'altSectionBkColor': '#1a202c', 
  'gridColor': '#4a5568', 
  'secondaryColor': '#4a5568', 
  'tertiaryColor': '#2d3748'
}}}%%
graph TD
    A[Error Occurs] --> B[NetworkErrorHandler]
    B --> C{Can Handle?}
    C -->|Yes| D[Process Network Error]
    C -->|No| E[ValidationErrorHandler]
    
    E --> F{Can Handle?}
    F -->|Yes| G[Process Validation Error]
    F -->|No| H[APIErrorHandler]
    
    H --> I{Can Handle?}
    I -->|Yes| J[Process API Error]
    I -->|No| K[GenericErrorHandler]
    
    K --> L[Process Generic Error]
    
    D --> M[Return Processed Error]
    G --> M
    J --> M
    L --> M
Loading

🔗 Related Documentation

⚠️ **GitHub.com Fallback** ⚠️