> 3. Component Design
Logical Components
I break my approach to this sort of embeddable library down into three distinct components based on when they're used, how volatile they'll be, and how consistent they are between libraries. Those components are as follows, and then described in detail below:
Registration Stub
The registration stub is the piece of common glue which allows each instance of the library to find the other ones, and which lets the AddOns that use the library find the appropriate instance of the library.
Upgrade/Merge Process
Finally, there must be a standard approach to reconciling two compatible versions of the library, so that the stub code doesn't need to know the details of the upgrade process.
Library Structure
There will likely be some guidelines on how the library itself should be structured, or at the very least, some best practices. This component isn't so much code as an approach.
Assumptions and Assertions
Before leaping into the components in detail, here are some basic assumptions about the environment and the way it will be used:
* The library itself consists of one or more lua files included within another AddOn.
* At some point, the library author has to be responsible for some minimal level of versioning.
* Multiple instances of the library may be compataible (and eligible for merging) or incompatible. We must support both.
* The library can be embedded and used in both normal and LoadOnDemand AddOns.
* Some library users may wish to make local references to library components.
* The approach must not use/abuse any parts of the Blizzard UI code because it's subject to change without notice.
* There's no guarantee to what order the various versions of a library will load in, or even when.
The Registration Stub
This is expected to be a very small amount of code which is included in every instance of a library, and which does not need to change with library versions. Ideally if this component is designed correctly it will never have to change.
First, a stub must be instantiated, this rule can be very simple, if there's no current stub for the library, create a new one, otherwise dont do anything. The stub will be a lua table with some methods.
Next, during library load, the stub is responsible for working out what to do with the new library instance. We'll use the terminiology of major and minor versions for this. All instances with the same major version are 'compatible', the minor version identifies which is the most recent. Instances with different major versions are incompatible, and there is no comparison between minor versions for different major versions. The process for registration therefore looks something like this:
Process flow for library registration
Finally, the stub will also be the mechanism by which the rest of an AddOn obtains access to a library instance. The process for this is as follows:
Process flow for obtaining a library instance
with these few features the stub provides enough capabilities to obtain access to an appropriate instance of a library, and for the various instances to be aware enough of one another to upgrade cleanly. We'll add a couple of other minor details in the library component smooth some edges, but this covers the basics of the stub.
Stub Methods
We expect each stub to end up with the following methods:
libInstance = stub:Register(newInstance);
This is passed in a newly created library instance and returns the instance to use for that library's major version. It's important to note that libInstance may or may not be equal to newInstance.
libInstance = stub:GetInstance([majorVersion]);
Request the appropriate instance for the given major version. If majorVersion is not provided then the last loaded major version is to be used.
stub:ReplaceInstance(oldInstance, newInstance);
A commonly used utility method which replaces the contents of the oldInstance table with the contents of the newInstance table.
stub.lastVersion
This is where the stub stores the last major version loaded. It's given an explicit location so that on-demand loading can be handled predictably by suitably motivated authors.
The Upgrade/Merge Process
In the event that there's an existing instance of the major version of a library, and a new one is then loaded which is a newer minor version for the same major version, there's got to be a somewhat standard process by which the new minor version replaces the old one. Also this process must be repeatable in case it happens again later. This design will take the following approach:
Division of Responsibilities
The responsibility of the process is split between the registration stub and the library instances. The registration stub is responsible for identifying that an upgrade is required, and for passing this on to the appropriate instance. It also manages the list of replaced instances. The new library instance is responsible for merging any data or cleaning up the old instance. To minimize complexity for simple libraries, the stub can also take on the responsibility of copying instance tables, if the new instances chooses not to.
Recommended Considerations
Library authors should consider the following when implementing update methods:
1. For each piece of data in the old library instance, know whether it's safer to throw it away, or copy/migrate it to the new instance.
2. You can ask the old instance what version it was, and have multiple upgrade methods if it's appropriate (There's a point at which it's probably not worth it, however).
3. Try and re-use un-destroyable objects like frames.
4. Give all of your library instances a cleanup method to unregister events and the like, in case you can't re-use them. Have your newer instances call the cleanup method of the instances they replace.
Library Structure
The library will also be a lua table, with minimally some methods, and possibly some normal function and data members, depending on what it does. Since this is just a framework we want to make as few rules as possible to give maximal flexibility to library authors, and instead rely on a basic set of methods needed by the scaffold, and some guidelines to authors on the requirements of their implementation.
Library Instance Lifecycle
During its existance each library instance will go through two or more of the following lifecycle stages, and must be ready (possibly just be design with no active participation) for all of them:
Creation - The library instance is created, and at minimum it must provide the required stub API functions to set itself up and get registered.
Activation - Those versions which are not immediately discarded during registration are activated. Activation covers both entirely new major versions, as well as replacing compatible but older minor versions with the same major version. The Activation step is also a good one for a library to create any 'expensive' objects it might need
Retirement - Library instances which are initially activated but then replaced are retired by their replacements' activation step. In this framework retirement is an implied process, and is to be performed by the replacement library.
Discard - Library instances which are known at registration time to already be superceded are simply discarded.
Framework Library Methods
To support the registration process and the lifecycle above, the following methods have special meanings and expected behaviors in a library instance:
majorVersion, minorVersion = lib:GetLibraryVersion(); -- REQUIRED
This method returns the major and minor version for the library. The major version can be any non-nil object type, but is recommended to be a string. The minor version must be numeric.
skipCopy = lib:LibActivate(stub, [oldInstance, olderInstanceList]); -- REQUIRED
This method is called by the registration stub to activate a new library instance. If the instance is replacing an older one, then that is passed in, as are any other instances which had been replaced in the past. The registration stubb will add oldInstance to olderInstanceList AFTER this function returns. The return value is a flag telling the stub whether or not to skip fixing up the old instances.
lib:LibDiscard(); -- OPTIONAL
If this method is present, and the library instance is discarded without being activated, then the method is invoked to allow the instance to do any cleanup it requires. The preferred approach is to defer the creation of things that require cleanup until activation.
lib:LibAddCallback(function); -- OPTIONAL
This method isn't part of the framework per-se, but it's suggested for those libraries that provide functions for which local bindings are likely going to be obtained. When it's called, the function specified should be added to a list (possibly a weak value reference list), and in the event that the instance is upgraded later, the function called with the new instance so that local bindings can be re-created to the new instance.
Stub Recommendations
Store all shared data in a sub-table of the library - that way when an upgrade happens, each old instance can simply be given a copy of each of the new methods, and references to a single common copy of the shared data.
Think about future upgrades when designing - When you design a new feature for your library, especially one which stores data, think a little about how a future enhancement may need to upgrade the data, and make sure it's possible. For those addons which are caching data, flushing the cache is a valid upgrade technique.
