Design Pattern: uniform interface for partially supported derived classes
I am designing an interface for my project and curious if the idea can come true or not. Here is the situation, At run time, I want to use an array of base class pointers to issue commands to different derived objects. Different derived objects got different implementation (virtual functions). My problem is: if those objects got different different support level of the interface, how can I avoid writing an empty function?
For example, (my current code)
Class Base { //this is the main interface
public:
virtual void basicFun() = 0;
virtual void funA(int input1) {return;}
virtual void funB(int input2) {return;}
virtual void funC(int input3) {return;}
}
Class Derived1 : public Base { //this class support only funA()
public:
void basicFun() {//....}
void funA(int input1) {//do something}
}
Class Derived2 : public Base { //this class support both funA() funB()
public:
void basicFun() {//....}
void funA(int input1) {//do something}
void funB(int input2) {//do something}
}
Class Derived3 : public Base { //this class support all
public:
void basicFun() {//....}
void funA(int input1) {//do something}
void funB(int input2) {//do something}
void funC(int input3) {//do something}
}
Assumption: for a certain object, unsupported function would never be called. i.e. BasePtr->funC() will never be called if the object pointed by basePtr is Derived1 or Derived2. The problem is:
- I must define an empty function either in Base or Derived if an uniform interface is desired
- If empty functions are defined like above, compiler keeps warning me unreferenced parameters (input1~input3). Of course I can turn it off, but just don't like this way.
So, is there any pattern that I can use to achieve an uniform interface without defining empty functions? I have been thinking about this fo开发者_如何学Gor a few days. It seems impossible. Because funA() funB() and funC() must be in the interface so that I can use a Base pointer array to control all objects, which means in Derived1, funB() and funC() must somehow be defined.
Thanks, and happy thanksgiving, and thanks for sharing your ideas.
Solti
Uniform interfaces are a good thing. Which means that you must implement all methods in an interface, even if it means you will have empty methods. There is no design pattern for this problem, because it's not a real problem in the first place.
Think about this for a moment: say you have a Car
interface, with methods Accelerate()
and Brake()
. A class that derives from Car
must implement all methods. Would you want an object derived from Car
implement the Accelerate()
method but not the Brake()
method? It would be an amazingly unsafe Car
!
An interface in the context of OOP must have a well defined contract that is adhered to by both sides. In C++, this is enforced to a certain extent by requiring all pure virtuals to be implemented in derived class(es). Trying to instantiate a class with unimplemented virtual methods result in compilation error(s), assuming one doesn't use stupid tricks to get around it.
You object to creating empty methods because they cause compiler warnings. In your case, just omit the parameter name:
void funC(int) // Note lack of parameter name
{
}
Or comment the name out:
void funC(int /*input3*/)
{
}
Or even by using templates!
template<class T> void ignore( const T& ) { }
//...
void funC(int input3)
{
ignore(input3);
}
Here's what I would have done: create a helper base class with empty default implementations of all pure virtual methods. Then you can derive from this base class instead of the main interface and then select which method to override.
// main interface, everything pure virtual
struct IBase
{
virtual ~IBase() {}
virtual void basicFun() = 0;
virtual void funA(int input1) = 0;
virtual void funB(int input2) = 0;
virtual void funC(int input3) = 0;
};
// helper base class with default implementations (empty)
class Base : public IBase
{
void basicFun() {}
void funA(int input1) {}
void funB(int input2) {}
void funC(int input3) {}
};
class Derived1 : public Base { //this class support only funA()
void funA(int input1) {//do something}
};
class Derived2 : public Base { //this class support both funA() funB()
void funA(int input1) {//do something}
void funB(int input2) {//do something}
};
class Derived3 : public IBase { //this class support all
void basicFun() {//....}
void funA(int input1) {//do something}
void funB(int input2) {//do something}
void funC(int input3) {//do something}
};
int main()
{
// I always program to the interface
IBase& b1 = Derived1(); b1.basicFun(); b1.funA(); b1.funB(); b1.funC();
IBase& b2 = Derived2(); b2.basicFun(); b2.funA(); b2.funB(); b2.funC();
IBase& b3 = Derived3(); b3.basicFun(); b3.funA(); b3.funB(); b3.funC();
}
If you truly need non uniform interfaces (think about it before), maybe the Visitor pattern is worth a try:
struct Visitor;
struct Base
{
virtual ~Base() {}
virtual void accept(Visitor& v) { v.visit(*this); }
};
struct InterfaceA : Base
{
void accept(Visitor& v) { v.visit(*this); }
virtual void MethodA() = 0;
};
struct InterfaceB : Base
{
void accept(Visitor& v) { v.visit(*this); }
virtual void MethodB() = 0;
};
struct InterfaceA2 : InterfaceA
{
void accept(Visitor& v) { v.visit(*this); }
void MethodA(); // Override, eg. in terms of MethodC
virtual void MethodC() = 0;
};
// Provide sensible default behavior. Note that the visitor class must be
// aware of the whole hierarchy of interfaces
struct Visitor
{
virtual ~Visitor() {}
virtual void visit(Base& b) { throw "not implemented"; }
virtual void visit(InterfaceA& x) { this->visit(static_cast<Base&>(x)); }
virtual void visit(InterfaceA2& x) { this->visit(static_cast<InterfaceA&>(x)); }
virtual void visit(InterfaceB& x) { this->visit(static_cast<Base&>(x)); }
};
// Concrete visitor: you don't have to override all the functions. The unimplemented
// ones will default to what you expect.
struct MyAction : Visitor
{
void visit(InterfaceA& x)
{
x.MethodA();
}
void visit(InterfaceB& x)
{
x.methodB();
}
};
usage:
Base* x = getSomeConcreteObject();
MyAction f;
x->accept(f);
this will invoke either MethodA
or MethodB
depending on the runtime type of x
. The visitor as implemented allows you not to overload many functions, and falls back to the action for a base class if the behavior is not implemented. Eventually, if you fail to provide an action in a visitor for some class, it will default to throw "not implemented"
.
Const correctness may force you to distinguish between Visitor
and ConstVisitor
, for whom all the accept
methods would be const.
精彩评论