Intermediate Project 3 — Shape Editor¶
Difficulty: Intermediate · Estimated effort: 12–16 hours · Prerequisites: Module 7 — Polymorphism, Module 8 — Abstract Classes, Module 11 — RAII and Smart Pointers, Module 14 — The STL with OOP · Combines: a polymorphic document, editing operations over owned objects, a text command interpreter, save/load
This is one of the designated integration projects. It must use, explicitly and in writing: multiple classes · relationships between classes · encapsulation · inheritance or composition · polymorphism · the STL · exception handling.
No complete solution is provided. The command set is given; the object model is not.
1. Scenario¶
A teaching lab wants to demonstrate an object-oriented editor: a drawing is a document containing shapes, and the operations are create, move, resize, delete, list and total area. Students type commands at a prompt and watch the document change.
The point of the exercise is the document, not the drawing: nothing is rendered, everything is inspected through queries.
2. Problem statement¶
Build a Document that owns a collection of shapes and supports editing commands by name, with
undo of the last operation. Adding a new shape kind must not require editing the command
handler, the listing, or the area total.
3. User requirements¶
- [ ] Type commands:
add circle r,add rect w h,move id dx dy,resize id factor,remove id,list,area,undo. - [ ] See each shape with a stable identifier, its kind and its current dimensions.
- [ ] Get the total area of the document at any time.
- [ ] Undo the most recent command that changed the document.
- [ ] Save the document and load it back unchanged.
4. Functional requirements¶
- [ ] An abstract
Shapeinterface with area, a bounds query, and aclone(). - [ ] At least three concrete shapes, including one with two parameters.
- [ ] A
Documentowning shapes withstd::unique_ptr. - [ ] Stable identifiers that are not reused within a session.
- [ ] A command parser returning a value object, not performing the operation itself.
- [ ] Undo of the last mutating command, with a documented depth (at least one).
- [ ] Explicit errors for unknown commands, unknown ids and impossible resizes.
5. Non-functional requirements¶
- [ ] No slicing: shapes are never copied by value through the interface.
- [ ]
clone()returns an owning pointer and is tested against every concrete shape. - [ ] The undo history does not leak and does not outlive the document.
- [ ] Every command is a single entry point; the parser never mutates the document.
- [ ] Builds warning-free with
g++ -std=c++17 -Wall -Wextra -Werror.
6. Suggested features¶
- Undo of the last n commands, as a bounded history.
- A
groupcommand treating several shapes as one, with an area that is the sum. - A bounding rectangle for the whole document.
- A
scriptmode reading commands from a file, with line numbers in error messages.
7. OOP concepts involved¶
| Concept | Required | Where it appears |
|---|---|---|
| Multiple classes | yes | Shape hierarchy, Document, command objects, parser, undo history, error types |
| Relationships | yes | Document ◆ shapes; undo history ◆ command records; parser ┈┈ uses document |
| Encapsulation | yes | Shape dimensions cannot go negative; identifiers cannot be reused |
| Inheritance or composition | yes | Interface inheritance for shapes; composition for document and history |
| Polymorphism | yes | area, describe, clone all dispatch; no type enum anywhere |
| The STL | yes | std::vector for shapes, std::map for lookup by id, std::find_if for searching |
| Exception handling | yes | Bad arguments and impossible operations throw a typed error the shell catches |
8. Suggested design considerations¶
Undo is the interesting design. Three approaches are defensible: a clone() snapshot of the
document (simple, expensive), a command record that stores the reverse operation (efficient,
harder), or a memento of the changed shape only. Choose one, say what it costs, and state the
depth limit.
clone() or copy-through-base? Copying through a base-class value slices; that is why
clone() exists. Implement it and say in the header why the compiler cannot generate it for you.
Keep the parser dumb. A parser returning a command value means the shell can be tested by feeding it strings, and the document can be tested without any parsing at all.
Identifiers are a resource. If removal frees an id, a stale script can address a different shape. Decide: never reuse, or reuse with a generation counter. Document it.
9. Minimum viable version¶
Shapes with area and describe, a document owning them, add, list, area, remove and
an error for an unknown id, driven from a while (std::getline(...)) loop.
10. Intermediate extension¶
Add move and resize with validation, then undo of the last mutating command, then verify
that an undo after a failed command does not undo the previous successful one — a subtle bug
that the checklist is designed to expose.
11. Advanced extension¶
Add a bounded undo history of the last five commands, plus save/load of the document, and make loading build its undo entries correctly (or explicitly disable undo across a load, with the reason stated). Then add a new shape and count the files that had to change.
12. Testing checklist¶
- [ ]
addreturns a fresh, non-reused identifier after aremove - [ ]
listshows every shape, and each shape's reported area matches a hand calculation - [ ]
areaequals the sum of the listed areas, including for an empty document (0) - [ ] A negative dimension in
addorresizeis refused and changes nothing - [ ]
moveon an unknown id is refused with the id in the message - [ ] A failed command followed by
undoreverts the previous successful command, not nothing - [ ]
undowith an empty history reports that there is nothing to undo - [ ]
clone()of each concrete shape yields an independent object (mutate one, check the other) - [ ] Save → load →
listreproduces the document exactly, ids included - [ ] Every shape is destroyed exactly once when the document is destroyed
13. Documentation checklist¶
- [ ] The command catalogue with argument syntax and every error message
- [ ] The ownership diagram and one responsibility sentence per class
- [ ] The undo approach, its cost, and its depth limit
- [ ] Identifier policy, including reuse
- [ ] The file format, with a round-trip example
14. Self-evaluation rubric¶
| Criterion | Excellent looks like | Weight |
|---|---|---|
| Polymorphic model | Adding a shape touches one file; no type switches anywhere | 25% |
| Undo design | Approach chosen with a stated cost; failed commands do not corrupt history | 20% |
| Ownership | unique_ptr throughout; clone() correct for every shape; no leaks |
20% |
| Interface | Parser separate from document; errors typed and specific | 15% |
| Persistence | Round trip preserves shapes and identifiers | 10% |
| Documentation | Command catalogue, undo cost and identifier policy written down | 10% |
Related: Intermediate projects · Project roadmap · All projects · Module 11 — RAII and Smart Pointers