Skip to content

Commit a1197f4

Browse files
committed
Add RealWorld example of Visitor
1 parent fe790fb commit a1197f4

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

src/Visitor/RealWorld/Output.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"menu":[{"item":"food","name":"Borscht","calories":"160kcal","label":"meat"},{"item":"food","name":"Samosa","calories":"250kcal","label":"vegetarian"},{"item":"food","name":"Sushi","calories":"300kcal","label":"fish"},{"item":"food","name":"Quinoa","calories":"350kcal","label":"vegan"},{"item":"drink","name":"Vodka","volume":"25ml","label":"alcholic"},{"item":"drink","name":"Chai","volume":"120ml","label":"hot"},{"item":"drink","name":"Sake","volume":"180ml","label":"alcholic"},{"item":"drink","name":"Kola","volume":"355ml","label":"cold"}]}

src/Visitor/RealWorld/main.cc

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/**
2+
* EN: Real World Example of the Visitor Design Pattern (Modern C++17 Standard)
3+
*
4+
* Need: Consider a restaurant \c Menu represented as a heterogeneous \c Item
5+
* collection of different \c Food and \c Drink items, which must be
6+
* (homogeneously) serialised into RFC 8259 JSON for some external API usage.
7+
*
8+
* Solution: A modern C++17 standard \c Serialiser Visitor be easily implemented
9+
* using the built-in utilities found within the \c <variant> header, namely,
10+
* the type-safe union \c std::variant to represent different menu items and the
11+
* functor \c std::visit to apply a callable \c Serialiser visitor.
12+
*
13+
* This simpler ("KISS") and boilerplate-free implementation of the Visitor
14+
* Design Pattern surpasses the classical object-oriented programming Visitor
15+
* that often requires maintaining two separate, but cyclically interdependent,
16+
* class hierarchies:
17+
*
18+
* \c Item<-Food/Drink and \c Visitor<-FoodVisitor/DrinkVisitor
19+
*
20+
* and suffers from performance penalties associated with the virtual function
21+
* calls in the double dispatch.
22+
*
23+
* In this contemporary take on the Visitor Design Pattern here, the (SOLID)
24+
* Open-Closed Principle is more expressively fulfilled because the \c Food and
25+
* \c Drink classes do not need to be derived from some base \c Item class and
26+
* also do not need to be updated with \c AcceptVisitor methods. The absence of
27+
* any intrusive polymorphism provides greater flexibility; this means that new
28+
* types (i.e. new \c Item types such as \c Snack ) and new visitors (i.e. ) are
29+
* more straightforward to incorporate.
30+
*
31+
* For such a \e procedural Visitor Design Pattern, performances gains can be
32+
* expected if the \c std::variant uses value semantics rather than reference
33+
* semantics, and if the collection storage is continguous, that is, instead of
34+
* the memory-scattering pointer indirections of the traditional Visitor Design
35+
* Pattern applied to multiple types.
36+
*/
37+
38+
#include <iostream>
39+
#include <string>
40+
#include <variant>
41+
#include <vector>
42+
43+
/**
44+
* EN: Stable Low-Lying Data Structures for Food, Drink,...
45+
*
46+
* Respecting the Open-Closed Principle, there is no need to modify these
47+
* classes to accept the visitors that are to be introduced later. Observe that
48+
* these \c Item classes are not part of an inheritance hierarchy and so there
49+
* is flexibility to create more such \c Item classes.
50+
*
51+
* However, note that these classes require a complete definition here in lieu
52+
* of their upcoming role within the \c std::variant union, that is, a forward
53+
* declaration of these classes is not sufficient. The public API consists of an
54+
* explicit constructor and the necessary access methods that are required by
55+
* the \c Serialiser Visitor.
56+
*/
57+
class Food {
58+
public:
59+
enum Label : unsigned { meat, fish, vegetarian, vegan };
60+
61+
public:
62+
explicit Food(std::string name, std::size_t calories, Label label)
63+
: name_{name}, calories_{calories}, label_{label} {}
64+
65+
auto name() const noexcept { return name_; }
66+
auto calories() const noexcept { return calories_; }
67+
auto label() const noexcept {
68+
switch (label_) {
69+
case Label::meat:
70+
return "meat";
71+
case Label::fish:
72+
return "fish";
73+
case Label::vegetarian:
74+
return "vegetarian";
75+
case Label::vegan:
76+
return "vegan";
77+
default:
78+
return "unknown";
79+
}
80+
}
81+
82+
private:
83+
std::string name_;
84+
std::size_t calories_;
85+
Label label_;
86+
};
87+
88+
class Drink {
89+
public:
90+
enum Label : unsigned { alcoholic, hot, cold };
91+
92+
public:
93+
explicit Drink(std::string name, std::size_t volume, Label label)
94+
: name_{name}, volume_{volume}, label_{label} {}
95+
96+
auto name() const noexcept { return name_; }
97+
auto volume() const noexcept { return volume_; }
98+
auto label() const noexcept {
99+
switch (label_) {
100+
case Label::alcoholic:
101+
return "alcholic";
102+
case Label::hot:
103+
return "hot";
104+
case Label::cold:
105+
return "cold";
106+
default:
107+
return "unknown";
108+
}
109+
}
110+
111+
private:
112+
std::string name_;
113+
std::size_t volume_;
114+
Label label_;
115+
};
116+
117+
/* ... */
118+
119+
/**
120+
* EN: Variant Union of the Item and Menu as an Item Collection
121+
*
122+
* The \c Item and \c Menu aliases carve out an architectural boundary
123+
* separating the low-lying data structures (above) and the client-facing
124+
* visitor (below), the former being more established in the codebase and the
125+
* latter being perhaps newer and often more changeable. Also note the value
126+
* semantics, which means there is no need for manual dynamic memory allocation
127+
* or management (e.g. via smart pointers) and hence lower overall complexity
128+
* when it comes to implementing the Visitor Design Pattern.
129+
*
130+
* For best performance, it is recommended to use \c Item types of similar, if
131+
* not identical, sizes so that the memory layout can be optimised. (If there
132+
* are considerable differences in the class sizes, then it may be sensible to
133+
* use the Proxy Design Pattern to wrap around larger-sized classes or even the
134+
* Bridge Design Pattern/"pimpl" idiom.) The memory layout of the members within
135+
* each of the \c Item classes themselves may also be of importance to overall
136+
* performance (c.f. padding) in this implementation as the \c std::visit method
137+
* will be applied to each \c Item element by iterating over the \c Menu
138+
* container.
139+
*/
140+
using Item = std::variant<Food, Drink /* ... */>;
141+
using Menu = std::vector<Item>;
142+
143+
/**
144+
* EN: Serialiser Visitor Functor
145+
*
146+
* This basic \c Serialiser class has non-canonical operator() overloads
147+
* which take the different \c Item types as input arguments, define lambdas to
148+
* perform a rudimentary conversion of the data to compressed/minified JSON
149+
* using the public API of the classes, and then print out the converted result
150+
* to some \c std::ostream by invoking the lambdas. Each \c Item has its own
151+
* unique overloaded operator() definition, which makes this class a prime
152+
* candidate for the Strategy Design Pattern e.g. different JSON specifications.
153+
*/
154+
class Serialiser {
155+
public:
156+
explicit Serialiser(std::ostream &os = std::cout) : os_{os} {}
157+
158+
public:
159+
auto operator()(Food const &food) const {
160+
161+
auto to_json = [&](auto f) {
162+
return R"({"item":"food","name":")" + f.name() + R"(","calories":")" +
163+
std::to_string(f.calories()) + R"(kcal","label":")" + f.label() +
164+
R"("})";
165+
};
166+
167+
os_ << to_json(food);
168+
}
169+
auto operator()(Drink const &drink) const {
170+
auto to_json = [&](auto d) {
171+
return R"({"item":"drink","name":")" + d.name() + R"(","volume":")" +
172+
std::to_string(d.volume()) + R"(ml","label":")" + d.label() +
173+
R"("})";
174+
};
175+
os_ << to_json(drink);
176+
}
177+
/* ... */
178+
179+
private:
180+
std::ostream &os_{std::cout};
181+
};
182+
183+
/* ... */
184+
185+
/**
186+
* EN: Applied Visitor for Menu (Item Collection) Serialisation
187+
*
188+
* The callable/invokable \c Serialiser Visitor can now be applied to each of
189+
* the \c Item elements in the \c Menu via the \c std::visit utility method, the
190+
* internal machinery of which could somewhat vary between different compilers
191+
* (e.g. GCC, Clang, MSVC, etc.) and their versions. Nevertheless, as a staple
192+
* part of the standard library from C++17 onwards, \c std::visit reliably and
193+
* conveniently automates the required boilerplate code and thereby reduces the
194+
* implementational friction that accompanies the traditional object-oriented
195+
* Visitor Design Pattern.
196+
*
197+
* Accordingly, it is now possible to perform a simple range-based for loop over
198+
* the \c Menu collection and apply visitor on each \c Item element in turn,
199+
* which has the best possible performance if the \c Item elements are stored
200+
* contiguously as values in memory.
201+
*/
202+
void serialise(Menu const &menu, std::ostream &os = std::cout) {
203+
bool first{true};
204+
os << R"({"menu":[)";
205+
for (auto const &item : menu) {
206+
if (!first)
207+
os << ",";
208+
else
209+
first = false;
210+
std::visit(Serialiser{os}, item);
211+
}
212+
os << R"(]})";
213+
}
214+
215+
/* ... */
216+
217+
/**
218+
* EN: Client Code: Variant Visitor
219+
*
220+
* The declaration of the \c Menu collection is clean and hassle-free, and the
221+
* addition of the \c Item elements in form of \c Food and \c Drink class
222+
* instances is also drastically simplified by the value semantics. Finally, the
223+
* neat \c serialise method can be called with the \c Menu input argument to
224+
* demonstrate Modern C++17 Visitor Design Pattern in action.
225+
*/
226+
int main() {
227+
228+
Menu menu;
229+
menu.reserve(8);
230+
231+
menu.emplace_back(Food{"Borscht", 160, Food::Label::meat});
232+
menu.emplace_back(Food{"Samosa", 250, Food::Label::vegetarian});
233+
menu.emplace_back(Food{"Sushi", 300, Food::Label::fish});
234+
menu.emplace_back(Food{"Quinoa", 350, Food::Label::vegan});
235+
menu.emplace_back(Drink{"Vodka", 25, Drink::Label::alcoholic});
236+
menu.emplace_back(Drink{"Chai", 120, Drink::Label::hot});
237+
menu.emplace_back(Drink{"Sake", 180, Drink::Label::alcoholic});
238+
menu.emplace_back(Drink{"Kola", 355, Drink::Label::cold});
239+
/* ... */
240+
241+
serialise(menu);
242+
}

0 commit comments

Comments
 (0)