- Developing High-Quality Software Efficiently with AI Tools
The shape-printer project showcases modern C++ design patterns and the power of AI-assisted development. By leveraging tools like GitHub Copilot, this project demonstrates how developers can:
- Rapidly prototype and implement extensible software architectures
- Focus on design decisions rather than boilerplate code
- Create more maintainable and testable code through proper separation of concerns
- Develop complex software systems with reduced development time and effort
This collaboration between human design expertise and AI assistance results in higher quality software that follows best practices while minimizing development overhead.
Shape Printer is a versatile C++ library that renders geometric shapes through various output methods. The project follows a modular, plugin-based architecture where:
- Shapes are defined by their mathematical properties or pixel data
- Output methods are interchangeable and independent of shape definitions
- The core printing logic remains unchanged when adding new shapes or output methods
This design philosophy embodies the Open/Closed Principle: the system is open for extension but closed for modification. When you add a new shape, existing output code continues to work without changes. Similarly, adding a new output method doesn't require modifying any shape definitions.
This project serves as a practical demonstration of how AI tools can revolutionize software development:
-
Accelerated Development: AI assistants like GitHub Copilot can generate boilerplate code, suggest implementations, and help with repetitive tasks, allowing developers to focus on higher-level design decisions.
-
Knowledge Augmentation: AI tools can suggest patterns, idioms, and best practices, serving as an always-available pair programming partner with broad knowledge of coding standards.
-
Reduced Cognitive Load: By handling routine coding tasks, AI tools free up mental bandwidth for solving complex problems and architectural considerations.
-
Learning Acceleration: Developers can learn new patterns and techniques by examining AI-generated suggestions, accelerating the learning curve for junior developers.
Shape Printer demonstrates excellent separation of concerns through its modular architecture:
-
Shape Definition: The system cleanly separates shape definition (what's inside vs. outside) from rendering logic.
Shapeinterface defines a contract for determining points inside shapesInsideShapeprovides a function-like interface for the same purpose- Both approaches allow treating shapes as black boxes with a simple inside/outside query
-
Output Handling: Output formatting and destination are completely decoupled from shape generation.
Outputinterface creates a boundary between shape data and presentation- Different output implementations can render the same data in various formats
-
Rendering Logic: The core
PrintShapeclass serves as a bridge between shapes and outputs.- Transforms shape definitions into concrete 2D boolean arrays
- Delegates to the output handler without knowing specifics of rendering
This separation ensures that changes in one area won't affect others, making the codebase more maintainable and resilient to change.
The system supports multiple approaches to define shapes, offering flexibility for different use cases:
-
Lambda Functions: For quick, one-off shape definitions
PrintShape printShape([](int x, int y, int n) { return x*x + y*y < n*n; }, output);
-
Free Functions: For reusable but simple shapes
bool insideHexagon(int x, int y, int n) { /* logic */ } PrintShape printShape(insideHexagon, output);
-
Functors: For shapes that may need internal state
class InsideStarShape : public InsideShape { /* implementation */ }; PrintShape printShape(InsideStarShape(), output);
-
Shape Classes: For complex shapes with additional methods
class RegularPolygon : public Shape { /* implementation */ }; PrintShape printShape(RegularPolygon(6), output); // hexagon
-
Image-Based Shapes: For irregular or bitmap-defined shapes
Image customShape(/* bitmap data */); PrintShape printShape(customShape, output);
Adding a new shape is as simple as implementing one of these approaches with your shape's logic, requiring no changes to the existing system.
The output system is designed for easy extension through the Output interface:
-
Console Output: Already implemented via
StreamOutStreamOut consoleOut(std::cout); printShape.setOutput(consoleOut); -
String Output: Using
StreamOutwith a string streamstd::ostringstream oss; StreamOut stringOut(oss); printShape.setOutput(stringOut); -
File Output: Using
StreamOutwith a file streamstd::ofstream file("shape.txt"); StreamOut fileOut(file); printShape.setOutput(fileOut);
-
Image Output: Using
BMPCreatorfor bitmap imagesBMPCreator bmpOut("shape.bmp"); printShape.setOutput(bmpOut);
-
Custom Output Formats: Creating new implementations of
Outputclass SVGOutput : public Output { /* implementation */ }; SVGOutput svgOut("shape.svg"); printShape.setOutput(svgOut);
This design makes it trivial to add support for new output formats (like HTML, SVG, or JSON) by simply creating a new class that implements the Output interface.
The modular design facilitates comprehensive testing of individual components:
-
Shape Testing: Each shape can be tested in isolation
TEST(DiamondTest, InsideDiamondFunction) { EXPECT_TRUE(insideDiamond(0, 0, 2)); EXPECT_FALSE(insideDiamond(2, 0, 2)); }
-
Output Testing: Output mechanisms can be verified independently
TEST(StreamOutTest, CustomCharacters) { std::ostringstream oss; StreamOut streamOut(oss, "O", "."); // Test with a known pattern }
-
Integration Testing: The
PrintShapeclass can be tested with mock objectsTEST(PrintShapeTest, LambdaFunction) { // Mock output captures the result std::vector<std::vector<bool>> result; auto output = [&result](const auto& image) { result = image; }; PrintShape printShape(insideDiamond, output); printShape(2); // Verify the result }
-
Dependency Injection: The system's use of interfaces enables easy mocking
class MockShape : public Shape { MOCK_METHOD(bool, inside, (int, int, int), (const, override)); }; class MockOutput : public Output { MOCK_METHOD(void, operator(), (const std::vector<std::vector<bool>>&), (const, override)); };
This approach allows for comprehensive test coverage, making it easier to maintain quality as the codebase evolves.
classDiagram
namespace shape_printer {
class PrintShape {
+operator()(size)
+setInsideShape(shape)
+setOutput(output)
}
class Shape {
<<interface>>
+inside(x, y, n)
}
class InsideShape {
<<interface>>
+operator()(x, y, n)
}
class Output {
<<interface>>
+operator()(image)
}
}
namespace shape_printer.output_extension {
class StreamOut {
+setOutput(output)
+setCharacters(inChar, outChar, eolChar)
+operator()(image)
}
class BMPCreator {
+operator()(image)
+setColors(red, green, blue, alpha)
}
}
namespace shape_printer.shape_extension {
class Diamond {
+inside(x, y, n)
}
class InsideDiamond {
+operator()(x, y, n)
}
class Image {
+inside(x, y, n)
+getSize()
}
class BMP {
+BMP(filename)
}
}
Output <|.. StreamOut
Output <|.. BMPCreator
Shape <|.. Diamond
InsideShape <|.. InsideDiamond
Shape <|.. Image
Image <|-- BMP
- CMake 3.10+
- C++17 compatible compiler
mkdir build
cd build
cmake ..
makecd build
ctestThis project demonstrates that the most effective approach is not to rely solely on AI tools, but to establish a productive collaboration:
- Human: Design high-level architecture and interfaces
- AI: Generate implementation details and boilerplate
- Human: Review, refine, and ensure quality
- AI: Assist with tests and documentation
- Human: Make final design decisions and optimizations
Through this collaboration, we achieve both development efficiency and high-quality software design.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.