Reputation: 947
I'm facing maybe a non-problem with some C++ class design, mostly because I need to integrate a database into my system. I've got a base class that has some child classes. I find it redundant to have a member called type
which is an enum which described the type
of child it is. Example
enum FooTypes {
kFooTypeGeneric,
kFooTypeA,
kFooTypeB
};
class Foo {
public:
Foo(FooTypes type):type(type){}
FooTypes type;
};
class FooA : public Foo {
public:
FooA():Foo(kFooTypeA){}
};
class FooB : public Foo {
public:
FooB() :Foo(kFooTypeB) {}
};
The reason that I feel forced to maintain an enum is because owners of Foo
s want to store what they have created into a database table. If the system reboots, they should be able to look into their own table and say, "Oh yeah, I have a FooA
that I need to initialize", and that can only really be done by setting a column called "FooType" to 1 in this case.
I'm just wondering if this way of giving child classes a type that is part of an enum that they then must know about is bad design. It seems redundant among other things.
Upvotes: 3
Views: 138
Reputation: 117298
You could use polymorphism and use overridden streaming functions for the derived classes. You'd then need a factory function to create different derived object depending on what you read from the database.
Here's a small example where the database is an std::istringstream
holding what has previously been saved.
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
// An abstract base class for all entities you can store in the database
struct Foo {
virtual ~Foo() = default;
virtual void serialize(std::ostream&) const = 0; // must be overridden
virtual void deserialize(std::istream&) = 0; // must be overridden
};
// streaming proxies, calling the overridden serialize/deserialize member functions
std::ostream& operator<<(std::ostream& os, const Foo& f) {
f.serialize(os);
return os;
}
std::istream& operator>>(std::istream& is, Foo& f) {
f.deserialize(is);
return is;
}
//--------------------------------------------------------------------------------------
class FooA : public Foo {
public:
void serialize(std::ostream& os) const override {
// serializing by streaming its name and its properties
os << "FooA\n" << a << ' ' << b << ' ' << c << '\n';
}
void deserialize(std::istream& is) override {
// deserializing by reading its properties
if(std::string line; std::getline(is, line)) {
std::istringstream iss(line);
if(not (iss >> a >> b >> c)) is.setstate(std::ios::failbit);
}
}
private:
int a, b, c;
};
class FooB : public Foo {
public:
void serialize(std::ostream& os) const override {
os << "FooB\n" << str << '\n';
}
void deserialize(std::istream& is) override {
std::getline(is, str);
}
private:
std::string str;
};
//--------------------------------------------------------------------------------------
// A factory function to create derived objects from a string by looking it up in a map
// and calling the mapped functor.
//--------------------------------------------------------------------------------------
std::unique_ptr<Foo> make_foo(const std::string& type) {
static const std::unordered_map<std::string, std::unique_ptr<Foo>(*)()> fm = {
{"FooA", []() -> std::unique_ptr<Foo> { return std::make_unique<FooA>(); }},
{"FooB", []() -> std::unique_ptr<Foo> { return std::make_unique<FooB>(); }},
};
if(auto it = fm.find(type); it != fm.end()) return it->second(); // call the functor
throw std::runtime_error(type + ": unknown type");
}
//--------------------------------------------------------------------------------------
// Deserialize all Foos from a stream
//--------------------------------------------------------------------------------------
std::vector<std::unique_ptr<Foo>> read_foos(std::istream& is) {
std::vector<std::unique_ptr<Foo>> entities;
std::string type;
while(std::getline(is, type)) { // type is for example "FooA" here
// Call make_foo(type), put the result in the vector and
// stream directly to the added element (C++17 or later required)
if(not (is >> *entities.emplace_back(make_foo(type)))) {
throw std::runtime_error(type + ": deserializing failure");
}
}
return entities;
}
//--------------------------------------------------------------------------------------
int main() {
std::istringstream db(
"FooA\n"
"1 2 3\n"
"FooB\n"
"Hello world\n"
);
auto entities = read_foos(db);
std::cout << "serialize what we got:\n";
for(auto& fooptr : entities) {
std::cout << *fooptr;
}
}
Output:
serialize what we got:
FooA
1 2 3
FooB
Hello world
Upvotes: 1
Reputation: 17454
This is fine. Generally you want to avoid the need to "know" a type in a polymorphic hierarchy ‐ if you're fine with the performance of virtual dispatch, it should be all you need in a good design.
But sometimes you do need a strong mapping (say, for passing to some external resource), and having an enum identify the actual "type" of an instance saves you having a string of dynamic_cast
to do the same job.
You could have the best of both worlds, and make a virtual
function that returns the right enum for the class. But then frankly you're forcing a loss of performance for nothing.
Upvotes: 0