Builder pattern in C++.pdf

DimitriosPlatis 165 views 13 slides Jan 02, 2023
Slide 1
Slide 1 of 13
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13

About This Presentation

A pragmatic approach to the Builder design pattern, with modern C++


Slide Content

Builder pattern
A pragmatic approach

Builder pattern - Why?
●Large number of attributes needed for
class initialization
○Most of them optional
○"Small" differences among multiple
instances
●Class construction becomes
troublesome
○Too many arguments or too many
constructors
○Awkward to pass unnecessary
arguments
●Wrapping all possible attributes in a
separate class and injecting it, can be
less expressive and inconvenient
Source: refactoring.guru/design-patterns/builder

Builder pattern - How?
●Split initialization in steps
●Initialize relevant attributes only
●Last step returns "complete" instance
●Optional "Director" class to
encapsulate the build steps further
Source: refactoring.guru/design-patterns/builder

Builder pattern - A pragmatic approach
●Textbook examples too complex
●Keep things as simple as possible
●Modern C++
●Skip the "Director"
○Overkill in most cases
●Demonstrate the simplest Builder, then
move on to more "complex"
○All samples are practical
Good sources for further reading:
●refactoring.guru
●Vishal Chovatiya

⚠ Disclaimers ⚠
-Treat code as pseudocode
-Assuming C++20
-Not (m)any C++20-specific features
demonstrated
-Most, if not everything, should be C++11
compatible
-Not following any specific coding
guidelines
-Assuming no domain restrictions
-There may be room for optimization

Use case: Menu
●Compulsory attributes
○ID
●Optional attributes
○Title
○Border
○Options
○Orientation

Without the Builder
// Constructor with all fields
Menu(std::string id,
std::string title,
std::vector<std::string> options,
bool horizontal,
int border);
// Constructors with all field combinations
Menu(std::string id);
Menu(std::string id, std::string title);
Menu(std::string id, std::vector<std::string> options);
// ...
Menu(std::string id);
void setTitle(std::string title);
void setOptions(std::vector<std::string> options);
void setHorizontal(bool horizontal);
void setBorder(int border);

Basic Builder
class Menu {
public:
static Menu create(std::string id) { return Menu{id}; }
void show() const {}
void select(int /* index */) const {}

Menu& withTitle(std::string title) { mTitle = title; return *this; }
Menu& withBorder(int pixels) { mBorder = pixels; return *this; }
Menu& addOption(std::string o) { mOptions.emplace_back(o); return *this; }
Menu& horizontal() { mHorizontal = true; return *this; }
Menu& vertical() { mHorizontal = false; return *this; }

private:
std::string mId{};
std::string mTitle{};
std::vector<std::string> mOptions{};
bool mHorizontal{false};
int mBorder{0};

Menu(std::string id) : mId{id} {}
};
const auto mainMenu = Menu::create("main")
. withTitle("Main Menu")
. addOption("Option 1")
. addOption("Option 2")
. horizontal();
mainMenu.show();
mainMenu.select(1);

const auto bottomMenu = Menu::create("bottom")
. addOption("Option 1")
. addOption("Option 2")
. addOption("Option 3")
. withBorder(2);

"Late access" aka "traditional" Builder
class Impl {
public:
void show() const {}
void select(int /* index */) const {}

private:
friend class Menu;

std::string mId{};
std::string mTitle{};
std::vector<std::string> mOpts{};
bool mHorizontal{false};
int mBorder{0};

Impl(std::string id) : mId{id} {}
};
class Menu {
public:
class Impl { /* Continued on the right -------------> */ };

Menu(std::string id) : mImpl{new Impl{id}} {}

std::unique_ptr<Impl> build() { return std::move(mImpl); }

Menu& withTitle(std::string title) { mImpl->mTitle = title; return *this;
}
Menu& withBorder(int pixels) { mImpl->mBorder = pixels; return *this; }
Menu& addOption(std::string o) { mImpl->mOpts.push_back(o); return *this;
}
Menu& horizontal() { mImpl->mHorizontal = true; return *this; }
Menu& vertical() { mImpl->mHorizontal = false; return *this; }

private:
std::unique_ptr<Impl> mImpl{};
};

"Late access" Builder
const auto /* std::unique_ptr<Menu::Impl> */ menu = Menu{"main"}
. withTitle("Main Menu")
. withBorder(1)
. addOption("Option 1")
. addOption("Option 2")
. horizontal()
. build();

menu->show();
menu->select(1);

Builder with interfaces
class Menu {
public:
virtual ~Menu() = default;
virtual void show() const = 0;
virtual void select(int index) const = 0;
};

class Builder {
public:
virtual ~Builder() = default;
virtual Builder& withTitle(std::string title) = 0;
virtual Builder& withBorder(int pixels) = 0;
virtual Builder& addOption(std::string option) = 0;
virtual Builder& horizontal() = 0;
virtual Builder& vertical() = 0;
virtual std::unique_ptr<Menu> build() = 0;
};
const auto menu = MenuBuilder{"main"}
. withTitle("Main Menu")
. withBorder(1)
. addOption("Option 1")
. addOption("Option 2")
. horizontal()
. build();
menu->show();
menu->select(1);

Builder with interfaces
class MenuBuilder : public Builder {
public:
class DefaultMenu : public Menu { /* Continued on the right ----------> */ };

MenuBuilder(std::string id) : mMenu{new DefaultMenu{id}} {}

std::unique_ptr<Menu> build() override { return std::move(mMenu); }

Builder& withTitle(std::string t) override { mMenu->mTitle = t; return *this; }
Builder& withBorder(int p) override { mMenu->mBorder = p; return *this; }
Builder& addOption(std::string o) override {
mMenu->mOpts.push_back(o);
return *this;
}
Builder& horizontal() override { mMenu->mHorizontal = true; return *this; }
Builder& vertical() override { mMenu->mHorizontal = false; return *this; }

private:
std::unique_ptr<DefaultMenu> mMenu{};
};
class DefaultMenu : public Menu {
public:
void show() const override {}
void select(int /* index */) const override {}

private:
friend class MenuBuilder;

std::string mId{};
std::string mTitle{};
std::vector<std::string> mOpts{};
bool mHorizontal{false};
int mBorder{0};

DefaultMenu(std::string id) : mId{id} {}
};

Takeaways - Builder pattern
●Facilitate initialization of complex objects
○Simpler
○More expressive
●Don't over-engineer
○Use the simplest form of the pattern that
makes sense
○Don't apply the pattern unless there's the
potential for a large number (>5) of optional
arguments
GitHub: platisd/cpp-builder-pattern
Source: refactoring.guru