C++-Adapters are full C++-classes wrapped around their counterpart C-structs "classes".
The the so called "Adapter pattern" is also known as the "Wrapper pattern". For a detailed description of this Object Oriented Design Pattern, see Gof, pp. 139.
As Gof point out, the Adapter Pattern's applicability comprises the following points: "Use the Adapter pattern when
- you wnat to use an existing class, and its interface does not match the one you need.
- you want to create a reusable class that cooperates with unrelated or unforeseen classes, that is, classes that don't necessarily have compatible interfaces.
- (object adapter only) you need to use several existing subclasses, but it's impractical to adapt their interface by subclassing every one. An object adapter can adapt the interface of tis parent class. "
As far as "Collaborations"" are concerned, the GoF has to say the following:
"Clients call operations on an Adapter instance. In turn, the adapter calls Adaptee operations that carry out the request."
(The GoF has much more to say about Adapter pattern's consequences, see pp. 142 XXX)
This approach of adapting the underlying C-structs is fruitful for a variety of reasons:
Advantages
- Compared to their C-structs, full C++ adapter classes define "proper" constructors and destructors, meaning that
- the objects are "easier to handle" on the user side: creating objects, and having them destroyed porperly through the language's own garbage collection system
- Easy to implement: mainly these full-feature classes do a simple delegation ("pass-through") of their own methods to the struct's original functions, while making them more type-safe
- C++-Classes are gargabe collected
- Garbage collected classes are
- easier to instantiate
- easier to work with
- make inheritance, interfaces and composition explicit
- therefore minimize risks
- while still holding up the speed of the original C implementation.
- Since the Adpater pattern makes it possible to generate compatible interfaces for other classes, in our case here, a crucial advantage can be found in that the existing code base need not be rewritten, but stays as it is. Imagine a vendor class interface with compiled objects, that you don't have direct access to, except for the documented interface. In all these cases the adapter pattern proves to be ideal.
- Declarative OO-Programming:
- In having the full class-system available, reliable Interfaces can be defined that all deriving classes have to implement. Thus, usage of all derived types of automata is made more reliable and safer and generalizable behavior can be summed up.
- Should the underlying C-Layer or the interface change, all deriving classes will adapt to the new behavior.
- New programmers to the project can tell the interdependency of explicitely declared classes (vs. structs) more easily.
- New programmers to the project can find their way into the code base more easily.
- New programmers to the project can make changes to the existing code base more easily and reliably through strict compile time checking of validness of the newly added code.
- Safe use of full C++-classes in Generative Programming:
- In creating the DocumentIndexingAutomaton, the adapter classes can be put to good use: Basically there is the standard option for extending an existing automaton to accomplish additional tasks: inheritance. With a class of automata however (InenagaCDAWG, UkkonenSuffixTree, BasicSuffixTree, SCDAWG, ...), *each* of these classes would have to form a class of its own (e.g. DocumentIndexingSCDAWG) and *each* of these classes would have to be writen separately.
- Since what we have here is a *familiy* of automata, the DocumentIndexing-behavior can be generalized through a templated class DocumentIndexingAutomaton<AutomatonType> (which still is capable of specialization for certain tasks for certain automata). This is a great way of code re-use, reliability, and following the DRY (Don't repeat yourself) principle of "good programming".
- Should the underlying pure-C-implementation change, all adapter classes can incorporate these changes and make sure all derived classes won't be affected.
The adapter pattern used here is inherently very tightly coupled to the Layered-System cf. Layer System Desription
General Description of Adapter Classes
Adapter classes are relatively easy to implement in that the adapting class keeps a pointer to the adaptee and, in general, forwards calls to the adaptee's functions.
Adapter classes can do more though. Not only can Adapter classes specifically override some (or all) of adaptee's behavior, adapters can also add behavior to the class adapted.
In our specific case here, not only do the adapter classes wrap the original C-structs, but also give them additional properties, like inheritability.
Conventions for adapter classes in this distribution
The Adapter classes defined in this distribution follow these conventions:
- The `struct SCDAWG`'s adapter can be found as the `class SCDAWGAdapter`
- `SCDAWGAdd(SCDAWG* scdawg, VoidSequence * input)` will turn into `SCDAWGAdapter::Add(VoidSequence * input)`
- adapted methods/functions are spelled with a Capital Letter
- newly added methods are spelled regularly using lower case letters
- Following the pattern, the adapting class holds a pointer to its adaptee:
- Class SCDAWGAdapter will hold a pointer to the struct SCDAWG from the C-Layer as `SCDAWG* C_SCDAWG`.
- Naming Convention: all adapter classes end in <OriginalStructsName>Adapter.
Construction and Destruction Handling in Adapter Classes
Construction and Destruction are handled transpararently in the following way:
- An Adapter class holds a private member:
- VoidSequenceAdapter will hold a private member C_VoidSequence which will point to the struct tVoidSequence
- (Ideally) exactly only one constructor per Adapter Class, will call VoidSequenceInitialize to do a proper initialization.
- The other constructor available to the adapter class are merely to make configuration eaiser on the user's end and do Constructor-Forwarding as needed
- The destructor ~VoidSequenceAdapter() will call VoidSequenceFree(C_VoidSequence) to release the pointer properly.
- Note, however, that this destructor -- in contrast to the user calling VoidSequenceFree() will be called through garbage collection automatically.