summaryrefslogtreecommitdiffstats
path: root/examples/corelib
diff options
context:
space:
mode:
authorMarc Mutz <marc.mutz@qt.io>2023-02-07 13:52:50 +0100
committerMarc Mutz <marc.mutz@qt.io>2023-05-03 21:36:14 +0200
commit5a3ac484dbcc64c8ee7a57854fcdde6b4b067aaa (patch)
treeefafa56f817ac16dbf66ead2150854156d22042f /examples/corelib
parenta7d92f809f3d05a22c38ec6f77f9c62190d2deb0 (diff)
savegame ex.: revamp the way we (de)serialize JSON
JSON, unlike, say, QDataStream, allows building up objects independent of some central object, and combining them into a QJsonDocument later. This suggests returning QJsonObjects from a toJson() const method instead of having the caller supply a QJsonObject. Doing it this way enables transparent move semantics to kick in, too. For deserialization, use a fromJson() named constructor for value-like classes (where identity doesn't matter, only equality). Keep using read(), too, and add a note to explain when to use which form. Also, avoid the triple lookup from if (json.contains("key") && json["key"].isSoughtType()) mFoo = json["key"].toSoughtType(); by using C++17 if-with-initializer and showing the trick with Undefined never being of isSoughtType(): if (const QJsonValue v = json["key"]; v.isSoughtType()) mFoo = v.toSoughtType(); Adjust the discussion to match the new code, up the copyright years and rename some qdoc snippet markers from nondescript [0]/[1] to [toJson]/[fromJson]. Task-number: QTBUG-108857 Pick-to: 6.5 6.4 6.2 Change-Id: Icaa14acc7464fef00a59534679d710252e921383 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
Diffstat (limited to 'examples/corelib')
-rw-r--r--examples/corelib/serialization/savegame/character.cpp30
-rw-r--r--examples/corelib/serialization/savegame/character.h4
-rw-r--r--examples/corelib/serialization/savegame/doc/src/savegame.qdoc133
-rw-r--r--examples/corelib/serialization/savegame/game.cpp62
-rw-r--r--examples/corelib/serialization/savegame/game.h2
-rw-r--r--examples/corelib/serialization/savegame/level.cpp44
-rw-r--r--examples/corelib/serialization/savegame/level.h4
7 files changed, 166 insertions, 113 deletions
diff --git a/examples/corelib/serialization/savegame/character.cpp b/examples/corelib/serialization/savegame/character.cpp
index 7be737f308..910e7e9e7a 100644
--- a/examples/corelib/serialization/savegame/character.cpp
+++ b/examples/corelib/serialization/savegame/character.cpp
@@ -48,28 +48,34 @@ void Character::setClassType(Character::ClassType classType)
mClassType = classType;
}
-//! [0]
-void Character::read(const QJsonObject &json)
+//! [fromJson]
+Character Character::fromJson(const QJsonObject &json)
{
- if (json.contains("name") && json["name"].isString())
- mName = json["name"].toString();
+ Character result;
- if (json.contains("level") && json["level"].isDouble())
- mLevel = json["level"].toInt();
+ if (const QJsonValue v = json["name"]; v.isString())
+ result.mName = v.toString();
- if (json.contains("classType") && json["classType"].isDouble())
- mClassType = ClassType(json["classType"].toInt());
+ if (const QJsonValue v = json["level"]; v.isDouble())
+ result.mLevel = v.toInt();
+
+ if (const QJsonValue v = json["classType"]; v.isDouble())
+ result.mClassType = ClassType(v.toInt());
+
+ return result;
}
-//! [0]
+//! [fromJson]
-//! [1]
-void Character::write(QJsonObject &json) const
+//! [toJson]
+QJsonObject Character::toJson() const
{
+ QJsonObject json;
json["name"] = mName;
json["level"] = mLevel;
json["classType"] = mClassType;
+ return json;
}
-//! [1]
+//! [toJson]
void Character::print(int indentation) const
{
diff --git a/examples/corelib/serialization/savegame/character.h b/examples/corelib/serialization/savegame/character.h
index 4dc25139a6..d91bc8519b 100644
--- a/examples/corelib/serialization/savegame/character.h
+++ b/examples/corelib/serialization/savegame/character.h
@@ -31,8 +31,8 @@ public:
ClassType classType() const;
void setClassType(ClassType classType);
- void read(const QJsonObject &json);
- void write(QJsonObject &json) const;
+ static Character fromJson(const QJsonObject &json);
+ QJsonObject toJson() const;
void print(int indentation = 0) const;
private:
diff --git a/examples/corelib/serialization/savegame/doc/src/savegame.qdoc b/examples/corelib/serialization/savegame/doc/src/savegame.qdoc
index 5302582fcc..b1aa599e79 100644
--- a/examples/corelib/serialization/savegame/doc/src/savegame.qdoc
+++ b/examples/corelib/serialization/savegame/doc/src/savegame.qdoc
@@ -24,45 +24,81 @@
The Character class represents a non-player character (NPC) in our game, and
stores the player's name, level, and class type.
- It provides read() and write() functions to serialise its member variables.
+ It provides static fromJson() and non-static toJson() functions to
+ serialise itself.
+
+ \note This pattern (fromJson()/toJson()) works because QJsonObjects can be
+ constructed independent of an owning QJsonDocument, and because the data
+ types being (de)serialized here are value types, so can be copied. When
+ serializing to another format — for example XML or QDataStream, which require passing
+ a document-like object — or when the object identity is important (QObject
+ subclasses, for example), other patterns may be more suitable. See the
+ \l{xml/dombookmarks} and \l{xml/streambookmarks} examples for XML, and the
+ implementation of \l QListWidgetItem::read() and \l QListWidgetItem::write()
+ for idiomatic QDataStream serialization.
\snippet serialization/savegame/character.h 0
- Of particular interest to us are the read and write function
+ Of particular interest to us are the fromJson() and toJson() function
implementations:
- \snippet serialization/savegame/character.cpp 0
+ \snippet serialization/savegame/character.cpp fromJson
- In the read() function, we assign Character's members values from the
- QJsonObject argument. You can use either \l QJsonObject::operator[]() or
- QJsonObject::value() to access values within the JSON object; both are
- const functions and return QJsonValue::Undefined if the key is invalid. We
- check if the keys are valid before attempting to read them with
- QJsonObject::contains().
+ In the fromJson() function, we construct a local \c result Character object
+ and assign \c{result}'s members values from the QJsonObject argument. You
+ can use either \l QJsonObject::operator[]() or QJsonObject::value() to
+ access values within the JSON object; both are const functions and return
+ QJsonValue::Undefined if the key is invalid. In particular, the \c{is...}
+ functions (for example \l QJsonValue::isString(), \l
+ QJsonValue::isDouble()) return \c false for QJsonValue::Undefined, so we
+ can check for existence as well as the correct type in a single lookup.
- \snippet serialization/savegame/character.cpp 1
+ If a value does not exist in the JSON object, or has the wrong type, we
+ don't write to the corresponding \c result member, either, thereby
+ preserving any values the default constructor may have set. This means
+ default values are centrally defined in one location (the default
+ constructor) and need not be repeated in serialisation code
+ (\l{https://en.wikipedia.org/wiki/Don%27t_repeat_yourself}{DRY}).
- In the write() function, we do the reverse of the read() function; assign
- values from the Character object to the JSON object. As with accessing
- values, there are two ways to set values on a QJsonObject:
- \l QJsonObject::operator[]() and QJsonObject::insert(). Both will override
- any existing value at the given key.
+ Observe the use of
+ \l{https://en.cppreference.com/w/cpp/language/if#If_statements_with_initializer}
+ {C++17 if-with-initializer} to separate scoping and checking of the variable \c v.
+ This means we can keep the variable name short, because its scope is limited.
- Next up is the Level class:
+ Compare that to the naïve approach using \c QJsonObject::contains():
+
+ \badcode
+ if (json.contains("name") && json["name"].isString())
+ result.mName = json["name"].toString();
+ \endcode
+
+ which, beside being less readable, requires a total of three lookups (no,
+ the compiler will \e not optimize these into one), so is three times
+ slower and repeats \c{"name"} three times (violating the DRY principle).
+
+ \snippet serialization/savegame/character.cpp toJson
+
+ In the toJson() function, we do the reverse of the fromJson() function;
+ assign values from the Character object to a new JSON object we then
+ return. As with accessing values, there are two ways to set values on a
+ QJsonObject: \l QJsonObject::operator[]() and \l QJsonObject::insert().
+ Both will override any existing value at the given key.
+
+ \section1 The Level Class
\snippet serialization/savegame/level.h 0
- We want to have several levels in our game, each with several NPCs, so we
- keep a QList of Character objects. We also provide the familiar read() and
- write() functions.
+ We want the levels in our game to each each have several NPCs, so we keep a QList
+ of Character objects. We also provide the familiar fromJson() and toJson()
+ functions.
- \snippet serialization/savegame/level.cpp 0
+ \snippet serialization/savegame/level.cpp fromJson
- Containers can be written and read to and from JSON using QJsonArray. In our
+ Containers can be written to and read from JSON using QJsonArray. In our
case, we construct a QJsonArray from the value associated with the key
\c "npcs". Then, for each QJsonValue element in the array, we call
- toObject() to get the Character's JSON object. The Character object can then
- read their JSON and be appended to our NPC array.
+ toObject() to get the Character's JSON object. Character::fromJson() can
+ then turn that QJSonObject into a Character object to append to our NPC array.
\note \l{Container Classes}{Associate containers} can be written by storing
the key in each value object (if it's not already). With this approach, the
@@ -70,11 +106,13 @@
element is used as the key to construct the container when reading it back
in.
- \snippet serialization/savegame/level.cpp 1
+ \snippet serialization/savegame/level.cpp toJson
- Again, the write() function is similar to the read() function, except
+ Again, the toJson() function is similar to the fromJson() function, except
reversed.
+ \section1 The Game Class
+
Having established the Character and Level classes, we can move on to
the Game class:
@@ -86,26 +124,43 @@
Next, we provide accessors for the player and levels. We then expose three
functions: newGame(), saveGame() and loadGame().
- The read() and write() functions are used by saveGame() and loadGame().
+ The read() and toJson() functions are used by saveGame() and loadGame().
+
+ \div{class="admonition note"}\b{Note:}
+ Despite \c Game being a value class, we assume that the author wants a game to have
+ identity, much like your main window would have. We therefore don't use a
+ static fromJson() function, which would create a new object, but a read()
+ function we can call on existing objects. There's a 1:1 correspondence
+ between read() and fromJson(), in that one can be implemented in terms of
+ the other:
+
+ \code
+ void read(const QJsonObject &json) { *this = fromJson(json); }
+ static Game fromObject(const QJsonObject &json) { Game g; g.read(json); return g; }
+ \endcode
- \snippet serialization/savegame/game.cpp 0
+ We just use what's more convenient for callers of the functions.
+ \enddiv
+
+ \snippet serialization/savegame/game.cpp newGame
To setup a new game, we create the player and populate the levels and their
NPCs.
- \snippet serialization/savegame/game.cpp 1
+ \snippet serialization/savegame/game.cpp read
- The first thing we do in the read() function is tell the player to read
- itself. We then clear the level array so that calling loadGame() on the
- same Game object twice doesn't result in old levels hanging around.
+ The read() function starts by replacing the player with the
+ one read from JSON. We then clear() the level array so that calling
+ loadGame() on the same Game object twice doesn't result in old levels
+ hanging around.
We then populate the level array by reading each Level from a QJsonArray.
- \snippet serialization/savegame/game.cpp 2
+ \snippet serialization/savegame/game.cpp toJson
- We write the game to JSON similarly to how we write Level.
+ Writing the game to JSON is similar to writing a level.
- \snippet serialization/savegame/game.cpp 3
+ \snippet serialization/savegame/game.cpp loadGame
When loading a saved game in loadGame(), the first thing we do is open the
save file based on which format it was saved to; \c "save.json" for JSON,
@@ -119,14 +174,16 @@
After constructing the QJsonDocument, we instruct the Game object to read
itself and then return \c true to indicate success.
- \snippet serialization/savegame/game.cpp 4
+ \snippet serialization/savegame/game.cpp saveGame
Not surprisingly, saveGame() looks very much like loadGame(). We determine
the file extension based on the format, print a warning and return \c false
if the opening of the file fails. We then write the Game object to a
- QJsonDocument, and call either QJsonDocument::toJson() or to
- QJsonDocument::toBinaryData() to save the game, depending on which format
- was specified.
+ QJsonObject. To save the game in the format that was specified, we
+ convert the JSON object into either a QJsonDocument for a subsequent
+ QJsonDocument::toJson() call, or a QCborValue for QCborValue::toCbor().
+
+ \section1 Tying It All Together
We are now ready to enter main():
diff --git a/examples/corelib/serialization/savegame/game.cpp b/examples/corelib/serialization/savegame/game.cpp
index 21dedf9482..3140178f88 100644
--- a/examples/corelib/serialization/savegame/game.cpp
+++ b/examples/corelib/serialization/savegame/game.cpp
@@ -21,7 +21,7 @@ QList<Level> Game::levels() const
return mLevels;
}
-//! [0]
+//! [newGame]
void Game::newGame()
{
mPlayer = Character();
@@ -59,9 +59,9 @@ void Game::newGame()
dungeon.setNpcs(dungeonNpcs);
mLevels.append(dungeon);
}
-//! [0]
+//! [newGame]
-//! [3]
+//! [loadGame]
bool Game::loadGame(Game::SaveFormat saveFormat)
{
QFile loadFile(saveFormat == Json
@@ -87,9 +87,9 @@ bool Game::loadGame(Game::SaveFormat saveFormat)
<< (saveFormat != Json ? "CBOR" : "JSON") << "...\n";
return true;
}
-//! [3]
+//! [loadGame]
-//! [4]
+//! [saveGame]
bool Game::saveGame(Game::SaveFormat saveFormat) const
{
QFile saveFile(saveFormat == Json
@@ -101,52 +101,44 @@ bool Game::saveGame(Game::SaveFormat saveFormat) const
return false;
}
- QJsonObject gameObject;
- write(gameObject);
+ QJsonObject gameObject = toJson();
saveFile.write(saveFormat == Json
? QJsonDocument(gameObject).toJson()
: QCborValue::fromJsonValue(gameObject).toCbor());
return true;
}
-//! [4]
+//! [saveGame]
-//! [1]
+//! [read]
void Game::read(const QJsonObject &json)
{
- if (json.contains("player") && json["player"].isObject())
- mPlayer.read(json["player"].toObject());
+ if (const QJsonValue v = json["player"]; v.isObject())
+ mPlayer = Character::fromJson(v.toObject());
- if (json.contains("levels") && json["levels"].isArray()) {
- QJsonArray levelArray = json["levels"].toArray();
+ if (const QJsonValue v = json["levels"]; v.isArray()) {
+ const QJsonArray levels = v.toArray();
mLevels.clear();
- mLevels.reserve(levelArray.size());
- for (const QJsonValue &v : levelArray) {
- QJsonObject levelObject = v.toObject();
- Level level;
- level.read(levelObject);
- mLevels.append(level);
- }
+ mLevels.reserve(levels.size());
+ for (const QJsonValue &level : levels)
+ mLevels.append(Level::fromJson(level.toObject()));
}
}
-//! [1]
+//! [read]
-//! [2]
-void Game::write(QJsonObject &json) const
+//! [toJson]
+QJsonObject Game::toJson() const
{
- QJsonObject playerObject;
- mPlayer.write(playerObject);
- json["player"] = playerObject;
-
- QJsonArray levelArray;
- for (const Level &level : mLevels) {
- QJsonObject levelObject;
- level.write(levelObject);
- levelArray.append(levelObject);
- }
- json["levels"] = levelArray;
+ QJsonObject json;
+ json["player"] = mPlayer.toJson();
+
+ QJsonArray levels;
+ for (const Level &level : mLevels)
+ levels.append(level.toJson());
+ json["levels"] = levels;
+ return json;
}
-//! [2]
+//! [toJson]
void Game::print(int indentation) const
{
diff --git a/examples/corelib/serialization/savegame/game.h b/examples/corelib/serialization/savegame/game.h
index 0e91343d27..4c72ea426e 100644
--- a/examples/corelib/serialization/savegame/game.h
+++ b/examples/corelib/serialization/savegame/game.h
@@ -26,7 +26,7 @@ public:
bool saveGame(SaveFormat saveFormat) const;
void read(const QJsonObject &json);
- void write(QJsonObject &json) const;
+ QJsonObject toJson() const;
void print(int indentation = 0) const;
private:
diff --git a/examples/corelib/serialization/savegame/level.cpp b/examples/corelib/serialization/savegame/level.cpp
index c2f88c3434..5a6137715a 100644
--- a/examples/corelib/serialization/savegame/level.cpp
+++ b/examples/corelib/serialization/savegame/level.cpp
@@ -25,39 +25,37 @@ void Level::setNpcs(const QList<Character> &npcs)
mNpcs = npcs;
}
-//! [0]
-void Level::read(const QJsonObject &json)
+//! [fromJson]
+Level Level::fromJson(const QJsonObject &json)
{
- if (json.contains("name") && json["name"].isString())
- mName = json["name"].toString();
+ Level result;
- if (json.contains("npcs") && json["npcs"].isArray()) {
- QJsonArray npcArray = json["npcs"].toArray();
- mNpcs.clear();
- mNpcs.reserve(npcArray.size());
- for (const QJsonValue &v : npcArray) {
- QJsonObject npcObject = v.toObject();
- Character npc;
- npc.read(npcObject);
- mNpcs.append(npc);
- }
+ if (const QJsonValue v = json["name"]; v.isString())
+ result.mName = v.toString();
+
+ if (const QJsonValue v = json["npcs"]; v.isArray()) {
+ const QJsonArray npcs = v.toArray();
+ result.mNpcs.reserve(npcs.size());
+ for (const QJsonValue &npc : npcs)
+ result.mNpcs.append(Character::fromJson(npc.toObject()));
}
+
+ return result;
}
-//! [0]
+//! [fromJson]
-//! [1]
-void Level::write(QJsonObject &json) const
+//! [toJson]
+QJsonObject Level::toJson() const
{
+ QJsonObject json;
json["name"] = mName;
QJsonArray npcArray;
- for (const Character &npc : mNpcs) {
- QJsonObject npcObject;
- npc.write(npcObject);
- npcArray.append(npcObject);
- }
+ for (const Character &npc : mNpcs)
+ npcArray.append(npc.toJson());
json["npcs"] = npcArray;
+ return json;
}
-//! [1]
+//! [toJson]
void Level::print(int indentation) const
{
diff --git a/examples/corelib/serialization/savegame/level.h b/examples/corelib/serialization/savegame/level.h
index e09e2c9f3c..ce39a611b4 100644
--- a/examples/corelib/serialization/savegame/level.h
+++ b/examples/corelib/serialization/savegame/level.h
@@ -21,8 +21,8 @@ public:
QList<Character> npcs() const;
void setNpcs(const QList<Character> &npcs);
- void read(const QJsonObject &json);
- void write(QJsonObject &json) const;
+ static Level fromJson(const QJsonObject &json);
+ QJsonObject toJson() const;
void print(int indentation = 0) const;
private: