Disclaimer: the minimum meaningful example I could think of ended up being quite big. For simplicity I will skip all the const
overloads of any method as well as any virtual
destructor and anything else not directly code-wise related.
I have five classes, arranged in a hierarchy as the following one:
struct A { };
struct B : A { };
struct C : A { };
struct D : C { };
struct E : C { };
All the instanced will be seen by user code at the maximum abstraction level possible, so they all will be seen as A&
. User code needs to do two type of operations:
- print
"I am C"
if the instance is of typeC
- print
"I am C"
if the instance is of typeC
or any of its subtypes (namelyD
orE
)
The idea was to use visitor design pattern to implement both the operations. For this class hierarchy, the base visitor (which will do nothing by default) looks something like this:
struct Visitor
{
virtual void visit( A& ) { }
virtual void visit( B& ) { }
virtual void visit( C& ) { }
virtual void visit( D& ) { }
virtual void visit( E& ) { }
};
All the classes in the hierarchy will be able to accept it and call visit on themselves. For example, the base class A
will have:
struct A
{
virtual void accept( Visitor& v )
{
v.visit( *this );
}
};
And all its subtypes (in the example is B
, but it is same code for C
, D
and E
as well) will have the specific overridden version of it:
struct B : A
{
void accept( Visitor& v ) override
{
v.visit( *this );
}
};
Operation 1 has a quite easy implementation, and almost works out of the visitor design pattern box with the infrastructure put in place:
struct OperationOne : Visitor
{
void visit( C& ) override { cout << "I am C" << endl; }
}
And as expected the string `"I am C" will be printed only once:
int main( )
{
A a; B b; C c; D d; E e;
vector< reference_wrapper< A > > vec = { a, b, c, d, e };
OperationOne operation_one;
for ( A& element : vec )
{
element.accept( operation_one );
}
}
The problem is: for the second operation, the whole infrastructure does not work anymore, assuming that we do not want to repeat the print code for D
and E
as well:
struct OperationTwo : Visitor
{
void visit( C& ) override { cout << "I am C" << endl; }
void visit( D& ) override { cout << "I am C" << endl; }
void visit( E& ) override { cout << "I am C" << endl; }
}
As much as this would work, if the hierarchy changes and D
is no longer a subtype of C
, but for example a direct subtype of A
, this code would still compile but would have not the expected behaviour at runtime, which is dangerous and undesirable.
One solution in order to implement operation 2 is to change the visitor infrastructure so that every visitable class would propagate the accepted visitor to its base classes:
struct B : A
{
void accept( Visitor& v ) override
{
A::accept( v );
v.visit( *this );
}
};
In this way, if the hierarchy changes we will have the compilation error, since the base class would not be found anymore by the compiler when trying to propagate the accepted visitor.
That said, we can now write the second operation visitor, and this time we do not need to duplicate the printing code for D
and E
:
struct OperationTwo : Visitor
{
void visit( C& ) override { cout << "I am C" << endl; }
}
As expected the string "I am C" will be printed three times in the user code when
OperationTwo` is used:
int main()
{
A a; B b; C c; D d; E e;
vector< reference_wrapper< A > > vec = { a, b, c, d, e };
OperationTwo operation_two;
for ( A& element : vec )
{
element.accept( operation_two );
}
}
But wait: OperationOne
and OperationTwo
code is exactly the same! This means that by changing the infrastructure for the second operation, we basically broke the first one. In fact, now also OperationOne
will print three times the string "I am C"
.
What can be done in order to have OperationOne
and OperationTwo
work seemlinglessly together? Do I need to combine the visitor design pattern with another design patter or do I need to not use visitor at all?
Aucun commentaire:
Enregistrer un commentaire