Skip to content

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 Shape interface with area, a bounds query, and a clone().
  • [ ] At least three concrete shapes, including one with two parameters.
  • [ ] A Document owning shapes with std::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 group command treating several shapes as one, with an area that is the sum.
  • A bounding rectangle for the whole document.
  • A script mode 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

  • [ ] add returns a fresh, non-reused identifier after a remove
  • [ ] list shows every shape, and each shape's reported area matches a hand calculation
  • [ ] area equals the sum of the listed areas, including for an empty document (0)
  • [ ] A negative dimension in add or resize is refused and changes nothing
  • [ ] move on an unknown id is refused with the id in the message
  • [ ] A failed command followed by undo reverts the previous successful command, not nothing
  • [ ] undo with 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 → list reproduces 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