问题
In a nutshell, I want to use a single interface, IProducer
, to create an object, IProduct
. IProduct
will have different components depending on which interface created it. The IProduct
class will then be used by the IConsumer
interface. The correct IConsumer
class should be used (I do not want to do type checking myself) based on the derived type of IProduct
.
I would essentially like to use the Strategy pattern (different behaviors behind a single interface), but with the added ability to return an object specific to the derived interface used. I want to abide by the Open/Close principle and not alter any of these existing classes when more functionality is added.
I would like to accomplish something like this (I'm sure the syntax is wrong somewhere but bear with me):
class IProduct {
public:
int intData;
};
class ProductA : public IProduct {
public:
float floatData;
};
class ProductB : public IProduct {
public:
bool boolData;
};
class IProducer {
public:
virtual IProduct* produce(void) = 0;
};
class ProducerA : public IProducer {
public:
IProduct* produce(void) {
return new ProductA;
}
};
class ProducerB : public IProducer {
public:
IProduct* produce(void) {
return new ProductB;
}
};
class IConsumer {
public:
virtual void consume(IProduct* prod) = 0;
};
class ConsumerA : public IConsumer {
public:
void consume(IProduct* prod) {
//I want to access the float!
}
};
class ConsumerB : public IConsumer {
public:
void consume(IProduct* prod) {
//I want to access the bool!
}
};
void main() {
IProducer* producer = ProducerFactory::create("ProducerA");
IProduct* product = producer->produce();
IConsumer* consumer = ConsumerFactory::create("ConsumerA");
consumer->consume(product); //I would like the correct consumer to be used here to deal with the ProductA class
}
If you think there is a better way to go about this I'm all ears. Thanks for your help!
回答1:
What you need is a registry which maps IProduct
implementations to the right IConsumer
implementations. Basically its just an abstraction of a map:
class ConsumerRegistry
{
std::map<size_t, std::shared_ptr<IConsumer>> m_consumers;
public:
// we are not responsible for products, so lets allow plain ptrs here for more flexibility and less overhead
std::shared_ptr<IConsumer> GetConsumer(IProduct* product)
{
auto it = m_consumers.find(typeid(product).hash_code());
if (it == m_consumers.end())
return nullptr;
else
return it->second;
}
template<typename P>
void RegisterConsumer(std::shared_ptr<IConsumer> consumer)
{
m_consumers.emplace(typeid(P).hash_code(), consumer);
}
template<typename P>
void UnregisterConsumer()
{
m_consumers.erase(typeid(P).hash_code());
}
};
Either expose this class globally (e.g as singleton) or use it in the contexts where you need it. You register consumers like this:
reg.RegisterConsumer<ProductA>(new ConsumerA());
reg.RegisterConsumer<ProductB>(new ConsumerB());
We could also have a virtual void Register(ConsumerRegistry& reg) = 0;
method inside IConsumer
allowing for safer registering:
void ConsumerA::Register(ConsumerRegistry& reg, std::shared_ptr<IConsumer> self)
{
IConsumer::Register<ProductA>(reg, self);
}
// Required for friendship, can be static:
template<typename T>
void IConsumer::Register(ConsumerRegistry& reg, std::shared_ptr<IConsumer> self)
{
reg->RegisterConsumer<T>(self);
}
void ConsumberRegistry::RegisterConsumer(std::shared_ptr<IConsumer> consumer)
{
consumer->Register(*this, consumer);
}
Make both Register()
and the low-level RegisterConsumer()
methods private and let ConsumerRegistry
and IConsumer
be friends. Can be used like this then:
reg.RegisterConsumer(new ConsumerA());
reg.RegisterConsumer(new ConsumerB());
回答2:
This is a solution I'm thinking of using. I'd appreciate any feedback.
I'm going to use the Visitor pattern and introduce the ProductVisitor
class like so:
class IProductVisitor {
public:
explicit IProductVisitor() {}
virtual ~IProductVisitor(){}
virtual void visitA(ProductA* model) = 0;
virtual void visitB(ProductB* model) = 0;
};
class ProductTypeVisitor : public IProductVisitor {
public:
typedef enum {Unknown, A, B} ProductType;
explicit ProductTypeVisitor() : modelType(Unknown) {}
virtual ~ProductTypeVisitor(){}
virtual void visitA(ProductA* product) {
modelType = A;
}
virtual void visitB(ProductB* product) {
modelType = B;
}
ProductType getProductType(void) {
return modelType;
}
ProductType modelType;
};
class IProduct {
public:
IProduct() : intData(3) {}
virtual ~IProduct(){}
int intData;
virtual void accept(IProductVisitor* v) = 0;
};
class ProductA : public IProduct {
public:
ProductA() : IProduct(), floatData(5.5) { }
virtual ~ProductA(){}
float floatData;
void accept(IProductVisitor* v) {
v->visitA(this);
}
};
class ProductB : public IProduct {
public:
ProductB() : IProduct(),boolData(false) { }
virtual ~ProductB(){}
bool boolData;
void accept(IProductVisitor* v) {
v->visitB(this);
}
};
When making my factory, ConsumerFactor
, I will use the ProductTypeVisitor
class to determine what class the product is, dynamically cast it correctly (based off of the state of the enum
), and then return a consumer initialized with the correct product.
class ConsumerFactory {
public:
explicit ConsumerFactory(void) {}
IConsumer* createFromProduct(IProduct* product) {
ProductTypeVisitor visitor;
product->accept(&visitor);
ProductTypeVisitor::ProductType productType = visitor.getProductType();
IConsumer* consumerPtr;
switch (productType) {
case ProductTypeVisitor::A :
consumerPtr = new ConsumerA(dynamic_cast<ProductA*>(product));
break;
case ProductTypeVisitor::B :
consumerPtr = new ConsumerB(dynamic_cast<ProductB*>(product));
break;
default:
std::cout << "Product type undefined. (throw exception)" << std::endl;
break;
}
return consumerPtr;
}
private:
ProductTypeVisitor visitor;
};
Finally, the code will look like this:
IProducer* producer = new ProducerA;
IProduct* product = producer->produce();
ConsumerFactory factory;
IConsumer* consumer = factory.createFromProduct(product);
consumer->consume();
Where the only thing that was ever specified, was ProducerA
. Which, in my case, is the only thing that should be specified by the user. Also, I've isolated change areas to just two classes, the ConsumerFactory
, and the IProductVisitor
(which are very small changes to begin with).
If anyone could offer improvements or suggestions I'm all ears!
回答3:
This is not the full solution (and maybe just a curiosity) but you can always do the tracking of types at compile time and use a bridging templated call to dispatch a product to the correct consumer.
#include <iostream>
template <class T>
class IProduct {
public:
virtual ~IProduct() {}
int intData;
typedef T consumer;
};
class ConsumerA;
class ProductA : public IProduct<ConsumerA> {
public:
float floatData;
};
class ConsumerB;
class ProductB : public IProduct<ConsumerB> {
public:
bool boolData;
};
template <class P, class C>
void apply(P* product, C* consumer) {
dynamic_cast<typename P::consumer*>(consumer)->consume(product);
}
template <class T>
class IConsumer {
public:
virtual void consume(IProduct<T>* prod) = 0;
};
class ConsumerA : public IConsumer<ConsumerA> {
public:
void consume(IProduct<ConsumerA>* prod) {
//I want to access the float!
std::cout << "ConsumerA" << std::endl;
std::cout << dynamic_cast<ProductA*>(prod)->floatData << std::endl;
}
};
class ConsumerB : public IConsumer<ConsumerB> {
public:
void consume(IProduct<ConsumerB>* prod) {
//I want to access the bool!
std::cout << "ConsumerB" << std::endl;
std::cout << dynamic_cast<ProductB*>(prod)->boolData << std::endl;
}
};
int main(int argc, char* argv[]) {
auto p_a = new ProductA;
auto c_a = new ConsumerA;
apply(p_a, c_a);
auto p_b = new ProductB;
auto c_b = new ConsumerB;
apply(p_b, c_b);
return 0;
}
来源:https://stackoverflow.com/questions/21317403/how-to-have-a-single-interface-return-different-data-types