architecture.service-provider

2017-06-30

Introduction

In software architectures, a common feature is parametrization of algorithms or protocols with higher order functions or classes and generic functions complying to a certain protocol. These places of potential variations can be thought of as requiring a certain service which can be provided by arbitrary providers (See [[*Java Service Provider Interface]] for a related approach).

While Common Lisp supports these designs very well on a language level (via symbols naming functions, first class functions, cl:make-instance, etc.), it is often useful to go a little bit beyond these builtin features:
  • It is considered good practice to use something like make-test-result-formatter instead of letting clients call cl:make-instance directly.
  • It is sometimes desirable to be able to enumerate all known providers of a given service.
  • Compile-time analysis of provider instantiation requests can reveal errors early or enable transformation into more efficient code.
  • Service providers can be loaded lazily when they are instantiated.

This system adds first class services and service providers to facilitate use cases like the above while trying to avoid conceptual mismatches with the builtin mechanisms.

Tutorial

Defining a Service

In the simplest case, a service is a named collection of providers with an optional documentation string:

  (service-provider:define-service my-service
    (:documentation
     "Providers of this service do stuff."))
#<STANDARD-SERVICE MY-SERVICE (0) {1007248B33}>

Registering a Provider

A provider of a service can be anything for which a method on service-provider:make-provider, and optionally service-provider:make-provider-form, is defined.

For the common cases in which a provider instantiates a class or calls a functions, the builtin provider classes service-provider:class-provider and service-provider:function-provider can be used. The easiest way to register providers of these two kinds are the functions service-provider:register-provider/class and service-provider:register-provider/function respectively.

For example, registering a class as a provider of a service can be accomplished like this:

  (defclass my-class ()
    ((foo :initarg :foo)))

  (service-provider:register-provider/class 'my-service 'my-class)
#<CLASS-PROVIDER MY-CLASS {1007325293}>

Instantiating a Provider

The primary way to instantiate a service provider is service-provider:make-provider which resembles cl:make-instance but takes a service and a provider designator instead of a class designator:

  (service-provider:make-provider 'my-service 'my-class :foo 1)
#<MY-CLASS {1007BCD683}>

Introspecting a Service

Introspection of services works much like CLOS introspection: after retrieving a service object, reader functions as well as cl:describe and cl:documentation can be applied to it:

  (let ((service (service-provider:find-service 'my-service)))
    (print (list (service-provider:service-name service)
                 (service-provider:service-providers service)))
    (fresh-line)
    (describe service)
    (fresh-line)
    (print (documentation service t)))

:

(MY-SERVICE (#<CLASS-PROVIDER MY-CLASS {100EEF8AC3}>))
#1=#<STANDARD-SERVICE MY-SERVICE (1) {100ED79C73}>

:

Providers:
#1=#<CLASS-PROVIDER MY-CLASS {100EEF8AC3}>

:

"Providers of this service do stuff."

Compilation

The system has some support for detecting uses of non-existent services and providers at compile-time. In particular, the service-provider:make-provider function can signal cl:style-warning s when service and/or provider arguments are constant and do not designate existing services or providers:

  (handler-bind ((warning (lambda (condition)
                            (format t "~S:~%~2@T~A~%"
                                    (type-of condition) condition))))
    (compile nil '(lambda ()
                    (service-provider:make-provider :no-such-service :provider)))
    (compile nil '(lambda ()
                    (service-provider:make-provider 'my-service :no-such-provider))))
SERVICE-PROVIDER:MISSING-SERVICE-WARNING:
  No service is known for the designator :NO-SUCH-SERVICE.
SERVICE-PROVIDER:MISSING-PROVIDER-WARNING:
  No provider of service #<STANDARD-SERVICE MY-SERVICE (0) {1005A0A2B3}> is
  known for the designator :NO-SUCH-PROVIDER.

TODO Efficiency Considerations

Dictionary

Service Protocol

The following generic functions operate on service objects:

service-name SERVICE

:

Return the symbol which is the name of SERVICE.
service-providers SERVICE

:

Return a sequence of the providers of SERVICE.
service-providers/alist SERVICE

:

Return the providers of SERVICE as an alist in which CARs are
provider names and CDRs are the corresponding provider objects.
service-providers/plist SERVICE

:

Return the providers of SERVICE as a plist in which keys are
provider names and values are the corresponding provider
objects.

The following generic functions query and manipulate the global set of services:

#+beginexample find-service NAME &KEY IF-DOES-NOT-EXIST

Find and return the service designated by the `service-designator' NAME.

IF-DOES-NOT-EXIST controls the behavior in case the designated service cannot be found:

The values #'error and 'error cause a `missing-service-error' to be signaled.

The values #'warn and 'warn cause a `missing-service-warning' to be signaled and nil to be returned.

The value nil causes nil to be returned without any conditions being signaled.

`retry' and `use-value' restarts are established around error signaling (if IF-DOES-NOT-EXIST mandates that).

#+endexample

#+beginexample (setf find-service) NEW-VALUE NAME &KEY IF-DOES-NOT-EXIST

Set the service designated by the `service-designator' NAME to NEW-VALUE. When non-nil, NEW-VALUE has to implement the service protocol.

If NAME already designates a service, the existing service object is replaced with NEW-VALUE.

If NEW-VALUE is nil, an existing service designated by NAME is removed.

IF-DOES-NOT-EXIST is accepted for parity with `find-service' and usually ignored. However, when NEW-VALUE is nil, IF-DOES-NOT-EXIST controls whether an error should be signaled in case the to-be-removed service does not exist.

#+endexample

Provider Protocol

The following generic functions operate on provider objects:

provider-name PROVIDER

:

Return the symbol which is the name of PROVIDER.

The following generic functions query and manipulate the providers of a service:

#+beginexample find-provider SERVICE PROVIDER &KEY IF-DOES-NOT-EXIST

Find and return the provider designated by the `provider-designator' PROVIDER in the service designated by the `service-designator' SERVICE.

IF-DOES-NOT-EXIST controls the behavior in case SERVICE or PROVIDER cannot be found:

The values #'error and 'error cause a `missing-service-error' to be signaled if SERVICE cannot be found and a `missing-provider-error' to be signaled if PROVIDER cannot be found.

The values #'warn and 'warn cause a `missing-service-warning' to be signaled if SERVICE cannot be found and a `missing-provider-warning' to be signaled if PROVIDER cannot be found. In both cases, nil is returned.

The value nil causes nil to be returned without any conditions being signaled.

`retry' and `use-value' restarts are established around error signaling (if IF-DOES-NOT-EXIST mandates that).

#+endexample

#+beginexample (setf find-provider) NEW-VALUE SERVICE PROVIDER &KEY IF-DOES-NOT-EXIST

Set the provider designated by the `provider-designator' PROVIDER in the service designated by the `service-designator' SERVICE to NEW-VALUE. When non-nil, NEW-VALUE has to implement the provider protocol.

If SERVICE and PROVIDER already designate a provider, the existing provider object is replaced with NEW-VALUE.

If NEW-VALUE is nil, an existing provider designated by SERVICE and PROVIDER is removed.

IF-DOES-NOT-EXIST is accepted for parity with `find-provider' and usually ignored. However, when NEW-VALUE is nil, IF-DOES-NOT-EXIST controls whether an error should be signaled in case the to-be-removed provider does not exist.

#+endexample

update-provider SERVICE NAME PROVIDER

:

Update the provider designated by NAME in SERVICE with the new
value PROVIDER.
add-provider SERVICE NAME PROVIDER

:

Add PROVIDER to SERVICE as the provider designated by NAME.
remove-provider SERVICE NAME PROVIDER

:

Remove PROVIDER from SERVICE as the provider designated by NAME.
make-provider SERVICE PROVIDER &REST ARGS

:

Make and return an instance of the provider designated by the
`provider-designator' PROVIDER of the service designated by the
`service-designator' SERVICE.

Convenience Layer

The following convenience functions and macros are provided for registering services:

#+beginexample register-service NAME SERVICE-CLASS &REST INITARGS

Register a service named NAME according to SERVICE-CLASS and INITARGS.

If NAME does not name an existing service, an instance of SERVICE-CLASS is made with INITARGS and registered.

If NAME names an existing service, the service is updated via re-initialization, potentially changing its class to SERVICE-CLASS.

The new or updated service instance is returned.

#+endexample

#+beginexample define-service NAME &BODY OPTIONS

Define a service named NAME with additional aspects specified in OPTIONS.

The following OPTIONS are accepted:

(:service-class CLASS-NAME)

Name of the class of the to-be-defined service. Defaults to `standard-service'.

(:documentation STRING)

If NAME already designates a service, the existing service object is destructively modified according to OPTIONS.

The service definition is performed at compile, load and execute time to ensure availability in subsequent provider definitions and/or compilation of e.g. `find-service' calls.

#+endexample

The following convenience functions are provided for registering providers:

#+beginexample register-provider SERVICE-NAME PROVIDER-NAME PROVIDER-CLASS &REST INITARGS

Register a provider of SERVICE-NAME according to PROVIDER-NAME, PROVIDER-CLASS and INITARGS.

If PROVIDER-NAME does not name an existing provider of the service designated by SERVICE-NAME, an instance of PROVIDER-CLASS is made with INITARGS and registered.

If PROVIDER-NAME names an existing provider of the service designated by SERVICE-NAME, the provider is updated via re-initialization, potentially changing its class to PROVIDER-CLASS.

The new or updated provider instance is returned.

#+endexample

#+beginexample register-provider/class SERVICE-NAME PROVIDER-NAME &REST ARGS &KEY (PROVIDER-CLASS 'CLASS-PROVIDER) (CLASS PROVIDER-NAME) &ALLOW-OTHER-KEYS

Register CLASS as the provider named PROVIDER-NAME of the service designated by SERVICE-NAME.

PROVIDER-CLASS can be used to select the class of which the created provider should be an instance.

The `cl:documentation' of CLASS is used as the documentation of the provider.

#+endexample

#+beginexample register-provider/function SERVICE-NAME PROVIDER-NAME &REST ARGS &KEY (PROVIDER-CLASS 'FUNCTION-PROVIDER) #'PROVIDER-NAME &ALLOW-OTHER-KEYS

Register FUNCTION as the provider named PROVIDER-NAME of the service designated by SERVICE-NAME.

PROVIDER-CLASS can be used to select the class of which the created provider should be an instance.

The `cl:documentation' of FUNCTION is used as the documentation of the provider.

#+endexample

Related Work

Java Service Provider Interface

See documentation of the ServiceLoader class for details.

Differences:
  • architecture.service-providers does not tie services to classes (or interfaces); services and providers are identified by symbols (or lists of symbols).
  • Introspection is modeled after CLOS introspection, e.g. cl:find-class.
  • Documentation is modeled after and integrates cl:defclass and cl:documentation.
  • Redefinitions and class-changes of services and service providers are supported.
  • Support for compile-time error-detection and optimizations can be added.

Settings

Author
Jan Moringen <jmoringe@techfak.uni-bielefeld.de>
Maintainer
Jan Moringen <jmoringe@techfak.uni-bielefeld.de>
License
LLGPLv3