Skip to content

Provider Pattern ​

The Provider pattern is commonly used to abstract the creation and provisioning of dependencies or services in various contexts like dependency injection

Problem ​

  1. Direct instantiation inside the provider (which breaks the abstraction).
  2. No clear separation between the interface of the provider and the concrete implementation.
ts
// Incorrect Example: 
class Service {
  getData() {
    return "Service Data";
  }
}

class ServiceProvider {
  // The provider directly instantiates the service, breaking the pattern.
  provide() {
    return new Service(); // No abstraction over the service creation
  }
}

// Usage:
const provider = new ServiceProvider();
const service = provider.provide();
console.log(service.getData()); // "Service Data"

Solution ​

  1. Use an interface for the service provider.
  2. Separate creation logic to make the provider more reusable and flexible.
  3. Avoid direct instantiation within the provider class.
ts
// Correct Example:

// Service interface
interface IService {
  getData(): string;
}

// Concrete implementation of the service
class Service implements IService {
  getData() {
    return "Service Data";
  }
}

// Another possible service implementation (e.g., a mock service)
class MockService implements IService {
  getData() {
    return "Mock Service Data";
  }
}

// ServiceProvider that abstracts the instantiation of services
class ServiceProvider {
  private serviceFactory: () => IService;

  constructor(serviceFactory: () => IService) {
    this.serviceFactory = serviceFactory;  // Accepts a factory function to create the service
  }

  provide(): IService {
    return this.serviceFactory();  // Uses the factory to create the service
  }
}

// Usage:
const serviceProvider = new ServiceProvider(() => new Service());
const service = serviceProvider.provide();
console.log(service.getData());  // "Service Data"

// If we need a mock service for testing:
const mockServiceProvider = new ServiceProvider(() => new MockService());
const mockService = mockServiceProvider.provide();
console.log(mockService.getData());  // "Mock Service Data"

Explanation:

  • IService Interface: This defines the contract that all service implementations must follow. Both Service and MockService implement this interface.
  • ServiceProvider: It accepts a factory function (serviceFactory) that is responsible for creating instances of services. This abstraction allows flexibility in providing different service implementations (e.g., real or mock services).
  • Factory Function: By passing a factory function to the provider, we make it easy to change the implementation of IService without modifying the provider itself.

Benefits of the Correct Approach:

  • Decoupling: The provider doesn't need to know about the concrete Service class. It only knows how to provide an IService instance.
  • Flexibility: You can easily swap the service implementation by changing the factory function, making it easier to adapt the provider to different contexts (e.g., production vs. testing).
  • Testability: It's easy to inject a mock or stub implementation for testing purposes without modifying the provider.

Released under the MIT License.