1. Problem
1.1. Context
One of your components needs to expose an interface for use by external clients.
1.2. Summary
You wish your component's interface to be as encapsulated as possible.
You
wish to maintain the compatibility of the exposed interface across
multiple versions of your component whilst retaining the freedom to
change how it is implemented.
You would like to rapidly develop a component but also allow for optimization or the addition of new features later.
1.3. Description
As the software in mobile devices becomes ever more complicated, it also becomes more difficult to both maintain and extend it.
You will often find yourself asking questions such as:
Does my software implement a specification that is subject to changes and updates?
Will I need to allow a different implementation to be provided?
Do I need to cope with different versions of software?
Do
I need to develop an early version of a component as soon as possible
which is'good enough' now but will need to be re-factored later on?
The problems of
developing software that is easy to expand, maintain and re-factor has
and always will be a key issue that you will face. A standard way to
achieve this is to ensure your interfaces are well encapsulated and
don't 'leak' any unnecessary details or dependencies especially when
external clients are relying on the interface to remain stable and not
change in any way that will stop them from working correctly.
But how can we do this in C++?
There are a number of ways to accidentally break clients. For instance,
you'd think that adding a private data member to a class would be safe
since it's private after all. However, doing this increases the size of
your class and any client code that calls a class constructor relies on
that being fixed. Indeed one of the benefits of the second-phase
construction common in Symbian OS
is that clients don't get to call the constructor directly. Instead
they get passed a pointer to the class and hence don't depend on the
size of the class which allows it to be changed. Unless it's an abstract
class and clients are expected to derive from it – then they have to
call your class's constructor which makes them sensitive to its size
again.
Whether you are developing
low-level kernel modifications, middle level services or end-user
applications, the ability to be able to change software quickly and
easily will make your life as a developer much less stressful!
1.4. Example
The HTTP framework provided
by Symbian OS supports HTTP requests and HTTP responses. A simple
design would implement these as two distinct classes as conceptually
they are used for different purposes. One factor influencing the design
would be that the requests and responses form part of a well-known
protocol that is unlikely to undergo any major changes.
The framework needed to be
developed in an iterative fashion so that clients had at least some HTTP
functionality as soon as possible. Additional features were then added
later. For example, the very first version of the HTTP framework only
supported HTTP GET. It would take a further nine months and five
iterations to fully develop a framework which had all the required
functionality that a web browser requires.
Although the two-phase
construction idiom used in Symbian OS means that the size of the classes
could be increased without breaking compatibility, it was felt that
this still exposed too much of the underlying implementation details to
the clients. If one mistake was made that meant clients accidentally
relied on the details of the implementation rather than the interface
this would've caused serious problems down the line. A way of further
separating the implementation of the classes from its interface was
needed.
It was the uncertainty of
the future design and the realization that it would almost certainly
need to change that lead to the use of this pattern not just for the
request and response classes but across the entire HTTP framework.
2. Solution
This pattern supports and
encourages encapsulation and loose coupling by hiding the implementation
details from your clients by dividing one class into two. The first of
these, the handle, simply defines the interface and then defers all implementation of that interface to the body.
A point to remember is that the
word 'handle' being referred to here does not necessarily imply the use
of a Symbian OS handle derived from RHandleBase such as RThread or RProcess.
2.1. Structure
A client is only ever able to
see the interface class and just uses that directly. However, the
interface class delegates all calls through a pointer to the underlying
handle which provides the implementation as shown in Figure 1.
This particular technique is
often referred to in C++ as the 'Cheshire Cat Idiom', named after the
character from Lewis Carroll's Alice in Wonderland,
and refers to the point where the cat disappears gradually until
nothing is left but its smile. Here the smile is the implementation
pointer.
If a data member is added to
the body class then the size of the handle class remains the same since
its size is just that of a pointer to the body. Hence the client does
not need to be recompiled as it is isolated from this change.
The handle and the body classes are normally defined in the same DLL but only the handle is exported. This binary firewall
allows the client to be shielded from any changes in the body. This is
particularly advantageous if you would like to vary the details of the
implementation depending on the situation. For instance, if the
interface is used to store and retrieve objects, the implementation
might need to change depending on exactly how many objects you expect
clients to store. This allows your interface to provide the best
performance for each situation.
2.2. Dynamics
The dynamics of this pattern aren't very complicated as the handle simply forwards all calls to its body as shown in Figure 2.
There is one additional
subtlety that should be mentioned though and that's the fact that by
forcing the body to be allocated on the heap you may
have introduced a new error path for the client to deal with. Consider
for a moment that you weren't using this pattern and instead provided an
exposed interface as a T class used solely on the client's stack.
Changing the design to use a pointer to a body class instead would mean
that clients now also have to deal with the handle possibly failing to
be created which wasn't a problem previously.
2.3. Implementation
To use this pattern you
need to separate your interface from your implementation. If you have an
existing class that you wish to convert to using this pattern then you
need to scoop out all the private data members, private functions and
implementation of the original class and put them to one side for now.
If you add a pointer to a CBody class, which we'll define later, then you have the beginnings of your handle class.
You then have the choice of implementing your handle as either an R or a C class. For this illustration, we've chosen to use a C
class to avoid obscuring the main thrust of the pattern with the
additional considerations that you need to take into account when using
an R class.
Next you should forward declare your body class in the header file used to define your handle class. This avoids you having to #include
any header files for the body and is another means of ensuring the
interface is independent of its implementation. It also incidentally
reduces the amount of code a client needs to compile when using the
interface.
This gives the following header file for the handle class:
class CBody;
class CHandle : public CBase
{
public:
IMPORT_C static CHandle* NewL();
IMPORT_C void DoSomethingL();
private:
CHandle();
private:
CBody* iBody
};
The source file for the interface simply has to forward any calls on to the body.
EXPORT_C void CHandle::DoSomethingL()
{
iBody->DoSomethingL();
}
Some additional points to note include:
No handle functions
should be declared as inline. Whilst it is tempting to do this as their
implementation is so simple, it means you will not be able to change the
function ever again without breaking binary compatibility. This is
because an inline function puts the code for the function in the
client's binary and not yours, which breaches the binary firewall that we are trying to erect.
The
handle class normally does not contain any private functions except for
the constructor functions. Instead all such functions should be
implemented by the body class.
Do
not implement the body by providing virtual functions in the handle
class and then deriving the body from the handle. Whilst this makes the
implementation of the handle slightly simpler, it effectively destroys
the benefits of this pattern by exposing the size of the class to
further derived classes; introducing vtables which are tricky to
maintain binary compatibility for, and preventing alternative bodies to
be used at run time.
No ConstructL() function is needed as normally you would just call a function on the body that returns a fully constructed object.
2.4. Consequences
Positives
This pattern allows an interface to be extended without breaking pre-existing clients.
Over
time, you will need to extend the interface to add new functionality.
This pattern gives you more flexibility when you need to extend your
component because it removes restrictions that you would otherwise have
felt. For instance, you are completely free to change the size of the
implementation class even if the interface has been derived from. Most
changes to the implementation are hidden from clients.
This
pattern reduces the maintenance costs for your component by allowing an
implementation to be replaced or re-factored without impacting clients
of your interface.
Over
time, code becomes more difficult to maintain as new features are
added. At some point it becomes more beneficial to completely replace,
re-factor or redesign the existing code to allow it to perform better or
to allow for new functionality to be added. Where this pattern has been
used, this is significantly easier to do because the interface is well
encapsulated so that as long as the interface remains compatible, the
underlying implementation can be completely re-factored or replaced.
You have the freedom to choose a different implementation.
Since
this pattern hides so much of the implementation from clients of your
interface you have more freedom to select the most appropriate body for
the circumstances. This can be done at compile time but also at run time
for even more flexibility. If you do need to dynamically load a body
then you should use the Symbian OS ECom plug-in service.
Remember that, in either case, each of the different bodies needs to
provide the same overall behavior as it's no good if clients still
compile but they then break at run time because your interface performs
in ways the client didn't expect.
Your testing costs are reduced.
Since
the interface to your component is well encapsulated you will have
fewer side-effects to deal with during testing of the component using
this pattern. However, this pattern also makes it easier to create mock
objects when testing other components by making it simple to replace the real implementation with a test implementation.
Negatives
Additional complexity can increase maintenance costs.
If
you hadn't applied this pattern then you'd have one fewer class in your
design. Whilst this doesn't sound like much, if you apply this pattern
to a lot of interfaces then you can get into a situation where class
hierarchies can be quite complicated. This can make it harder to
understand and debug your component as a result.
Additional overheads are caused by the extra indirection.
By
introducing an additional level of indirection this incurs small
execution-time and code-size penalties for your component. The extra
function call used when calling the body class, and the extra object
allocation, leads to the loss of some CPU cycles whilst the extra class
requires more code.
2.5. Example Resolved
As seen in the solution above
this is a very simple pattern, but it is always worth looking at how it
has been used in a real example.
As described above,
Symbian OS provides an interface for clients to use HTTP functionality.
One of the interfaces exposed by this component is the RHTTPSession
class which represents a client's connection with a remote entity and
allows multiple transactions to take place using this connection. This
class is the starting point for developers using the Symbian OS HTTP
service.
Since it was important to provide a stable interface to clients whilst not restricting future growth, this pattern was used.
Handle
RHTTPSession is
the handle as it provides the interface used by clients. In this case
it is an R class to indicate that it provides access to a resource owned
elsewhere, which in this case is in the HTTP protocol itself.
The following code shows
the most important points of this class from the point of view of this
pattern. The full details can be found in epoc32\include\http\rhttpsession.h.
class CHTTPSession; // Note the forward declare of the body
class RHTTPSession
{
public:
IMPORT_C void OpenL();
IMPORT_C RHTTPHeaders RequestSessionHeadersL();
...
private:
friend class CHTTPSession;
CHTTPSession* iImplementation; // Note the pointer to the body
};
The following is the simplest of several different options for beginning an HTTP session. This method, OpenL(), acts as the factory construction function for RHTTPSession and allows clients to create and initialize a session with the HTTP service.
This function call constructs the body by simply calling its NewL(). Slightly unusually, the handle function, OpenL(),
has a different name to this body function. This simply reflects the
fact that the handle is an R class whilst the body is a C class which
means developers wouldn't expect to see a NewL() on RHTTPSession whilst they would on CHTTPSession. The functions also act very slightly differently in that OpenL() is called on an existing object whilst NewL() is a static function and doesn't need to be called on an object.
Finally, note the use of Fail Fast (see page 17) to catch clients who call this function twice which would otherwise cause a memory leak.
EXPORT_C void RHTTPSession::OpenL()
{
__ASSERT_DEBUG(iImplementation == 0,
HTTPPanic::Panic(HTTPPanic::ESessionAlreadyOpen));
iImplementation = CHTTPSession::NewL();
}
The following shows an example of one of the handle's functions that simply forward the call on to the body to deal with.
EXPORT_C RHTTPHeaders RHTTPSession::RequestSessionHeadersL()
{
return iImplementation->RequestSessionHeadersL();
}
Body
The CHTTPSession class forms the body in this use of the pattern as follows:
class CHTTPSession : public CBase
{
public:
static CHTTPSession* NewL();
RHTTPHeaders RequestSessionHeadersL();
inline RHTTPSession Handle();
...
private:
CHeaderFieldPart* iConnectionInfo; // The proxy or connection details
RArray<CTransaction*> iTransactions; // All the open transactions
RArray<THTTPFilterRegistration> iFilterQueue;
...
};
The NewL()
isn't shown because it is implemented in the standard Symbian way as a
two-phase constructor. However, here is the implementation of CHTTPSession::RequestSessionHeadersL() which provides the functionality for RHTTPSession::RequestSessionHeadersL().
Note the use of Lazy Allocation
(see page 63) to create the request header collection class on demand.
Apart from that and returning an object on the stack from the function,
there is nothing unusual here.
RHTTPHeaders CHTTPSession::RequestSessionHeadersL()
{
// Create the session headers if necessary
if (iRequestSessionHeaders == NULL)
{
iRequestSessionHeaders = CHeaders::NewL(*iProtocolHandler->Codec());
}
return iRequestSessionHeaders->Handle();
}
The one additional function we should discuss is the Handle() function provided by the body which returns an RHTTPSession instance which uses the current object as its body. Providing a Handle()
function on a body class may look slightly odd at first but points to
an interesting side-effect which the HTTP framework takes full advantage
of.
If you take a closer look at RequestSessionHeadersL(), you will see that it returns an object by value rather than a reference or pointer to one. This is because RHTTPHeaders is also implemented using this pattern and is a handle to a CHeaders object. This means that RHTTPHeaders is an object of size 4 bytes, because of its iBody pointer, which makes it efficient to pass by value on the stack.
The CHTTPSession::Handle()
method works in the same way and creates a new handle to itself. The
function can be inlined because it's only ever used within your DLL and
so there's no danger of it leaking out into client DLLs and causing
compatibility problems in the future. If you were wondering why CHTTPSession was declared as a friend by RHTTPSession it's so the following would be possible:
inline RHTTPSession CHTTPSession::Handle()
{
RHTTPSession r;
r.iImplementation = this;
return r;
}
This isn't an essential part of
implementing this pattern but can be useful in other implementations as
you can use the handle class in a similar way to the way you would
treat a pointer to the body class but with more protection. Note that
this all works because only shallow copies of the handle class are made.
Hence copying a handle could mean that there are two handles pointing
to the same body just as if you'd copied a pointer. This means the
ownership of the handle classes should be treated carefully to avoid
double deleting the body.
3. Other Known Uses
Image Conversion
This component makes use of this pattern in both CImageDisplay and CImageTransform
to provide these widely used classes with the flexibility to be
extended in future to allow different image formats to be displayed and
transformed through the same interface by simply changing the underlying
body providing the implementation of the interface.
ECom
The
Symbian OS ECom plug-in service is based on this pattern as frameworks
interact with plug-ins through a handle class. A plug-in provider then
provides a body class to implement that handle.