1. Problem
1.1. Context
There is a pre-existing
component, the adaptee, which exposes an interface that you wish to use
via an alternative incompatible interface, the target.
1.2. Summary
You need to resolve one or more of the following:
You wish to reduce your development costs by porting a pre-existing component, the adaptee, to a new environment.
You
have re-engineered a component, the adaptee, but you wish to support
the old interface, the target, to preserve compatibility for legacy
clients.
You wish to
increase the functionality and flexibility of a component by re-using
existing components, the adaptees, at run time.
1.3. Description
This pattern was originally described in [Gamma et al.,
1994]. Hence in this discussion rather than examine the pattern itself
in too much detail, we place emphasis on how it can be used on Symbian
OS, covering a range of diverse circumstances.
The context of this
pattern of having to match incompatible interfaces can arise in numerous
different situations. Software based on Symbian OS is subject to many
pressures all of which can give rise to problems of mismatching
interfaces: it is an evolving system with a large user base, which means
that maintaining interface compatibility from release to release is
important not just for the operating system itself but also for any
services and frameworks built on top of it. It targets a domain which
covers a wide and diverse set of technologies, many of which are
standards-driven and must interoperate, and it is a specialized
operating system, carefully evolved to work in a demanding environment.
In addition, you may not be
able to change existing components to make compatible what were
incompatible interfaces. The reasons for this range from pragmatic ones
of engineering cost or risk, to business reasons (for example, licensing
terms), to quality reasons (avoiding tight coupling between logically
independent components).
Each of the following
kinds of problem demonstrates the same common factor: the need, for
whatever reason, to make incompatible interfaces work together, without
changing existing components:
Preserving compatibility
When
the legacy functionality provided by one component is made obsolete by
changes that, as your system evolves, introduce new, alternative
functionality which meet at least the same requirements, clients relying
on the legacy behavior must still be supported, if only for a
transitional period. For reasons of maintainability and code size, it
may not be feasible to leave the old component in place alongside the
newer version.
Wrapping components to ease porting
For
existing components created for a different environment from the one
you would like to use it in, it may be impossible or undesirable to make
changes to the component. This commonly arises when integrating an
industry-standard implementation of a specific technology.
The
interfaces presented to a component in the Symbian OS environment may
be incompatible with what the component expects and, vice versa, the
interfaces it presents may be incompatible with what potential Symbian
OS clients of the component expect.
For
Symbian OS, the problem is complicated by the fact that native C++
interfaces are often not directly usable by ported components written in
standard C or C++; and standard C or C++ client interfaces of ported
components are typically not directly usable by native Symbian OS
clients.
Run-time adaptation
Different
components that all store state may present very different settings
interfaces, but updating their settings may be the responsibility of a
single agent or service. Hardwiring knowledge of each different settings
interface into the updating service may be a poor design choice,
because it breaks modularity and flexibility by forcing a tight coupling
between many different components, possibly in quite different parts of
the system.
1.4. Example
Here we give an example for each of the three main uses of this pattern identified in the Description above.
Preserving Compatibility
Symbian OS
provides a communications database which is a persistent store of the
data required to configure communications components or to make
connections with remote servers. Until Symbian OS v9.1, the interface to
this communications database was via the CCommsDatabase provided by the CommDB component that used the existing OS database service DBMS.
However, in Symbian OS v9.1,
Symbian introduced a new component called CommsDat that provides the
same interface as the old CommDB, CCommsDatabase,
but uses the Symbian OS Central Repository instead. This move makes use
of the extra functionality provided by the Central Repository compared
to DBMS, such as standard tools to set up and configure the Central
Repository during development as well as performance enhancements such
as caching repositories used by multiple clients.
Since the underlying data is
stored in a different place, the old CommDB component cannot simply be
maintained alongside the new CommsDat component. Hence an alternative
solution is needed which maintains compatibility to allow the legacy
clients of CommDB to continue to work but which uses the new storage
location.
Wrapping Components to Ease Porting
Before Symbian OS v9.2,
a native database component known as DBMS provided all clients with a
database service supporting the SQL specification. However, for v9.2,
Symbian decided to port the open source SQLite component
to Symbian OS. One of the main reasons for this was that there was a
requirement to provide more SQL features, such as triggers, that aren't
provided by DBMS.
One decision taken early on
was not to attempt to attempt to preserve compatibility with the old
DBMS interface but instead to provide an interface to SQLite alongside
it. This was because, whilst SQLite provided more SQL functionality than
DBMS, it didn't, at the time, provide a superset, i.e. there is some
SQL functionality that DBMS supported that SQLite didn't.
This left the following remaining challenges to be overcome to integrate SQLite into Symbian OS:
SQLite exposes a C-based API to clients but most Symbian OS programmers would expect a Symbian OS C++ API.
The
databases accessed through SQLite need to comply with features of the
operating system such as platform security as well as backup and
restore.
SQLite expects a POSIX API to support it from underneath.
Run-time Adaptation
Consider the example of
the remote management of a device, for example, a network operator
configuring an email account on a subscriber's device 'over the air'. In
this case, the provisioning agent or service needs to provide a service
to the network that is logically identical to a service provided
locally on the device. However, it's often the case that the
standards-driven network interface is different to the interface
provided locally. Hence a means is needed to join the two interfaces
together.
That's not the end of the
problem though because clearly it would be beneficial to deploy the same
device management system on as many devices as possible and they will
vary in their support for features such as email. This means that the
central device management system needs to provide an extension point
which discovers and loads plug-ins representing the components being
remotely managed. This extension point defines a target interface into
which we need to adapt existing components.
2. Solution
This pattern decouples interfaces by creating an intermediary class, called the adapter, whose purpose is to adapt the interface presented by one class, the adapter, and present the interface, known as the target, expected by another class, the client,
that wants to use the adapter. This pattern can therefore be used to
solve the problem of incompatibility between two existing interfaces,
without requiring changes to either interface.
2.1. Structure
From the client's point of
view, only the adapter need be visible or known; the implementation of
the adapter and any details of the conversion or translation it performs
using the adaptee can remain hidden from the client. In short, the
client never doesn't directly depend on the adaptee. This is achieved by
the structure shown in Figure 1.
2.2. Dynamics
At its simplest, an adapter
is a wrapper. For example, methods are provided that convert the API of a
ported application and make it appear to be a native API to clients.
Wrappers can also be used to convert outbound system calls from the
ported component to match native Symbian OS calls. The adapter simply
redirects a call made through the target's interface to a matching
adaptee function (see Figure 2).
In more complex cases,
rather than simply wrapping each method, an adapter may be a complete
component that intercepts and converts calls made to one API into calls
to a quite different API where there doesn't exist a close match for the
target method called. The adapter may even need to use multiple
adaptees to satisfy the client's request on the target interface (see Figure 3).
In the most complex case,
at run time the adapter may need to select and load the adaptee that
satisfies the client's request on the target interface. In these more
complex cases, it is common to implement the adapter in its own DLL
separately from the adaptees so that clients have even fewer
dependencies on the adaptees. This allows the adapter to use the ECom
service to dynamically load adapters as plug-ins at run time if needed.
2.3. Implementation
There's little to say about
the implementation of this pattern over and above what's already been
said in the Structure and Dynamics sections. The one exception to this
is if you wish to load adaptees at run time. If so, you should consider
using Buckle (see page 252) to do this securely based upon the ECom plug-in service provided by Symbian OS.
2.4. Consequences
Positives
This pattern is simple to implement and understand.
Development time is reduced by enabling the re-use of existing components.
By
preserving any pre-existing interfaces, you reduce your testing costs
because any existing tests written to use the target interface also work
for the adaptees if the tests use the adapter.
It
preserves decoupling between the target and adaptee interfaces. By
keeping interfaces decoupled, your solution remains flexible which makes
future maintenance and evolution easier. Typically, it is much easier
to update the adapter in the case of future changes than to try to
evolve the clients or the adaptee if that would even be possible.
In
the case of a run-time adapter design, new adaptees can be integrated
with potentially no change to the client or even the adapter. This makes
your design more open and easily extensible.
Negatives
It imposes additional
overheads on the use of an adaptee interface because an additional layer
is interposed between the client and the functionality it wants to use
in the adaptee. This increases both the code size and the execution time
for the solution although they are unlikely to be significant in all
but the most extreme of cases.
If
you are using this pattern to preserve compatibility by maintaining a
legacy API in addition to a new API then this necessarily increases the
complexity of the system since there are now two APIs for clients to
choose from during development as well as two APIs for the provider of
the adapter to support.
Using
a run-time adapter based on the ECom plug-in service has no impact on
execution time once the plug-in has been loaded. However, searching for
and loading a plug-in is an additional overhead compared to statically
loading a DLL which is why, if your intention is just to wrap a single
legacy component, a regular DLL could be a better choice.
2.5. Example Resolved
Preserving Compatibility
In the example described above there was a need to maintain a target interface, CCommsDatabase,
originally implemented by the CommDB component. However this interface
needed to work with the new location for the communications database
within the Symbian OS Central Repository instead of the old DBMS
location. Figure 4 shows the old structure of CommDB.
This problem was solved by
removing the old CommDB component and providing a replacement, the
adapter, called the CommDB shim which implements the CCommsDatabase
interface, the target, in terms of the new communications database
component CommsDat, the adaptee. This is possible because CommsDat
provides all the functionality of CommDB and more. Figure 5 shows the new structure.
This solution clearly provides source compatibility between the legacy and the new shim implementations of the CCommsDatabase
interface since the same class is used in both. Binary compatibility is
also preserved by providing the new adapter in a binary with the same
name as used in the legacy solution, commdb.dll.
However, this doesn't guarantee that the new implementation of the old
interface will behave exactly as it used to. The only way to guarantee
that this behavioral compatibility has been maintained is to ensure the
tests written for the legacy CommDB component work for the new shim
version.
The result of this is that Symbian OS now has two APIs to one communications database: CCommsDatabase and CMDBSession.
Wrapping Components to Ease Porting
The example described above
for this use of the pattern was the porting of SQLite to Symbian OS. The
first problem was that, as SQLite is implemented in standard C, it
expects a POSIX API to support it from underneath to provide interfaces
into the memory model, the file system, and so on. This was solved by
using the PIPS
to provide near-perfect support for the SQLite C calls, requiring
little or no tuning of the SQLite code and working 'straight out of the
box'.
More challenging was
providing an interface for use by clients of SQLite. The problem was not
that the interface needed to be compatible with the legacy database
service, DBMS. Instead the existing interface is written in C and most
Symbian OS programmers would expect a Symbian OS C++ API that supports
the system-wide functionality, such as platform security as well as
backup and restore. So a new interface for Symbian OS, based on C++ was
needed.
The key to understanding the
final design is to remember that the new interface to SQLite would have
to check the security credentials of any clients attempting to access a
database. Since this can only be done securely across a process
boundary, this immediately rules out the use of Client-Thread Service (see page 171) to provide the service interface. Instead, Client–Server
(see page 182) was selected to be able to provide a secure service
interface to the underlying SQLite component. Accordingly the new
component is called the SQL Server. This server provides all the support
specific to Symbian OS needed by clients accessing the SQLite library
underneath. It resulted in the component structure shown in Figure 6.
Run-time Adaptation
Another example of this
pattern is provided by the Symbian OS Device Management (DM) subsystem,
which supports all aspects of remote provisioning of devices (for
example by network operators) to manage device settings. In this case,
multiple adapters are required which implement a common provisioning
interface CSmlDmAdapter, the target, which
is used by the DM client on the handset acting as an agent for a DM
server hosted remotely off the device. The adapters map the common
target DM interface into various settings interfaces provided by diverse
Symbian OS components, for example phone, messaging, and mail.
This gives rise to the structure shown in Figure 7.
However, this only partly solves the design problem. Some issues still remain:
Shielding the client from changes in the adaptee with a binary firewall (not just a compilation firewall) to reduce any dependencies during development.
Keeping track of the number of adapters available on the device.
Loading the appropriate adapter based on the type of settings it supports.
Supporting
the addition or removal of adapter implementations to or from the
device at run time to allow third-party developers to supply adapters
for the DM client.
Here ECom comes to the rescue.
By implementing the adapters as ECom plug-ins, the adapters have to be
provided in a separate DLL from the DM client, thereby making it
completely agnostic of the implementation details or locations of
adapters.
Issues 2 and 3 are resolved by ECom's REComSession::List-ImplementationsL() and REComSession::CreateImplementationL() methods, which allow the DM client to specify a 'tag' that identifies the plug-ins it is looking for.
Furthermore, the registry of
ECom plug-ins is updated whenever a plug-in is added or removed.
Clients of the ECom service can register for notification of when this
happens, through REComSession::NotifyOnChange().
This is used by the DM Client so that it can maintain an up-to-date
list of adapter implementations available on the device, which addresses
issue 4.
This example also
illustrates how adapters can potentially handle more than one adaptee.
Going back to the definition of DM, provisioning a device includes
operations such as adding, deleting or modifying various device-level
settings. Though there are different types of such settings, each one
must support these three main operations. The OMA DM provisioning standard defines these as ADD, DELETE and REPLACE 'commands' respectively.
Consider the case of
remotely managing the MMS accounts on a device. The MMS framework in
Symbian uses two classes to store the details of the MMS accounts:
CMmsAccounts
is the container for MMS accounts. Each MMS Account item holds the ID
and name of the account along with a reference to its MMS Settings
object.
CMmsSettings
holds all the configuration information that is associated with an MMS
account. It includes settings such as proxy and IAP settings, delivery
notification flags, image specifications, and so on.
To 'ADD' an MMS account,
the remote server sends the details via a SyncML message. The DM
framework passes the received details and uses ECom to decide that it
should be passed on to the MMS adapter (CDmMmsAdapter). CDmMmsAdapter in turn has to deal with both CMmsAccounts and CMmsSettings.
Figure 8 shows a simplified version of how the DM MMS adapter distributes work to its multiple adaptees.
When instructed to create a new MMS account, the CDmMmsAdapter does the following:
// Create a new MMS Settings object
mmsSettings = CMmsSettings::NewL(); // Adaptee 1
CleanupStack::PushL(mmsSettings);
// Add this to Settings array
iMmsSettings.AppendL(mmsSettings);
// Ownership transferred
CleanupStack::Pop(mmsSettings);
// Device-management house-keeping code
// Create a new MMS account
iMmsAccounts.CreateMMSAccountL(aName, *mmsSettings); // Adaptee 2
This is followed by calls to set (or reset) various MMS Settings parameters (as and when the DM commands arrive), e.g.
// Set an MMS Proxy
iMmsSettings[index].AddProxyL(aProxyUid);
or
// Set delivery notification
iMmsSettings[index].SetAllowDeliveryNotification(aAllow);
3. Other Known Uses
Preserving Compatibility
A
number of features of Symbian OS may be excluded from a device by a
device manufacturer, usually because it doesn't have the hardware to
support them. For example, this is true of infrared and Bluetooth.
However, if the feature is excluded completely then any existing
software that uses the feature would fail to compile as the APIs it
depends on aren't present. Instead a mechanism is provided to allow
existing software to compile and instead check at run time if the
feature is present.
This
pattern is used to allow such clients to compile by adapting the target
interface of a feature to a non-existent adaptee. The adapter simply
provides stubs for the target interface that return KErr-NotSupported.
This provides a binary- and source-compatible component for clients to
use though, of course, it's not possible to provide behavior
compatibility!
Porting
Java, Freetype, and OpenGL have all been ported to Symbian OS using this pattern.
Run-time Adapters
SyncML
uses ECom-based adapters to implement an extensible framework for
handling multiple transport mechanisms. A number of Transport Connection
Adapters (TCAs) exist for the various transport types (HTTP, OBEX over
USB, OBEX over Bluetooth, WSP, etc.) and are implemented as ECom
plug-ins. This allows device manufacturers to create new adapter
plug-ins to add support for new transport mechanisms.