C++
Design patterns
Write modular structure with design patterns.
Design patterns layer
In this lesson we add two design patterns to the project: Singleton (Logger) and Factory (TaskFactory). These patterns make the code modular and extensible.
Logger singleton: one instance, global access. It is obtained with static Logger& instance(). TaskFactory::makePrint produces a PrintTask. PrintTask derives from the abstract Task interface and its execute() method writes to the Logger. This way adding new task types is easy: you just write a new Task derivative and a factory method. The previous layers stay in place.
Task interface and Factory
Task is the abstract interface: virtual void execute() = 0;. PrintTask implements it. TaskFactory::makePrint returns std::unique_ptr<Task>.
Example: auto t = TaskFactory::makePrint("task done"); t->execute();. makePrint produces a PrintTask but returns it through the Task interface. This lets the caller run a task without knowing the concrete type. To add a new task type, you only write a new Task derivative and a new factory method. std::make_unique manages memory safely.
Exercise Add Logger and TaskFactory
Write the Logger singleton class with members static Logger& instance() and void log(const std::string& msg). Add the Task abstract interface, the PrintTask derivative, and the TaskFactory::makePrint factory method. In run add auto t = TaskFactory::makePrint("task done"); t->execute(); (end the previous line with << std::endl).
Show example solution
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
#include <algorithm>
#include <concepts>
#include <thread>
#include <atomic>
#include <memory>
class Logger {
public:
static Logger& instance() {
static Logger inst;
return inst;
}
void log(const std::string& msg) { std::cout << "[log] " << msg; }
private:
Logger() = default;
};
class Task {
public:
virtual ~Task() = default;
virtual void execute() = 0;
};
class PrintTask : public Task {
public:
PrintTask(const std::string& m) : msg_(m) {}
void execute() override { Logger::instance().log(msg_); }
private:
std::string msg_;
};
class TaskFactory {
public:
static std::unique_ptr<Task> makePrint(const std::string& m) {
return std::make_unique<PrintTask>(m);
}
};
class Framework {
public:
int run();
template<std::integral T>
int process_data(const std::vector<T>& data);
int parallel_sum(const std::vector<int>& data);
};
template<std::integral T>
int Framework::process_data(const std::vector<T>& data) {
int sum = 0;
for (auto x : data | std::views::filter([](T v) { return v > 0; })) {
sum += x;
}
return sum;
}
int Framework::parallel_sum(const std::vector<int>& data) {
std::atomic<int> total{0};
std::thread t1([&]() { for (auto x : data) if (x > 0) total += x; });
t1.join();
return total;
}
int Framework::run() {
std::cout << "Project ready" << std::endl;
std::vector<int> data{1, -2, 3};
std::cout << "sum: " << process_data(data) << std::endl;
std::cout << "parallel: " << parallel_sum(data) << std::endl;
auto t = TaskFactory::makePrint("task done");
t->execute();
return 0;
}
int main() {
Framework fw;
fw.run();
return 0;
}
Test yourself
-
What does the Singleton pattern provide?
- Multiple instances
- A single instance and global access
- Multithreading
- Automatic memory cleanup
Show answer
A single instance and global access — Singleton provides a single instance and global access.
-
Match each pattern to its description
- Singleton
- Single instance
- Factory
- Object creation
- Logger
- Logger
- Task
- Abstract work
-
static instance
static Logger& ____() { static Logger inst; return inst; }
Show answer
static Logger& instance() { static Logger inst; return inst; }
Next lesson: Putting it together