The template pattern is used frequently within Nektar++ to provide a common interface to a range of related classes. The base class declares common functionality or algorithms, in the form of public functions, deferring to protected virtual functions where a specific implementation is required for each derived class. This ensures code external to the class hierarchy sees a common interface.
In the top level parent class you will find the interface functions, such as Function(), declared as public members. In some cases, these may implement a generic algorithm common to all classes. In the limit that the function is entirely dependent on the derived class, it may call through to a virtual counterpart, usually named v_Function(). These functions are usually protected to allow them to be called directly by other classes in the inheritance hierarchy, without exposing them to external classes.
As an example of this, let us consider a triangle element (TriExp). The TriExp class (eventually) inherits from the StdExpansion class. The StdExpansion defines the Integral() function which is used to provide integration over an element. However, in this case, the implementation is shape-specific. Therefore StdExpansion::Integral() calls the (in this case) TriExp::v_Integral() function. We should also note that while TriExp::v_Integral() does setup work, it then makes use of its parent’s StdExpansion2D::v_Integral() function to calculate the final value. This is only possible if the v_Integral() function was declared as protected.
Nektar++ makes extensive use of the Factory Pattern. Factories are used to create (allocate) instances of classes using class-specific creator functions. More specifically, a factory will create a new object of some sub-class type but return a base class pointer to the new object. In general, there are two ways that a factory knows what specific type of object to generate: 1) The Factory’s build function (CreateInstance()) is passed a key that details what to build; or 2) The factory may have some intrinsic knowledge detailing what objects to create. The first case is almost exclusively used throughout Nektar++. The factory pattern provides the following benefits:
The NekFactory class implements the factory pattern in Nektar++. There are two distinct aspects to creating a factory-instantiated collection of classes: defining the public interface, and; registering specific implementations. Both of these tasks involve adding mostly standard boilerplate code.
It is assumed that we are writing a code which implements a particular concept or capability within the code, for which there are (potentially) multiple implementations. The reasons for multiple implementations may be low level, such as alternative algorithms for solving a linear system, or high level, such as selecting from a range of PDEs to solve. The NekFactory can be used in both cases and applied in exactly the same way.
A base class must be defined which prescribes an implementation-independent interface. In Nektar++, the template method pattern (see Section 3.5.1 above) is used, requiring public interface functions to be defined which call through to protected virtual implementation methods. This is because the factory returns the newly created object via a base-class pointer and the objects will almost always be used via this base class pointer. Without a public interface in the base class, much of the benefits and generalisation of code offered by the factory pattern would be lost. The virtual functions will be overridden in the specific implementation classes. In the base class these virtual methods should normally be defined as pure virtual, since there is typically no implementation and we will never be explicitly instantiating this base class.
As an example we will create a factory for instantiating different implementations of some
MyConcept, defined in
MyConcept.h, we need to include the NekFactory header
The following code should then be included just before the base class declaration (within the same namespace as the class):
The template parameters to the NekFactory define the datatype of the key used to retrieve a particular implementation (usually a string, enum or custom class such as MyConceptKey), the base class datatype (in our case MyConcept and a list of zero or more parameters which are taken by the constructors of all implementations of the type MyConcept (in our case we have two). Note that all implementations must take the same parameter list in their constructors. Since we have not yet declared the base class type MyConcept, we have forward-declared it above the NekFactory type definition.
The normal definition of our base class then follows:
We must also define a shared pointer for our base class, which should be declared outside the base class declaration.
A new class, derived from the base class above, is defined for each specific implementations of a concept. It is these specific implementations which are instantiated by the factory.
In our example we will have an implementations called MyConceptImpl1 defined in
MyConceptImpl1.cpp. In the header file we include the base class
We then define the derived class as one would normally:
In order for the factory to work, it must know two things:
To allow the factory to create instances of our class we define a creator function in our class, which may have arbitrary name, but is usually called create out of convention:
The example above the create function simply creates an instance of
the Nektar++ memory manager and the supplied parameters. It must be a static function
because we are not operating on any existing instance and it should return a shared pointer to
a base class object (rather than a MyConceptImpl1 shared pointer), since the point of the
factory is that the calling code does not know about specific implementations. An
advantage of having each class providing a creator function is that it allows for two-stage
initialisation – for example, initialising base-class variables based on the derived
The final task is to register each of our implementations with the factory. This is done using the RegisterCreatorFunction member function of the NekFactory. However, we wish this to happen as early on as possible (so we can use the factory straight away) and without needing to explicitly call the function for every implementation at the beginning of our program (since this would again defeat the point of a factory)! One solution is to use the function to initialise a static variable: it will force the function to be executed prior to the start of the main() routine, and can be located within the class it is registering, satisfying our code decoupling requirements.
MyConceptImpl1.h we define a static class member variable with the same datatype as the
key used in our factory (in our case std::string)
The above variable can be private since it is typically never actually used within the code. We
then initialise it in
The first parameter specifies the value of the key which should be used to select this implementation. The second parameter is a function pointer to the static function which should be used by the factory to instantiate our class. The third parameter provides a description which can be printed when listing the available MyConcept implementations. A specific implementation can be registered with the factory multiple times if there are multiple keys which should instantiate an object of this class.
To create instances of MyConcept implementations elsewhere in the code, we must first include the ”base class” header file
Note we do not include the header files for the specific MyConcept implementations anywhere
in the code (apart from
MyConceptImpl1.cpp). If we modify the implementation, only the
implementation itself requires recompilation and the executable relinking.
We create an instance by retrieving the MyConceptFactory and calling the CreateInstance member function of the factory, for example,
Note that the instance of the specific implementation is used through the pointer p, which is of type MyConceptShPtr, allowing the use of any of the public interface functions in the base class (and therefore the specific implementations behind them) to be called, but not directly any functions declared solely in a specific implementation.