Design Patterns with Real-World Examples
Welcome to my growing collection of design patterns with practical examples! As I develop robotic applications, I’ve encountered various design challenges that can be elegantly solved using established patterns. This post documents these patterns with real-world implementations, primarily in C++ with occasional Python examples.
Factory Method Pattern
The Factory Method pattern is a creational design pattern that provides an interface for creating objects without specifying their exact class. By encapsulating object creation logic, it makes code more flexible, maintainable, and easier to extend.
Real-World Application: Input Source Manager
In a recent multiple object tracking (MOT) project, I needed to handle various input sources including video files, IP cameras, USB cameras, and streaming protocols. Instead of cluttering the main application logic with conditional creation code, I implemented a Factory Method pattern to manage these different input types cleanly.
Implementation Breakdown
Let’s walk through each component of this Factory Method implementation, starting with the foundational types and building up to the complete solution.
1. Type-Safe Input Classification
First, we define an enum class to represent all possible input types. Using enum class
instead of a regular enum provides better type safety and prevents implicit conversions:
1
2
3
4
5
6
7
8
9
enum class InputType {
VIDEO_FILE,
IP_CAMERA,
USB_CAMERA,
SDI_INPUT,
HDMI_INPUT,
RTSP_STREAM,
HTTP_STREAM
};
2. Configuration Structure
Next, we create a configuration structure that encapsulates all the parameters needed to initialize an input source:
1
2
3
4
5
6
struct InputSourceConfig {
InputType input_type;
std::string source;
bool enabled = true;
int priority = 0;
};
This structure allows us to pass all necessary configuration in a single, clean interface.
3. Abstract Base Interface
The heart of our Factory Method pattern is the abstract base class that defines the common interface for all input sources:
1
2
3
4
5
6
7
8
class BaseInputSource {
public:
virtual ~BaseInputSource() = default;
virtual bool Initialize(const InputSourceConfig& config) = 0;
virtual bool ReadFrame(cv::Mat& frame) = 0;
virtual bool IsAvailable() const = 0;
virtual std::string GetInfo() const = 0;
};
This interface ensures that all concrete input implementations provide the same functionality, enabling polymorphic usage throughout the application.
4. Concrete Implementations
Now we implement specific input source classes that inherit from our base interface. Each class handles the unique requirements of its input type:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class VideoFileInput : public BaseInputSource {
public:
bool Initialize(const InputSourceConfig& config) override {
// Implementation for video file initialization
// Open video file, validate format, set up codec
return true;
}
bool ReadFrame(cv::Mat& frame) override {
// Implementation for reading video frames
// Read next frame from video file
return true;
}
bool IsAvailable() const override {
// Check if video file exists and is readable
return true;
}
std::string GetInfo() const override {
return "Video File Input";
}
};
class IPCameraInput : public BaseInputSource {
public:
bool Initialize(const InputSourceConfig& config) override {
// Implementation for IP camera initialization
// Connect to camera, authenticate, configure stream
return true;
}
bool ReadFrame(cv::Mat& frame) override {
// Implementation for reading camera frames
// Capture frame from network stream
return true;
}
bool IsAvailable() const override {
// Check network connectivity and camera status
return true;
}
std::string GetInfo() const override {
return "IP Camera Input";
}
};
Each concrete class encapsulates the specific logic needed for its input type, while maintaining the same interface contract.
5. The Factory Implementation
The factory class is where the magic happens. It encapsulates all the object creation logic and provides convenient utility methods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class InputFactory {
public:
// Main factory method - creates appropriate input source based on configuration
static std::unique_ptr<BaseInputSource> CreateInput(const InputSourceConfig& config) {
switch (config.input_type) {
case InputType::VIDEO_FILE:
return std::make_unique<VideoFileInput>(); // C++14 preferred
// or (if you are using C++ 11_
// return std::unique_ptr<BaseInputSource>(new VideoFileInput());
case InputType::IP_CAMERA:
case InputType::RTSP_STREAM:
case InputType::HTTP_STREAM:
return std::make_unique<IPCameraInput>();
case InputType::USB_CAMERA:
return std::make_unique<USBCameraInput>();
default:
return nullptr; // Handle unknown types gracefully
}
}
// Utility method to automatically detect input type from source string
static InputType DetectInputType(const std::string& source) {
std::string lower_source = source;
std::transform(lower_source.begin(), lower_source.end(), lower_source.begin(), ::tolower);
// Check file extensions
if (lower_source.substr(lower_source.length() - 4) == ".mp4") return InputType::VIDEO_FILE;
if (lower_source.substr(lower_source.length() - 4) == ".avi") return InputType::VIDEO_FILE;
// Check network protocols
if (source.find("rtsp://") == 0) return InputType::RTSP_STREAM;
if (source.find("http://") == 0) return InputType::HTTP_STREAM;
// Check device paths
if (source.find("/dev/video") == 0) return InputType::USB_CAMERA;
return InputType::VIDEO_FILE; // Default fallback
}
// Helper method for debugging and logging
static std::string GetInputTypeName(InputType type) {
switch (type) {
case InputType::VIDEO_FILE: return "Video File";
case InputType::IP_CAMERA: return "IP Camera";
case InputType::USB_CAMERA: return "USB Camera";
case InputType::SDI_INPUT: return "SDI Input";
case InputType::HDMI_INPUT: return "HDMI Input";
case InputType::RTSP_STREAM: return "RTSP Stream";
case InputType::HTTP_STREAM: return "HTTP Stream";
default: return "Unknown";
}
}
};
The factory provides three key methods:
CreateInput()
: The main factory method that creates objects based on configurationDetectInputType()
: Automatically determines input type from source stringGetInputTypeName()
: Provides human-readable names for debugging
6. Putting It All Together: Usage Example
Here’s how you would use this Factory Method implementation in practice:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <memory>
#include <opencv2/opencv.hpp>
int main() {
// Auto-detect input type from source string
std::string source = "rtsp://192.168.1.100/stream";
InputType detected_type = InputFactory::DetectInputType(source);
// Create configuration object
InputSourceConfig config;
config.input_type = detected_type;
config.source = source;
config.enabled = true;
// Use factory to create appropriate input source
auto input_source = InputFactory::CreateInput(config);
if (input_source) {
std::cout << "Created: " << input_source->GetInfo() << std::endl;
if (input_source->Initialize(config)) {
std::cout << "Input source initialized successfully" << std::endl;
cv::Mat frame;
if (input_source->ReadFrame(frame)) {
std::cout << "Frame captured successfully" << std::endl;
// Process frame...
}
}
} else {
std::cout << "Failed to create input source" << std::endl;
}
return 0;
}
This example demonstrates the elegance of the Factory Method pattern: the client code doesn’t need to know about specific input types or their creation details. It simply provides configuration and receives a ready-to-use object.
Architecture Overview
Let’s summarize the key components that make this Factory Method implementation work:
enum class InputType
: Provides type-safe enumeration of all supported input types, preventing invalid configurationsInputSourceConfig
: Encapsulates configuration parameters in a clean, extensible structureBaseInputSource
: Defines the common interface that all input sources must implement- Concrete Classes:
VideoFileInput
,IPCameraInput
, etc. - each handles specific input type requirements InputFactory
: Centralizes object creation logic and provides utility methods for type detection- Smart Pointers:
std::unique_ptr
ensures automatic memory management and exception safety
The key is to use the factory interface CreateInput()
to create a smart pointer to a concrete class object while leveraging polymorphism (using <BaseInputSource>
as the base type in the std::make_unique
template).
The beauty of this design lies in its use of polymorphism: the factory returns a std::unique_ptr<BaseInputSource>
, allowing client code to work with any input type through the same interface.
Why This Pattern Works
The Factory Method pattern delivers several key advantages in this robotic application context:
- Encapsulation: All object creation logic is centralized in the factory, keeping client code clean and focused on business logic
- Extensibility: Adding new input types (like thermal cameras or depth sensors) requires only implementing the
BaseInputSource
interface and updating the factory switch statement - Maintainability: Changes to initialization logic are isolated to specific concrete classes, reducing the risk of breaking existing functionality
- Polymorphism: Client code works with the abstract interface, making it easy to swap implementations or add runtime behavior changes
- Memory Safety: Smart pointers handle cleanup automatically, preventing memory leaks in long-running robotic applications
- Type Safety: The enum class prevents invalid input type configurations at compile time
Pattern Classification
This implementation follows the Simple Factory pattern - the most practical and commonly used variant of the Factory Method pattern. It’s perfect for scenarios where you have a limited, well-defined set of object types and want to centralize their creation logic.
The Simple Factory differs from the full Factory Method pattern in that it uses static methods rather than inheritance-based factories, making it simpler to implement and understand while still providing the core benefits of the pattern.
Comments powered by Disqus. Accept cookies to comment.