architecture.service-provider

2019-10-07

Provides a framework for registering and finding services and providers of these.

Upstream URL

github.com/scymtym/architecture.service-provider

Author

Jan Moringen <jmoringe@techfak.uni-bielefeld.de>

Maintainer

Jan Moringen <jmoringe@techfak.uni-bielefeld.de>

License

LLGPLv3
README
architecture.service-provider README

1Introduction

In software architectures, a common feature is parametrization ofalgorithms or protocols with higher order functions or classes andgeneric functions complying to a certain protocol. These places ofpotential variations can be thought of as requiring a certainservice which can be provided by arbitrary providers (See [[*JavaService 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 likemake-test-result-formatter instead of letting clients callcl:make-instance directly.
  • It is sometimes desirable to be able to enumerate all knownproviders of a given service.
  • Compile-time analysis of provider instantiation requests canreveal errors early or enable transformation into more efficientcode.
  • 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.

    https://travis-ci.org/scymtym/architecture.service-provider.svg

2Tutorial

2.1Defining a Service

In the simplest case, a service is a named collection of providerswith an optional documentation string:
     (service-provider:define-service my-service
       (:documentation
        "Providers of this service do stuff."))
     #<STANDARD-SERVICE MY-SERVICE (0) {1004A98793}>

2.2Registering a Provider

A provider of a service can be anything for which a method onservice-provider:make-provider, and optionallyservice-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 {1004AAED33}>

2.3Instantiating a Provider

The primary way to instantiate a service provider isservice-provider:make-provider which resembles cl:make-instancebut takes a service and a provider designator instead of a classdesignator:
     (service-provider:make-provider 'my-service 'my-class :foo 1)
     #<MY-CLASS {1004AEE443}>

2.4Introspecting a Service

Introspection of services works much like CLOS introspection: afterretrieving a service object, reader functions as well ascl: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 {1004AAED33}>))
     #<STANDARD-SERVICE MY-SERVICE (1) {1004A98793}>

     Providers:
     #<CLASS-PROVIDER MY-CLASS {1004AAED33}>

     "Providers of this service do stuff."

2.5Compilation

The system has some support for detecting uses of non-existentservices and providers at compile-time. In particular, theservice-provider:make-provider function can signalcl:style-warning s when service and/or provider arguments areconstant 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 (1) {1004A98793}> is
       known for the designator :NO-SUCH-PROVIDER.

2.6TODOEfficiency Considerations

3Dictionary

    (ql:quickload '(:architecture.service-provider :alexandria :split-sequence))
    (defun doc (symbol kind)
      (let* ((lambda-list (sb-introspect:function-lambda-list symbol))
             (string      (documentation symbol kind))
             (lines       (split-sequence:split-sequence #\Newline string))
             (trimmed     (mapcar (alexandria:curry #'string-left-trim '(#\Space)) lines)))
        (format nil "~(~A~) ~<~{~A~^ ~}~:@>~2%~{~A~^~%~}"
                symbol (list lambda-list) trimmed)))

3.1Service Protocol

The following generic functions operate on service objects:
     (doc 'service-provider:service-name 'function)
     service-name SERVICE

     Return the symbol which is the name of SERVICE.
     (doc 'service-provider:service-providers 'function)
     service-providers SERVICE

     Return a sequence of the providers of SERVICE.
     (doc 'service-provider:service-providers/alist 'function)
     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.
     (doc 'service-provider:service-providers/plist 'function)
     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:

     (doc 'service-provider:find-service 'function)
     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).
     (doc '(setf service-provider:find-service) 'function)
     (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.

3.2Provider Protocol

The following generic functions operate on provider objects:
     (doc 'service-provider:provider-name 'function)
provider-name PROVIDER

Return the symbol which is the name of PROVIDER.

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

     (doc 'service-provider:find-provider 'function)
   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).
     (doc '(setf service-provider:find-provider) 'function)
   (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.
     (doc 'service-provider:update-provider 'function)
update-provider SERVICE NAME PROVIDER

Update the provider designated by NAME in SERVICE with the new
value PROVIDER.
     (doc 'service-provider:add-provider 'function)
add-provider SERVICE NAME PROVIDER

Add PROVIDER to SERVICE as the provider designated by NAME.
     (doc 'service-provider:remove-provider 'function)
remove-provider SERVICE NAME PROVIDER

Remove PROVIDER from SERVICE as the provider designated by NAME.
     (doc 'service-provider:make-provider 'function)
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.

3.3Convenience Layer

The following convenience functions and macros are provided forregistering services:
     (doc 'service-provider:register-service 'function)
   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.
     (doc 'service-provider:define-service 'function)
   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.

The following convenience functions are provided for registering providers:

     (doc 'service-provider:register-provider 'function)
   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.
     (doc 'service-provider:register-provider/class 'function)
   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.
     (doc 'service-provider:register-provider/function 'function)
   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.

4Related Work

4.1Java 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 bysymbols (or lists of symbols).
  • Introspection is modeled after CLOS introspection, e.g.cl:find-class.
  • Documentation is modeled after and integrates cl:defclass andcl:documentation.
  • Redefinitions and class-changes of services and service providersare supported.
  • Support for compile-time error-detection and optimizations can beadded.

Dependencies (6)

  • alexandria
  • architecture.hooks
  • fiveam
  • let-plus
  • more-conditions
  • utilities.print-items

Dependents (1)

  • GitHub
  • Quicklisp