Subclass Initialization
It's pretty rare for me to have a class that doesn't require some kind of initialization passed through the constructor. With my first post on Factories our factory creation function didn't really give us much room to do this. So let's discuss some options for initialization.
Trivial Initialization
Suppose all of your derived classes can be initialized using one type of object, perhaps some struct with data common to all the subclasses. But from base class to base class, this initialization object may be of a different class, so we might like to continue our Generic factory pattern.
So let's start with extending our factory's template
template<class Base, class Initializer, class ClassIDKey = std::string>
class GenericFactory{
public:
using BaseCreateFn = std::function<std::unique_ptr<Base>(const Initializer&)>;
// the rest of the definition largely holds because the change
// is absorbed by this alias for the base create function
};
So let's start with extending our factory's template
template<class Base, class Initializer, class ClassIDKey = std::string>
class GenericFactory{
public:
using BaseCreateFn = std::function<std::unique_ptr<Base>(const Initializer&)>;
// the rest of the definition largely holds because the change
// is absorbed by this alias for the base create function
};
and with our Registration function:
template<class Base, class Derived, class Initializer, class ClassIDKey = std::string>
class RegisterInFactory{
public:
static std::unique_ptr<Base> CreateInstance(const Initializer& initializer){
return std::make_unique<Base>(initializer)
}
RegisterInFactory(const ClassIDKey& key){
GenericFactory<Base, Initializer, ClassIDKey>::instance().RegCreateFn(key, &CreateInstance);
}
};
Simpler Registration class
It occurs to me around now that we're relying on good development practices where, when we create our RegisterInFactory object, we create it like RegisterInFactory<Base, Derived> registerMe{"derived"}; . We're relying on good development practices to retype the Base class and Derived classes out and so on. And since we usually use an alias to give a good name to our factory, it seems silly to not be able to reuse that alias well.
Furthermore, with our initializers, we want the registry objects to have matching initializers with the factory, requiring still more template matching.
So, let's simplify:
Furthermore, with our initializers, we want the registry objects to have matching initializers with the factory, requiring still more template matching.
So, let's simplify:
template<class Base, class Initializer, class ClassIDKey = std::string>
class GenericFactory{
public:
using BaseCreateFn = std::function<std::unique_ptr<Base>(const Initializer&)>;
// ... all the usual stuff
template<class Derived>
class RegisterInFactory{
//.. moving all of our Registry functions in here, we get Base, Initializer, and ClassIDKey types for free from GenericFactory
};
};
And then in use somewhere, suppose we've defined using BaseFactory = GenericFactory<Base, Initializer>; (taking the default key type to be string), when we wish to register a derived class we can now use BaseFactory::RegisterInFactory<Derived> registerMe{"derived"};
So I'll continue to reuse this pattern going forward so I don't have to keep retyping all those template parameters.
class GenericFactory{
public:
using BaseCreateFn = std::function<std::unique_ptr<Base>(const Initializer&)>;
// ... all the usual stuff
template<class Derived>
class RegisterInFactory{
//.. moving all of our Registry functions in here, we get Base, Initializer, and ClassIDKey types for free from GenericFactory
};
};
And then in use somewhere, suppose we've defined using BaseFactory = GenericFactory<Base, Initializer>; (taking the default key type to be string), when we wish to register a derived class we can now use BaseFactory::RegisterInFactory<Derived> registerMe{"derived"};
So I'll continue to reuse this pattern going forward so I don't have to keep retyping all those template parameters.
String Map Initialization
Being even more generic, Subclasses can have different initialization needs from one class to another. When we considered our Generic Factory with Initializer, since the Factory itself is templated upon the Initializer class, we are kind of stuck with one class of object to serve initialization. Suppose that class is a struct. Now you've got to list all the possible kinds of initialization information in the struct, whether one subclass uses it or not; and you've lost the ability to dynamically add new stuff to your factory if it introduces new initialization needs.
One relatively simple way to resolve this conflict is with a map. Maps come in a variety of flavors. Let's restrict ourselves for now to what's available in the STL. (Framework factories are next time). The most generic map we could use is probably a string-string map. You can use stoi and related functions to pack numbers into strings. You can write or use various serializers to store binary data in the string*, and so on. (*: Although, maybe use a string-std::vector<uchar> map might be better if you're doing binary data, so that you don't run into the problems of string operations being designed around human-readable strings)
One relatively simple way to resolve this conflict is with a map. Maps come in a variety of flavors. Let's restrict ourselves for now to what's available in the STL. (Framework factories are next time). The most generic map we could use is probably a string-string map. You can use stoi and related functions to pack numbers into strings. You can write or use various serializers to store binary data in the string*, and so on. (*: Although, maybe use a string-std::vector<uchar> map might be better if you're doing binary data, so that you don't run into the problems of string operations being designed around human-readable strings)
We can create an Initializer class, like the trivial example above, where the map is a part of the Initialization class; supposing that some variables are always present in all subclasses, but some variables are only present in some subclasses, one could put the common ones directly in the struct and then tack on a map at the end for further customization.
Or, if the only initialization you need is just the map itself, we can define it to an alias for simplicity.
using Initializer = std::map<std::string, std::string>;
Pretty straightforward, right? I can't see why you couldn't use an unordered_map, but the maps aren't likely to be too large, they're initializers. (Classes with many initializer values usually strike me as a code smell, and probably could stand some refactoring.) Suffice to say, I often like to use the more familiar STL classes unless there's a good reason to choose another.
Coming up next: When I've used some specific frameworks, I've used some variations on this generic discussion to leverage the frameworks. Examples will be Qt's QObjects and QVariant; using a formatted file (e.g. yaml) to allow users to make program settings without having to edit or recompile code. Generic Factories within a Framework!
No comments:
Post a Comment