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:
Encourages modularisation of code such that conceptually related algorithms are grouped together;
Structuring of code such that different implementations of the same concept are encapsulated and share a common interface;
Users of a factory-instantiated modules need only be concerned with the interface and not the details of underlying implementations;
Simplifies debugging since code relating to a specific implementation resides in a single class;
The code is naturally decoupled to reduce header-file dependencies and improves compile times;
Enables implementations (e.g. relating to third-party libraries) to be disabled through the build process (CMake) by not compiling a specific implementation, rather than scattering preprocessing statements throughout the code.
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
concept MyConcept
, defined in MyConcept.h
and MyConcept.cpp
.
First in MyConcept.h
, we need to include the NekFactory header
1#include <LibUtilities/BasicUtils/NekFactory.hpp>
The following code should then be included just before the base class declaration (within the same namespace as the class):
1class MyConcept 2 3// Datatype for the MyConcept factory 4typedef LibUtilities::NekFactory< std::string, MyConcept, 5 ParamType1, 6 ParamType2 > MyConceptFactory; 7MyConceptFactory& GetMyConceptFactory();
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:
1class MyConcept 2{ 3 public: 4 MyConcept(ParamType1 p1, ParamType2 p2); 5 ... 6};
We must also define a shared pointer for our base class, which should be declared outside the base class declaration.
1typedef boost::shared_ptr<MyConcept> MyConceptShPtr;
A new class, derived from the base class above, is defined for each specific implementation of a concept. It is these specific implementations which are instantiated by the factory.
In our example we will have an implementation called MyConceptImpl1
defined in
MyConceptImpl1.h
and MyConceptImpl1.cpp
. In the header file we include the base class
header file
1#include <MyConcept.h>
We then define the derived class as one would normally:
1class MyConceptImpl1 : public MyConcept 2{ 3... 4};
In order for the factory to work, it must know two things:
that MyConceptImpl1
exists; and
how to create an instance of it.
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:
1/// Creates an instance of this class 2static MyConceptSharedPtr create( 3 ParamType1 p1, 4 ParamType2 p2) 5{ 6 return MemoryManager<MyConceptImpl1>::AllocateSharedPtr(p1, p2); 7}
In the example above the create
function simply creates an instance of MyConceptImpl1
using
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
type.
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.
In 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
)
1static std::string className;
The above variable can be private since it is typically never actually used within the code. We
then initialise it in MyConceptImpl1.cpp
1string MyConceptImpl1::className 2 = GetMyConceptFactory().RegisterCreatorFunction( 3 "Impl1", 4 MyConceptImpl1::create, 5 "First implementation of my concept.");
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
1#include <MyConcept.h>
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,
1ParamType p1 = ...; 2ParamType p2 = ...; 3MyConceptShPtr p = GetMyConceptFactory().CreateInstance( "Impl1", p1, p2 );
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.