design patterns - saltict/Demo-Docs GitHub Wiki
Comprehensive overview of the design patterns and architectural decisions implemented in the SubWallet Services SDK.
📖 Navigation
- Singleton Pattern
- Factory Pattern
- Strategy Pattern
- Observer Pattern
- Template Method Pattern
- Dependency Injection
- Error Handling Patterns
The SDK implements the Singleton pattern to ensure a single instance manages all services and configuration.
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');
}
}
- 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
%%{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"
The SDK uses the Factory pattern for creating and configuring service instances.
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);
}
- Consistent Service Creation: Standardized service instantiation
- Configuration Validation: Centralized config validation
- Service Setup: Common service initialization logic
- Type Safety: Generic factory with type constraints
The Strategy pattern is used for handling different platform configurations and request strategies.
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 }
});
}
}
%%{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
The Observer pattern is implemented for configuration updates and service notifications.
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);
}
}
// 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);
});
}
}
The Template Method pattern is used in the BaseApi class to define the structure of API requests while allowing services to customize specific steps.
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');
}
}
}
The SDK uses constructor-based dependency injection for managing service dependencies.
// 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();
}
}
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());
%%{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
🔗 Related Documentation