Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
[config-dev] Loading/retrieval: deferring

I was tasked this week with producing a proposal for an API supporting the loading and retrieval use case.

After some offline conversations with a few different people on and off the committee I don't think my proposal as currently gestating is suitable given the committee's current positions on things like formats and structures and conversion and such.  Therefore I'll defer to someone else and am happy to give up my agenda slot for Thursday's meeting.

Best,
Laird

Below, for historical/posterity purposes only, is some informal notes to myself from a sketch of where I was going:

// Prior art with inspiring bits:
// * Typesafe Config (for distinction between null and missing, ConfigBeanFactory etc.)
// * Dropwizard Config (for POJO-first and user-designs-config mentality; documentation is
//   solved as the "schema" is just the config object itself and the documentation is just
//   its javadoc; no intermediate map-like structures or scalarization)
// * java.util.ResourceBundle (for distinction (clumsy) between null and missing and
//   including other implicit coordinates as part of an address (Locale.getDefault())
// * MicroProfile Config, mostly just for flat keys
// * javax.naming.Context (and object factories etc.; look up a Thing at an address;
//   it's built and made for you out of other parts; distinguishes between null and
//   missing, features rich addresses (names +
//   hash tables-representing-environmental-coordinates))

// Things I need to bear in mind:
// * one goal seems to be to allow the config system to configure itself; don't prevent this
// * no DI in the API, C or otherwise :-)
// * focus hard on class developer, not application; address-space comes to the forefront
// * address collisions: developer 1 creates address A; developer 2 creates address B;
//   A == B in some abstract way but they "target" different things; they're now combined
//   into an app; kaboom; how do we deal with this? need transliteration

// What's the main use case? Class developer asks configuration system to load a very
// particular object of a specific type. System responds with exactly the object asked for,
// or an object that is suitable, or indicates that the object isn't there, permanently
// or temporarily. Addresses are written by class developers,
// but used by applications, which combine objectspaces from multiple class developers.

// Open questions early on: if developer asks for a CharSequence, can String be returned?
// Seems like subtyping like this should work, so we're talking runtime type resolution.
// If user asks for a List<String>...?  A FancyObject<Map<String, List<String>>>...?
// (Config is, of course, DI without the I, so all the same problems exist)

// Loading:
// Get a loader thing, as simply as possible:
Loader loader = Loader.loader();

// Ideally, since this is config, loader() will *start* by using something easy like
// ServiceLoader, but maybe also has built-in post-bootstrapping use-the-loader-to-load-
// the-real-loader machinery?  So maybe a custom Loader implementation can load another
// Loader implementation using some rich facility of its own; in any event, this is and
// should be a one-liner from the class developer standpoint.

// First up: value absence, presence and determinism:
// 
// Let's posit OptionalSupplier<T>:
//
// OptionalSupplier<T> looks like an Optional<T>, let's say, but is deliberately not one,
// and its Optional-like methods do not trigger on null, but on *absence*,
// as indicated by get()'s throwing of a suitable exception, following conventions
// established by java.util.ResourceBundle, javax.naming.Context.lookup(), 
// and others.  An OptionalSupplier<T> as used here represents an optimized production
// facility for a T-typed value.  Optimized: because loader.load() may have scanned 347
// sub-providers of which only five were even remotely possible for producing this
// kind of value; the five would be encapsulated/represented by this supplier, not the
// other 342:
OptionalSupplier<String> greetingSupplier = loader.load(someKindOfAddress);

// Punt for now on registering for changes, but maybe:
// loader.load(someKindOfAddress, somethingThatLooksLikeJNDIEventContext);

// (Maybe Loader itself is also an OptionalSupplier? then you could do (in JNDI style):)
// Loader<String> greetingSupplier = loader.load(someKindOfAddress);

// Not *much* different from:
Optional<String> greetingOptional = Config.getOptionalValue("someName", String.class);
// ...but! Different nonetheless! And **the differences are critical**.

// OptionalSupplier.get() deliberately may return null or anything else at all times as a 
// valid value.
// So its orElse() etc. methods trigger on value *absence*, not value *nullness*.
// If get() throws a NoSuchElementException, for example, that would indicate absence.
// If it returns null, greeting gets null:
String greeting = greetingSupplier.get(); // could return null

// Let's say you want a default value for *absence* but not necessarily null.
// Here, the orElse() method doesn't "trigger" if greetingSupplier's get() method
// returns null, but does "trigger" if get() throws, say, a NoSuchElementException:
greeting = greetingSupplier.orElse("Hello!");

// At any point you can turn an OptionalSupplier into an Optional.  Maybe you 
// don't *want* to distinguish between null and absence in your application:
greeting = greetingSupplier.optional().orElse("Hello!");

// OK, what happens if you call get() twice?

// Let's say you want to know if the value's absence or presence is permanent:
if (greetingSupplier.isDeterministic()) {
  // Whatever get() does, it must always. do.  This might be a null value,
  // a non-null value, or a NoSuchElementException (or the like).
  // Now you know you can cache the value or whatever; you never
  // have to call get() again
} else {
  // greetingSupplier.get() might return different values, or might throw
  // to indicate values becoming absent; null is a valid value in all cases
}

// Back to loader.load(someKindOfAddress): what's the address?
// 
// someKindOfAddress is effectively a non-specific flat pointer.  It would be a combination
// of type, discriminator (name), and application-supplied coordinates (instead of
// just name and class, for example).  It is flat, and there is no hierarchy (as with
// some JAX-RS paths, for example, where attempting to get an intermediate "node" results
// in a "missing" exception).
//
// non-specific: because you are asking for something very specific, but the config machine
// may only have something very general.  "I want the greeting for the test environment
// and for the German locale and the one to be used on Thursdays" "Sorry, all I have is
// an English greeting; it's not earmarked for test or German or anything else"  Or of 
// course the thing it points to may be absent, temporarily or deterministically.
//
// A rich address lets you do:
//
// "Get me the most suitable Frobnicator<List<String>> for where my application 
// is currently running and not just any Hairball<String> but the one
// {waves hands} located at/described by/discriminated by/pointed to by
// the combination of "goop", whatever that means, and any application coordinates that
// may exist:"
Frobnicator<List<String>> fancyThing =
  loader.load(new Address<Frobnicator<List<String>>>("goop",
                                                     Loader.getAppCoordinates(),
                                                     otherDiscriminators()));

// The Loader interface can be implemented in one class that handles all loading requests 
// itself, or with multiple mediated back ends, or with a central class and an SPI
// belonging to that central class, or....

// Everything else happens behind the scenes, and none of these affect the API above:
// * conversion
// * one vs. many backends
// * object binding
// * service discovery/provider loading/loader loading
// * etc.


Back to the top