architecture.service-provider
2019-10-07
Provides a framework for registering and finding services and providers of these.
Upstream URL
Author
Maintainer
License
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 like
make-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-instance
but 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.