Advanced Project 9 — Serialization Framework¶
Difficulty: Advanced · Estimated effort: 2–3 weeks · Prerequisites: Module 12 — Templates, Module 13 — Exception Handling, Module 15 — SOLID and Design Patterns · Demanded pattern decisions: visitor, factory
This is one of the designated integration projects, and one of the projects that requires a design-pattern decision: at least two patterns must be named and justified by a requirement, and one alternative must be considered and declined in writing.
No complete solution is provided. How a type declares its own serialisation is the central design question here, and it is deliberately unanswered.
1. Scenario¶
A department stores several kinds of record in text files and keeps writing the same save/load code by hand for each one, with three recurring bugs: the file format drifts between records, a field added to a struct is forgotten in both directions, and a corrupt file crashes the loader instead of reporting a line number.
You are asked for one small framework so a type says what it has, once, and gets saving, loading and validation from that statement.
2. Problem statement¶
Build a serialization framework in which a type describes its members and the framework derives the text format, a version marker, and robust loading. Round-trip fidelity must be a property you test, corrupt input must be reported with a line number, and adding a member must be a change in one place.
3. User requirements¶
- [ ] Save any registered type to a text file and load it back unchanged.
- [ ] Save and load collections of that type, including an empty one.
- [ ] Be told the line number and the field name for a corrupt or missing field.
- [ ] Add or reorder a field and get a clear, early failure when loading an older file.
- [ ] Use the framework with a nested type (a record containing another record).
- [ ] See the produced text file, in a format a human can read and hand-edit.
4. Functional requirements¶
- [ ] A description of a type's members, with names, kinds and ordering.
- [ ]
saveandloadfunctions driven by that description, in at least two of: a field-per-line format, a key=value format, a brace-delimited format. - [ ] Support for the fundamental types,
std::string, and at least one nested described type. - [ ] Collections: a described type that contains a
std::vectorof primitives or of nested types. - [ ] A version marker and a documented compatibility rule.
- [ ] Line-numbered errors distinguishing missing field, unexpected field and bad value.
- [ ] A creation point turning a format name into the right writer and reader — which is where the factory decision lives.
5. Non-functional requirements¶
- [ ] Round trip is exact: the reloaded object equals the original, for every registered type and including empty collections and strings containing the separator.
- [ ] A failed load throws with a line number and leaves the caller's object untouched.
- [ ] Adding one member to a described type requires editing exactly one function in that type's file — and you must demonstrate that with a real addition and a file count.
- [ ] No reflection tricks outside the standard library; the framework is portable C++17.
- [ ] Builds warning-free with
g++ -std=c++17 -Wall -Wextra -Werror.
6. Suggested features¶
- A second format behind the same description, chosen at run time.
- A
describe()output that prints the schema, useful as documentation and as a test fixture. - A migration hook: when the version is older, a named function upgrades the record.
- A length-prefixed container encoding, with a note on why variable-length records need one.
7. OOP concepts involved¶
| Concept | Required | Where it appears |
|---|---|---|
| Multiple classes | yes | Member descriptors, described types, writers, readers, factory, error types, examples |
| Relationships | yes | A type ──> its description; writer ──> an output stream; framework ◆ the format's components |
| Encapsulation | yes | Described types keep their invariants: loading constructs a valid object or fails |
| Inheritance or composition | yes | Described types have a description rather than deriving a base per format — and you must justify that against the alternative |
| Polymorphism | yes | A visitor over member descriptors so one algorithm handles every member kind |
| The STL | yes | std::vector for members, std::variant/std::function for member access, <sstream> for parsing |
| Exception handling | yes | Three distinct load errors with field names and line numbers |
8. Suggested design considerations¶
The pattern decision you must make and justify¶
Visitor, for walking a record's members. Saving and loading are two algorithms over the same
structure — a record's list of members — and each member is of a different kind. A visitor over
the member descriptors gives one algorithm per operation instead of one branch per kind per
operation. The alternative is polymorphic save/load methods on each member descriptor; argue
which you chose and what the trade-off was.
Factory, for format selection. The format is chosen by name, and a new format must not require editing the framework. The alternative is a small registry of functions. Pick one and decline the other in writing — that is the declined-pattern item.
Ideas worth weighing and recording: template method for the write/read skeleton (likely earned); memento for the "load fails, restore" path (avoid by construction instead, and say why); interpreter for a self-describing format (not required).
Other design points¶
The "one place" claim is the acceptance test. Add a member and count the files. If it takes two files, the description is not the single source of truth and the framework has failed its own purpose — which is a legitimate finding for your report, if you say so.
Design the failure path first. A corrupt file is not an edge case here; it is most of the requirement. Work out what each error message contains before writing the loader.
Round trip includes the awkward cases. An empty vector, a string containing the separator, and a zero value are the three cases that catch most encoding bugs. Put them in the test suite first, not last.
9. Minimum viable version¶
A member description for one record type, a field-per-line format, save and load for that one type, and round-trip equality for a hand-built instance.
10. Intermediate extension¶
Add nested and collection members, the version marker, and the three line-numbered error types. Then run the round trip over a set of awkward values: empty strings, empty collections, values at the maximum of their type, and a string containing the separator.
11. Advanced extension¶
Add the second format, the migration hook and the describe output. Then add a member to a described type and record exactly how many files had to change, against your prediction. Then write the design note: the patterns, the declined alternative, and the decision you would reverse.
12. Testing checklist¶
- [ ] Round trip of a simple record with defaults preserves every field
- [ ] Round trip of an empty collection, and of a collection with one element, both preserve size
- [ ] A string containing the separator round-trips through the documented escape
- [ ] A nested record round-trips, and its inner fields are validated with line numbers
- [ ] A missing field is reported as missing, naming the field and the line
- [ ] An unexpected extra field is reported, not ignored
- [ ] A field with a bad value names the field, the bad text and the conversion that failed
- [ ] A file with the wrong version is reported per the documented compatibility rule
- [ ] A failed load leaves the caller's object byte-for-byte unchanged
- [ ] Adding one member required editing exactly one function — the file count is recorded
- [ ] Both formats, loaded from the same record, produce equal objects (where lossless)
- [ ] Save → load → save produces identical files, for both formats
13. Documentation checklist¶
- [ ] How to register a type, as a short worked example with its output
- [ ] The formats, with a full file example and the escaping rule
- [ ] The compatibility and version rules
- [ ] The error catalogue: message shape for each of the three load errors
- [ ] The patterns used and declined, with the requirement each served
14. Self-evaluation rubric¶
| Criterion | Excellent looks like | Weight |
|---|---|---|
| Single source of truth | Adding a member is one edit; measured and reported | 25% |
| Round-trip fidelity | Awkward values verified, including empty containers and separators | 25% |
| Pattern decisions | Visitor and factory justified; one alternative declined with a requirement | 20% |
| Failure design | Three typed load errors with field names and line numbers; no partial state | 20% |
| Formats | A second format behind the same description, with the factory keeping the framework clean | 5% |
| Documentation | Registration example, format specification and error catalogue | 5% |
Related: Advanced projects · Project roadmap · All projects · Module 12 — Templates