CodeBlanks Logo Icon CodeBlanks

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.

Practice 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).

Starter code

main.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
#include <algorithm>
#include <concepts>
#include <thread>
#include <atomic>
#include <memory>

// Write the Logger class (singleton)
// Write the Task interface and PrintTask
// Write the TaskFactory (makePrint)

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;
    // Create a task with TaskFactory::makePrint and execute it
    return 0;
}

int main() {
    Framework fw;
    fw.run();
    return 0;
}

Sample solution

main.cpp
#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;
}

Expected output

Project ready
sum: 4
parallel: 4
[log] task done

Questions and answers

Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.

1 What does the Singleton pattern provide?

  • Multiple instances
  • A single instance and global access Correct answer
  • Multithreading
  • Automatic memory cleanup

Explanation: Singleton provides a single instance and global access.

2 Match each pattern to its description

Term Meaning
Singleton Single instance
Factory Object creation
Logger Logger
Task Abstract work

3 static instance

Question
static Logger& ____() { static Logger inst; return inst; }
Answer
static Logger& instance() { static Logger inst; return inst; }

Explanation: instance() returns the single instance of the singleton.

All lessons in Advanced