C++ Design Patterns for ADAS/ML Systems
This guide covers the top C++ design patterns essential for building modular, scalable, and testable systems in
machine learning, computer vision, ADAS, and autonomous vehicles. Each pattern includes real-world relevance,
code examples, and when to use them.
1. Strategy Pattern - Swappable Algorithms
What It Is
Encapsulates interchangeable algorithms behind a common interface. Clients choose one at runtime.
When to Use
• Sensor fusion (radar-first vs. camera-first)
• Object tracking (e.g., Kalman vs. particle filter)
Code Example
class FusionStrategy {
public:
virtual void fuse() = 0;
virtual ~FusionStrategy() = default;
};
class RadarFirstFusion : public FusionStrategy {
public:
void fuse() override { std::cout << "Radar-first fusion\n"; }
};
class CameraFirstFusion : public FusionStrategy {
public:
void fuse() override { std::cout << "Camera-first fusion\n"; }
};
class FusionSystem {
FusionStrategy* strategy_;
public:
void setStrategy(FusionStrategy* strategy) { strategy_ = strategy; }
void runFusion() { strategy_->fuse(); }
};
2. Factory Pattern - Encapsulated Object Creation
What It Is
Creates objects without exposing the instantiation logic.
When to Use
• Choose backend engine (ONNX, TensorRT)
• Modularize loggers, models, or data sources
Code Example
class ModelRunner {
public:
virtual void run() = 0;
};
class TensorRTRunner : public ModelRunner {
public:
void run() override { std::cout << "Running with TensorRT\n"; }
};
class ONNXRunner : public ModelRunner {
public:
void run() override { std::cout << "Running with ONNX\n"; }
};
class ModelFactory {
public:
static std::unique_ptr<ModelRunner> create(const std::string& type) {
if (type == "tensorrt") return std::make_unique<TensorRTRunner>();
if (type == "onnx") return std::make_unique<ONNXRunner>();
return nullptr;
}
};
3. Abstract Factory - Families of Related Objects
What It Is
Creates families of related components that work together, abstracting away specific details.
When to Use
• Create platform-specific stacks (e.g., Orin vs. x86)
• Ensure compatibility between components
Code Example
class PreProcessor {
public:
virtual void preprocess() = 0;
};
class CUDAProcessor : public PreProcessor {
public:
void preprocess() override { std::cout << "CUDA preprocessing\n"; }
};
class CPUProcessor : public PreProcessor {
public:
void preprocess() override { std::cout << "CPU preprocessing\n"; }
};
class InferenceFactory {
public:
virtual std::unique_ptr<ModelRunner> createRunner() = 0;
virtual std::unique_ptr<PreProcessor> createPreproc() = 0;
};
class NvidiaFactory : public InferenceFactory {
public:
std::unique_ptr<ModelRunner> createRunner() override {
return std::make_unique<TensorRTRunner>();
}
std::unique_ptr<PreProcessor> createPreproc() override {
return std::make_unique<CUDAProcessor>();
}
};
4. Observer Pattern - Event Notification System
What It Is
One-to-many relationship where subjects notify observers about state changes.
When to Use
• Broadcast sensor frame updates
• Log or visualize new data frames
Code Example
class Observer {
public:
virtual void onFrame() = 0;
};
class FrameProvider {
std::vector<Observer*> observers;
public:
void add(Observer* obs) { observers.push_back(obs); }
void newFrame() {
for (auto* obs : observers) obs->onFrame();
}
};
class Logger : public Observer {
void onFrame() override { std::cout << "Logging frame\n"; }
};
5. Singleton Pattern - Single Instance Global Access
What It Is
Ensures only one instance of a class exists globally.
When to Use
• GPU or memory context
• Global configuration or logging state
Code Example
class GPUContext {
private:
static GPUContext* instance;
GPUContext() { std::cout << "GPU context initialized\n"; }
public:
static GPUContext* get() {
if (!instance) instance = new GPUContext();
return instance;
}
void allocate() { std::cout << "Allocating GPU resources\n"; }
};
GPUContext* GPUContext::instance = nullptr;
6. Adapter Pattern - Legacy Code Integration
What It Is
Translates one interface into another to allow reuse of legacy or third-party code.
When to Use
• Wrap legacy CUDA kernels or 3rd-party APIs
Code Example
class IKernel {
public:
virtual void run() = 0;
};
class LegacyKernel {
public:
void executeLegacy() { std::cout << "Legacy CUDA kernel\n"; }
};
class LegacyAdapter : public IKernel {
LegacyKernel legacy;
public:
void run() override { legacy.executeLegacy(); }
};
7. Decorator Pattern - Add Behavior Dynamically
What It Is
Dynamically adds new behavior to objects by wrapping them.
When to Use
• Add logging, profiling, or validation to a core object
Code Example
class IProcessor {
public:
virtual void process() = 0;
};
class CoreProcessor : public IProcessor {
public:
void process() override { std::cout << "Core processing\n"; }
};
class ProfilingDecorator : public IProcessor {
std::unique_ptr<IProcessor> wrapped;
public:
ProfilingDecorator(std::unique_ptr<IProcessor> w) : wrapped(std::move(w)) {}
void process() override {
auto start = std::chrono::high_resolution_clock::now();
wrapped->process();
auto end = std::chrono::high_resolution_clock::now();
std::cout << "[Profiling] Time: "
<< std::chrono::duration<double, std::milli>(end - start).count() << " ms\n";
}
};
8. Facade Pattern - Simplified High-Level Interface
What It Is
Hides system complexity by providing a unified interface to multiple subsystems.
When to Use
• Control an entire ADAS perception pipeline
Code Example
class PerceptionFacade {
SensorManager sensor;
Preprocessor preproc;
ModelRunner model;
PostProcessor postproc;
public:
void runPipeline() {
sensor.capture();
preproc.preprocess();
model.infer();
postproc.process();
std::cout << "Perception pipeline complete\n";
}
};
Summary Table
Pattern Purpose Typical Use Case
Strategy Choose behavior at runtime Fusion strategy, tracking method
Factory Create one object based on type Select inference backend
Abstract Factory Create group of related objects Platform-specific pipelines
Observer Notify many objects when event occurs Frame ready → trigger logging, fusion, etc.
Singleton One shared instance globally GPU context, config manager
Adapter Wrap legacy/3rd-party code Use old CUDA kernel in modern pipeline
Decorator Add behavior without changing original object Inject profiling/logging around core components
Facade Simplify interaction with complex subsystems Run full perception pipeline in one function