Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Exploring the Builder Pattern in C++ and C

May 3, 2023

Exploring the Builder Pattern in C++ and C

When building complex objects in programming, especially in C++ and C, the Builder Design Pattern offers a structured approach to construct composite objects. This pattern is particularly useful for creating large, intricate objects dynamically, which is a challenge for simpler approaches like the Abstract Factory pattern.

Core Concept of Builder Pattern

Unlike the Abstract Factory pattern, which is used for creating families of similar objects, the Builder pattern is designed to construct complex objects from smaller parts using different builders. It often involves a Director class to manage the building process.

Logger Example in C++

Let’s create a logging utility library as an example. This library will have different builders to provide various types of loggers.

Content Providers

The Logger contains several content providers for different components like date/time/timestamp, message prefix, and postfix.

 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
// IContentProvider Interface
struct IContentProvider {
    virtual ~IContentProvider() {}
    virtual std::string getContent() = 0;
};

// NullContentProvider
struct NullContentProvider : IContentProvider {
    std::string getContent() override {
        return "";
    }
};

// TextProvider
struct TextProvider : IContentProvider {
    TextProvider(std::string text) : text(text) {}
    std::string getContent() override {
        return text;
    }
private:
    std::string text;
};

// DateTimeProvider
struct DateTimeProvider : IContentProvider {
    std::string getContent() override {
        // Implementation to return current date-time as string
    }
};

Data Sinks

Data sinks handle the output of constructed messages, including a standard output sink and a null sink.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// IDataSink Interface
struct IDataSink {
    virtual ~IDataSink() {}
    virtual void printLine(std::string) = 0;
};

// NullDataSink
struct NullDataSink : IDataSink {
    void printLine(std::string) override {}
};

// CoutDataSink
struct CoutDataSink : IDataSink {
    void printLine(std::string text) override {
        std::cout << text << std::endl;
    }
};

The Logger

The Logger combines these components to construct and log messages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Logger
struct Logger {
    // Constructors, methods to set content providers and data sink
    void log(std::string message) {
        // Compose and log the message
    }
private:
    IContentProvider *dateTimeProvider;
    IContentProvider *prefixProvider;
    IContentProvider *postfixProvider;
    IDataSink *dataSink;
};

The Builders

We create different types of loggers, such as a modest one and a full-option logger, using builders.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// ALoggerBuilder Abstract Class
struct ALoggerBuilder {
    // Methods to create and configure Logger
protected:
    virtual IContentProvider *getDateTimeProvider() = 0;
    virtual IContentProvider *getPrefixProvider() = 0;
    virtual IContentProvider *getPostfixProvider() = 0;
    virtual IDataSink *getDataSink() = 0;
    Logger *logger;
};

// ModestLoggerBuilder
struct ModestLoggerBuilder : ALoggerBuilder {
    // Implementations for a modest logger
};

// FullOptionLoggerBuilder
struct FullOptionLoggerBuilder : ALoggerBuilder {
    // Implementations for a full-option logger
};

Director and Testing

The Director class uses these builders to construct different loggers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// LoggerBuildingDirector
struct LoggerBuildingDirector {
    void setBuilder(ALoggerBuilder *b) {
        builder = b;
    }
    void buildLogger() {
        // Builder logic
    }
    Logger *getLogger() {
        return builder->getLogger();
    }
private:
    ALoggerBuilder *builder;
};

// main
int main() {
    // Use Director to build and test different loggers
}

Expanding the Example: C Implementation

Implementing the Builder pattern in C can be challenging due to the lack of classes and inheritance. However, we can use structures and function pointers to simulate this pattern.

Logger in C

Define a logger structure with function pointers for different components and a function to build the logger.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Logger in C
typedef struct {
    // Function pointers for content providers and data sink
    char *(*getDateTime)();
    char *(*getPrefix)();
    char *(*getPostfix)();
    void (*printLine)(char *);
} Logger;

// Function to build and configure Logger
void buildLogger(Logger *logger, /* Parameters for providers and sink */) {
    // Set function pointers
}

Using the Logger

In the main function, create a Logger instance and configure it with the desired content providers and data sink.

1
2
3
4
5
6
// main in C
int main() {
    Logger myLogger;
    buildLogger(&myLogger, /* Parameters */);
    // Use the logger
}

Conclusion

The Builder pattern in C++ provides a structured and scalable approach to construct complex objects, while in C, it requires a bit more creativity but can still be achieved. This pattern enhances the modularity of code, allowing the construction of objects with various configurations and capabilities.


➡️ Mastering code cohesion and coupling for cleaner designs


⬅️ Design patterns and SOLID principles: A comprehensive overview


Go back to Posts.