From 2764d6453f91b4416e76e5fe923c6ed2552ea14a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jul 2024 11:32:50 +0200 Subject: [PATCH] docs,changesets: update to mention the manual service factory options pattern Signed-off-by: Patrik Oldsberg --- .changeset/spotty-apricots-tan.md | 20 ++++++++++ .../architecture/03-services.md | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/.changeset/spotty-apricots-tan.md b/.changeset/spotty-apricots-tan.md index 8eea2b1dce..a2be38d775 100644 --- a/.changeset/spotty-apricots-tan.md +++ b/.changeset/spotty-apricots-tan.md @@ -52,6 +52,26 @@ export const customFooServiceFactory = createServiceFactory({ This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. +In cases where the old options patterns significantly improves the usability of the service factory, the old pattern can still be implemented like this: + +```ts +const fooServiceFactoryWithOptions = (options?: { bar: string }) => + createServiceFactory({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }); + +export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), +); +``` + This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. A separate benefit of this change is that it simplifies the TypeScript types in a way that allows TypeScript to provide a much better error message when a service factory doesn't properly implement the service interface. diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index e1d86164c3..95e3d5305a 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -223,3 +223,43 @@ export const customFooServiceFactory = createServiceFactory({ ``` This allows you to provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. + +## Service Factory Options Pattern + +:::note Note + +This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration or re-implementation instead. + +::: + +In some cases it might be beneficial to allow users of your service factory to pass options to the factory itself, rather than to the service implementation. This can be enabled by also defining the service factory as a function that returns a reconfigured factory. For example: + +```ts +const fooServiceFactoryWithOptions = (options?: { + transform: (foo: string) => string; +}) => + createServiceFactory({ + service: fooServiceRef, + deps: {}, + factory() { + return DefaultFooService.create({ + transform: options?.transform, + }); + }, + }); + +export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), +); +``` + +This makes it possible to use the `fooServiceFactory` directly, as well passing additional options to create a customized factory: + +```ts +backend.add(fooServiceFactory); +// OR +backend.add(fooServiceFactory({ transform: foo => foo.toLowerCase() })); +``` + +This pattern is discouraged due to the inability to access other services through dependency injection. It is however used in a few places in the Backstage framework where the ability to directly pass options without re-implementing the service is very convenient, such as the `mockServices` from `@backstage/backend-test-utils`.