Skip to main content
Glama
COMPOSITION_MIGRATION_GUIDE.md23.3 kB
# Composition Pattern Migration Guide ## Overview This document provides comprehensive guidance for migrating from inheritance-based architecture to the new composition pattern implemented in MCP WordPress v2.6.4+. ## Table of Contents - [Why Migrate?](#why-migrate) - [Architecture Comparison](#architecture-comparison) - [Migration Strategy](#migration-strategy) - [Implementation Examples](#implementation-examples) - [Testing Patterns](#testing-patterns) - [Common Pitfalls](#common-pitfalls) - [Best Practices](#best-practices) ## Why Migrate? ### Problems with Inheritance The previous inheritance-based architecture suffered from several issues: - **Tight Coupling**: Classes were tightly coupled through inheritance chains - **Testing Difficulties**: Hard to mock specific behaviors without mocking entire parent classes - **Ripple Effects**: Changes to base classes affected all subclasses - **Limited Flexibility**: Behavior was fixed at compile time - **SOLID Violations**: Mixed responsibilities violated Single Responsibility Principle ### Benefits of Composition The new composition pattern provides: - **Loose Coupling**: Components depend only on interfaces, not implementations - **Easy Testing**: Mock individual behaviors using dependency injection - **Isolated Changes**: Modifications affect only specific implementations - **Runtime Flexibility**: Swap behaviors dynamically - **SOLID Compliance**: Clear separation of concerns - **Enhanced Reusability**: Components can be reused across different contexts ## Architecture Comparison ### Before: Inheritance-Based ```typescript // ❌ Old Pattern - Removed in v2.6.4 class RequestManager extends BaseManager { constructor(config: WordPressClientConfig) { super(config); // Inherits all base functionality this.timeout = config.timeout || 30000; this.retries = config.maxRetries || 3; } async request(method: HTTPMethod, endpoint: string): Promise<unknown> { // Mixed concerns in single method: this.validateMethod(method); // Validation logic const auth = this.getAuthHeaders(); // Authentication logic try { const response = await this.makeHttpRequest(method, endpoint, auth); this.logSuccess(`${method} ${endpoint}`); // Logging logic return response; } catch (error) { this.handleError(error, `${method} ${endpoint}`); // Error handling logic } } // All behaviors inherited from BaseManager protected validateMethod(method: string): void { /* inherited */ } protected getAuthHeaders(): Record<string, string> { /* inherited */ } protected handleError(error: unknown, operation: string): never { /* inherited */ } protected logSuccess(operation: string): void { /* inherited */ } } ``` **Problems:** - All behaviors are inherited, creating tight coupling - Testing requires mocking the entire BaseManager - Changes to BaseManager affect all subclasses - Cannot swap individual behaviors at runtime ### After: Composition-Based ```typescript // ✅ New Pattern - v2.6.4+ export class ComposedRequestManager implements RequestHandler { constructor( private dependencies: { configProvider: ConfigurationProvider; errorHandler: ErrorHandler; validator: ParameterValidator; authProvider: AuthenticationProvider; }, ) { // Dependencies injected, not inherited } async request<T>(method: HTTPMethod, endpoint: string, data?: unknown): Promise<T> { // Each concern handled by dedicated dependency this.dependencies.validator.validateString(method, "method", { required: true }); const authHeaders = this.dependencies.authProvider.getAuthHeaders(); try { const response = await this.makeHttpRequest(method, endpoint, data, authHeaders); this.dependencies.errorHandler.logSuccess(`${method} ${endpoint}`); return response; } catch (error) { this.dependencies.errorHandler.handleError(error, `${method} ${endpoint}`); } } } ``` **Benefits:** - Each behavior is a separate, mockable dependency - Changes to validation don't affect authentication - Can swap error handlers without changing core logic - Clear separation of concerns ## Migration Strategy ### Phase 1: Interface Definition Start by defining behavioral interfaces for each concern: ```typescript // Define what each component should do, not how export interface ConfigurationProvider { readonly config: WordPressClientConfig; getConfigValue<T>(path: string, defaultValue?: T): T | undefined; getTimeout(): number; isDebugEnabled(): boolean; validateConfiguration(): void; } export interface ErrorHandler { handleError(error: unknown, operation: string): never; logSuccess(operation: string, details?: unknown): void; } export interface ParameterValidator { validateRequired(params: Record<string, unknown>, required: string[]): void; validateString(value: unknown, name: string, options?: ValidationOptions): string; validateNumber(value: unknown, name: string): number; validateWordPressId(id: unknown): number; } export interface AuthenticationProvider { authenticate(): Promise<boolean>; isAuthenticated(): boolean; getAuthHeaders(): Record<string, string>; handleAuthFailure(error: unknown): Promise<boolean>; getAuthStatus(): AuthStatus; } ``` ### Phase 2: Implementation Classes Create concrete implementations of each interface: ```typescript export class ConfigurationProviderImpl implements ConfigurationProvider { constructor(public readonly config: WordPressClientConfig) {} getConfigValue<T>(path: string, defaultValue?: T): T | undefined { return path.split(".").reduce((obj, key) => obj?.[key], this.config) ?? defaultValue; } getTimeout(): number { return this.config.timeout || 30000; } isDebugEnabled(): boolean { return process.env.NODE_ENV === "development" || process.env.DEBUG === "true"; } validateConfiguration(): void { if (!this.config.baseUrl) { throw new Error("Missing required configuration: baseUrl"); } if (!this.config.auth) { throw new Error("Missing required configuration: auth"); } } } export class ErrorHandlerImpl implements ErrorHandler { constructor(private configProvider: ConfigurationProvider) {} handleError(error: unknown, operation: string): never { const context = { operation, isDebug: this.configProvider.isDebugEnabled() }; if (error instanceof WordPressAPIError) { throw this.formatWordPressError(error, context); } if (error instanceof Error && error.message.includes("ECONNREFUSED")) { throw new Error(`Connection failed during ${operation}. Please check your WordPress site URL.`); } throw new Error(`Unknown error during ${operation}: ${String(error)}`); } logSuccess(operation: string, details?: unknown): void { if (this.configProvider.isDebugEnabled()) { debug.log(`✓ ${operation}`, details); } } } ``` ### Phase 3: Composed Manager Create the new manager using dependency injection: ```typescript export class ComposedRequestManager implements RequestHandler { private stats: ClientStats; private initialized: boolean = false; constructor(private dependencies: ComposedRequestManagerDependencies) { this.stats = this.initializeStats(); } // Factory method for convenient creation static create(clientConfig: WordPressClientConfig, authProvider: AuthenticationProvider): ComposedRequestManager { const configProvider = new ConfigurationProviderImpl(clientConfig); const errorHandler = new ErrorHandlerImpl(configProvider); const validator = new ParameterValidatorImpl(); return new ComposedRequestManager({ configProvider, errorHandler, validator, authProvider, }); } async initialize(): Promise<void> { if (this.initialized) return; this.dependencies.configProvider.validateConfiguration(); await this.dependencies.authProvider.authenticate(); this.initialized = true; } async request<T>(method: HTTPMethod, endpoint: string, data?: unknown): Promise<T> { this.ensureInitialized(); this.stats.totalRequests++; try { // Use injected dependencies this.dependencies.validator.validateString(method, "method", { required: true }); this.dependencies.validator.validateString(endpoint, "endpoint", { required: true }); const response = await this.makeRequestWithRetry(method, endpoint, data); this.stats.successfulRequests++; this.dependencies.errorHandler.logSuccess(`${method} ${endpoint}`); return response; } catch (error) { this.stats.failedRequests++; this.dependencies.errorHandler.handleError(error, `${method} ${endpoint}`); } } } ``` ### Phase 4: Factory Pattern Simplify object creation with a factory: ```typescript export class ComposedManagerFactory { createConfigurationProvider(config: WordPressClientConfig): ConfigurationProvider { return new ConfigurationProviderImpl(config); } createErrorHandler(configProvider: ConfigurationProvider): ErrorHandler { return new ErrorHandlerImpl(configProvider); } createParameterValidator(): ParameterValidator { return new ParameterValidatorImpl(); } createAuthenticationProvider(config: WordPressClientConfig): AuthenticationProvider { return ComposedAuthenticationManager.create(config); } async createComposedClient(options: ComposedClientOptions): Promise<ComposedWordPressClient> { const configProvider = this.createConfigurationProvider(options.clientConfig); const errorHandler = this.createErrorHandler(configProvider); const validator = this.createParameterValidator(); // Create and initialize authentication const authManager = new ComposedAuthenticationManager({ configProvider, errorHandler, validator, }); await authManager.authenticate(); // Create request manager const requestManager = new ComposedRequestManager({ configProvider, errorHandler, validator, authProvider: authManager, }); await requestManager.initialize(); return new ComposedWordPressClient(authManager, requestManager, options.clientConfig); } } // Convenient factory function export async function createComposedWordPressClient(config: WordPressClientConfig): Promise<ComposedWordPressClient> { const factory = new ComposedManagerFactory(); return await factory.createComposedClient({ clientConfig: config }); } ``` ## Implementation Examples ### Authentication Migration **Before (Inheritance):** ```typescript class AuthenticationManager extends BaseManager { constructor(config: WordPressClientConfig) { super(config); this.authMethod = this.detectAuthMethod(); } async authenticate(): Promise<boolean> { // Method detection and validation mixed with authentication logic switch (this.authMethod) { case "jwt": return this.authenticateJWT(); case "app-password": return this.authenticateAppPassword(); } } } ``` **After (Composition):** ```typescript export class ComposedAuthenticationManager implements AuthenticationProvider { constructor(private dependencies: AuthenticationDependencies) { this.authMethod = this.getAuthMethodFromConfig(); this.validateAuthConfiguration(); // Use injected validator } async authenticate(): Promise<boolean> { try { this.lastAuthAttempt = new Date(); switch (this.authMethod) { case "app-password": return await this.authenticateAppPassword(); case "jwt": return await this.authenticateJWT(); case "basic": return await this.authenticateBasic(); case "api-key": return await this.authenticateApiKey(); default: throw new AuthenticationError(`Unsupported method: ${this.authMethod}`, this.authMethod); } } catch (error) { this.isAuth = false; this.dependencies.errorHandler.handleError(error, "authentication"); } } private validateAuthConfiguration(): void { // Use injected validator instead of inherited method const authConfig = this.dependencies.configProvider.config.auth; if (!authConfig) { throw new AuthenticationError("No authentication configuration provided", this.authMethod); } switch (this.authMethod) { case "app-password": this.dependencies.validator.validateRequired(authConfig, ["username", "appPassword"]); break; case "jwt": this.dependencies.validator.validateRequired(authConfig, ["username", "password"]); break; } } } ``` ## Testing Patterns ### Inheritance Testing (Difficult) ```typescript // ❌ Old way - had to mock entire base class describe("RequestManager", () => { let manager: RequestManager; let mockBaseManager: Partial<BaseManager>; beforeEach(() => { // Need to mock all inherited behaviors mockBaseManager = { validateMethod: vi.fn(), getAuthHeaders: vi.fn().mockReturnValue({}), handleError: vi.fn(), logSuccess: vi.fn(), config: mockConfig, // ... many more inherited methods }; manager = new RequestManager(mockConfig); // Complex setup to override inherited methods Object.assign(manager, mockBaseManager); }); it("should make request", async () => { // Test is brittle and tests too many things at once await manager.request("GET", "/endpoint"); expect(mockBaseManager.validateMethod).toHaveBeenCalled(); expect(mockBaseManager.getAuthHeaders).toHaveBeenCalled(); // Hard to test individual behaviors in isolation }); }); ``` ### Composition Testing (Easy) ```typescript // ✅ New way - mock only what you need describe("ComposedRequestManager", () => { let requestManager: ComposedRequestManager; let mockAuthProvider: vi.Mocked<AuthenticationProvider>; let mockErrorHandler: vi.Mocked<ErrorHandler>; let mockValidator: vi.Mocked<ParameterValidator>; beforeEach(() => { // Mock only specific behaviors being tested mockAuthProvider = { authenticate: vi.fn().mockResolvedValue(true), isAuthenticated: vi.fn().mockReturnValue(true), getAuthHeaders: vi.fn().mockReturnValue({ Authorization: "Bearer token" }), handleAuthFailure: vi.fn().mockResolvedValue(true), getAuthStatus: vi.fn().mockReturnValue({ isAuthenticated: true, method: "jwt" }), }; mockErrorHandler = { handleError: vi.fn().mockImplementation((error) => { throw error; }), logSuccess: vi.fn(), }; mockValidator = { validateString: vi.fn().mockImplementation((value) => value as string), validateRequired: vi.fn(), validateNumber: vi.fn().mockImplementation((value) => Number(value)), validateWordPressId: vi.fn().mockImplementation((id) => Number(id)), }; requestManager = new ComposedRequestManager({ configProvider: mockConfigProvider, errorHandler: mockErrorHandler, validator: mockValidator, authProvider: mockAuthProvider, }); }); // Test individual behaviors in isolation it("should validate method parameter", async () => { global.fetch = vi.fn().mockResolvedValue(createMockResponse({})); await requestManager.request("GET", "/wp/v2/posts"); expect(mockValidator.validateString).toHaveBeenCalledWith("GET", "method", { required: true }); }); it("should use authentication headers", async () => { global.fetch = vi.fn().mockResolvedValue(createMockResponse({})); await requestManager.request("GET", "/wp/v2/posts"); expect(mockAuthProvider.getAuthHeaders).toHaveBeenCalled(); expect(global.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer token", }), }), ); }); it("should handle errors via error handler", async () => { const testError = new Error("Test error"); global.fetch = vi.fn().mockRejectedValue(testError); await requestManager.request("GET", "/wp/v2/posts"); expect(mockErrorHandler.handleError).toHaveBeenCalledWith(testError, "GET /wp/v2/posts"); }); }); ``` ## Common Pitfalls ### 1. Over-Abstracting **❌ Don't create interfaces for everything:** ```typescript // Unnecessary abstraction interface StringProcessor { processString(input: string): string; } class UpperCaseProcessor implements StringProcessor { processString(input: string): string { return input.toUpperCase(); } } ``` **✅ Create interfaces for behavioral contracts:** ```typescript // Meaningful abstraction interface ErrorHandler { handleError(error: unknown, operation: string): never; logSuccess(operation: string, details?: unknown): void; } ``` ### 2. Constructor Injection Overload **❌ Too many dependencies:** ```typescript class OverComplexManager { constructor( private dep1: Dependency1, private dep2: Dependency2, private dep3: Dependency3, private dep4: Dependency4, private dep5: Dependency5, private dep6: Dependency6, // ... 10+ dependencies ) {} } ``` **✅ Group related dependencies:** ```typescript interface ManagerDependencies { configProvider: ConfigurationProvider; errorHandler: ErrorHandler; validator: ParameterValidator; authProvider: AuthenticationProvider; } class WellDesignedManager { constructor(private dependencies: ManagerDependencies) {} } ``` ### 3. Leaky Abstractions **❌ Interface exposes implementation details:** ```typescript interface BadAbstraction { authenticateWithJWT(): Promise<boolean>; authenticateWithAppPassword(): Promise<boolean>; authenticateWithBasic(): Promise<boolean>; // Exposes all authentication methods } ``` **✅ Interface hides implementation details:** ```typescript interface AuthenticationProvider { authenticate(): Promise<boolean>; isAuthenticated(): boolean; getAuthHeaders(): Record<string, string>; // Implementation method is hidden } ``` ### 4. Circular Dependencies **❌ Components depend on each other:** ```typescript class ComponentA { constructor(private componentB: ComponentB) {} } class ComponentB { constructor(private componentA: ComponentA) {} // Circular dependency! } ``` **✅ Use events or mediator pattern:** ```typescript interface EventEmitter { emit(event: string, data: unknown): void; on(event: string, handler: (data: unknown) => void): void; } class ComponentA { constructor(private eventEmitter: EventEmitter) {} doSomething() { this.eventEmitter.emit("componentA.action", { data: "test" }); } } class ComponentB { constructor(private eventEmitter: EventEmitter) { this.eventEmitter.on("componentA.action", this.handleComponentAAction); } } ``` ## Best Practices ### 1. Interface Segregation Keep interfaces focused on single responsibilities: ```typescript // ✅ Good - focused interfaces interface ConfigurationReader { getConfigValue<T>(path: string, defaultValue?: T): T | undefined; } interface ConfigurationValidator { validateConfiguration(): void; } // ❌ Bad - kitchen sink interface interface ConfigurationEverything { getConfigValue<T>(path: string): T; validateConfiguration(): void; saveConfiguration(config: unknown): void; reloadConfiguration(): void; // ... 10+ more methods } ``` ### 2. Dependency Injection Inject dependencies, don't create them: ```typescript // ✅ Good - dependencies injected class ComposedManager { constructor(private dependencies: ManagerDependencies) {} } // ❌ Bad - creates own dependencies class BadManager { private errorHandler: ErrorHandler; constructor(config: Config) { this.errorHandler = new ErrorHandlerImpl(config); // Hard-coded dependency } } ``` ### 3. Factory Methods Use factories to simplify complex object creation: ```typescript // ✅ Simple creation const client = await ComposedManagerFactory.createComposedClient({ clientConfig }); // Instead of complex manual setup const configProvider = new ConfigurationProviderImpl(clientConfig); const errorHandler = new ErrorHandlerImpl(configProvider); const validator = new ParameterValidatorImpl(); const authManager = new ComposedAuthenticationManager({ configProvider, errorHandler, validator, }); await authManager.authenticate(); // ... many more steps ``` ### 4. Test-Driven Development Write tests first to drive interface design: ```typescript // Test drives interface design describe("RequestHandler", () => { it("should make HTTP requests", async () => { const requestHandler = new ComposedRequestManager(mockDependencies); const result = await requestHandler.request("GET", "/wp/v2/posts"); expect(result).toBeDefined(); }); }); // Interface emerges from test requirements interface RequestHandler { request<T>(method: HTTPMethod, endpoint: string, data?: unknown): Promise<T>; } ``` ### 5. Composition Root Create objects in a single location (composition root): ```typescript // ✅ All composition happens in factory export class ComposedManagerFactory { async createComposedClient(options: ComposedClientOptions): Promise<ComposedWordPressClient> { // Single place where all dependencies are wired together const configProvider = new ConfigurationProviderImpl(options.clientConfig); const errorHandler = new ErrorHandlerImpl(configProvider); const validator = new ParameterValidatorImpl(); const authManager = new ComposedAuthenticationManager({ configProvider, errorHandler, validator, }); const requestManager = new ComposedRequestManager({ configProvider, errorHandler, validator, authProvider: authManager, }); return new ComposedWordPressClient(authManager, requestManager, options.clientConfig); } } ``` ## Migration Checklist - [ ] **Identify Behaviors**: List all behaviors in your current inheritance hierarchy - [ ] **Define Interfaces**: Create focused interfaces for each behavior - [ ] **Implement Interfaces**: Create concrete implementations - [ ] **Create Composed Class**: Build new class using dependency injection - [ ] **Add Factory Method**: Provide convenient creation method - [ ] **Write Tests**: Create comprehensive test suite with mocks - [ ] **Update Usage**: Replace old inheritance-based usage - [ ] **Remove Old Code**: Clean up inheritance-based implementation ## Conclusion The migration from inheritance to composition provides significant benefits in terms of testability, maintainability, and flexibility. While it requires more initial setup, the long-term benefits far outweigh the costs. The composition pattern implemented in MCP WordPress v2.6.4+ demonstrates these benefits with: - **463 tests** covering composed managers with 100% success rate - **Easy mocking** of individual behaviors - **Clear separation** of concerns - **Runtime flexibility** for different environments - **Full SOLID compliance** throughout the architecture Use this guide as a reference when implementing composition patterns in your own WordPress tools or when contributing to the MCP WordPress project.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server