806 lines
No EOL
27 KiB
Markdown
806 lines
No EOL
27 KiB
Markdown
## Builder
|
|
|
|
Whenever we need to create some complex object, which may have a lot of parameters, we end up with a constructor which takes plenty of arguments. In most cases we even don't want to set up all of this, so we create dozens of constructors which allow us to set up only needed parameters. And when we add another parameter, we need to add few more constructors. That's Horrible, isn't it? So how we can solve such a problem? How to create only one Constructor and allow user to build an object by injecting only needed parameters. In previous sentence I use a word `buid`, and this is exactly a design pattern that I want to present you.
|
|
|
|
<img data-src="images/builder_uml.png" alt="Builder UML">
|
|
___
|
|
|
|
## Ship
|
|
|
|
This is the constructor of class `Ship`. We can easily imagine that in the future we will add at least a few more parameters.
|
|
|
|
```C++
|
|
Ship::Ship(Time* time,
|
|
std::unique_ptr<DifficultLvlStrategy<Ship, bool>> strategy,
|
|
const std::string& name,
|
|
int capacity,
|
|
int maxCrew,
|
|
int crew,
|
|
std::unique_ptr<PrintVisitor>&& visitor,
|
|
int armor,
|
|
int maxArmor,
|
|
int canons,
|
|
int maxCanons,
|
|
int durability,
|
|
int maxDurability)
|
|
: _time(time),
|
|
_strategy(std::move(strategy)),
|
|
_name(name),
|
|
_capacity(capacity),
|
|
_maxCrew(maxCrew),
|
|
_crew(crew),
|
|
_visitor(std::move(visitor)),
|
|
_armor(armor),
|
|
_maxArmor(maxArmor),
|
|
_canons(canons),
|
|
_maxCanons(maxCanons)
|
|
_durability(durability),
|
|
_maxDurability(maxDurability) {
|
|
if (_capacity < 0) {
|
|
throw std::runtime_error("Capacity can't be negative value!");
|
|
}
|
|
assert(time);
|
|
_time->attach(this);
|
|
}
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.60em" -->
|
|
___
|
|
|
|
## Create a ship
|
|
|
|
Can you explain to me which number argument is an `armor` value, and which is a `cannon` value? Reading such code is quite hard. We can easily provide values in wrong order and instead of setting `armor` we will set a `maxCrew` or something else.
|
|
|
|
```C++
|
|
auto ship = Ship(
|
|
time,
|
|
std::move(strategy),
|
|
"Black Pearl",
|
|
200,
|
|
100,
|
|
50,
|
|
std:move(visitor),
|
|
100,
|
|
200,
|
|
20,
|
|
25,
|
|
1000,
|
|
1000);
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.80em" -->
|
|
___
|
|
|
|
## A new way
|
|
|
|
This is a new way of creating a ship. What do you think about this approach?
|
|
|
|
```C++
|
|
// We need to provide all mandatory parameters, the rest will have default values.
|
|
auto ship = Ship(time, name, capacity, maxCrew, maxArmor, maxCannons, maxDurability)
|
|
// Provide optional arguemnts
|
|
.setDifficultLvlStrategy(std::move(strategy))
|
|
.setCrew(100)
|
|
.setPrintVisitor(std::move(visitor))
|
|
.setArmor(100)
|
|
.setCannons(10)
|
|
.setDurability(1000)
|
|
.build();
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.92em" -->
|
|
___
|
|
|
|
## A better way
|
|
|
|
In previous example, we still need to provide few arguments in constructor before we will create an object. We can refactor this code and verify in `build` method if all mandatory parameters were set.
|
|
|
|
```C++
|
|
auto ship = ShipBuilder().setTime(time)
|
|
.setName("Black Pearl")
|
|
.setCapacity(500)
|
|
.setMaxCrew(200)
|
|
.setMaxArmor(1000)
|
|
.setMaxCannons(20)
|
|
.setMaxDurability(1000)
|
|
.setDifficultLvlStrategy(std::move(strategy))
|
|
.setCrew(100)
|
|
.setPrintVisitor(std::move(visitor))
|
|
.setArmor(100)
|
|
.setCannons(10)
|
|
.setDurability(1000)
|
|
.build();
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.92em" -->
|
|
___
|
|
|
|
## Builder class
|
|
|
|
```C++
|
|
class ShipBuilder {
|
|
public:
|
|
ShipBuilder()
|
|
: _ship(std::make_unique<Ship>()) {}
|
|
|
|
[[nodiscard]] std::unique_ptr<Ship> build() {
|
|
if (!_ship) {
|
|
std::cout << "Builder may be use once\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// Check mandatory fields
|
|
if (!_ship->time() ||
|
|
_ship->name().empty() ||
|
|
_ship->capacity() == -1 ||
|
|
_ship->maxCrew() == -1 ||
|
|
_ship->maxArmor() == -1 ||
|
|
_ship->maxCannons() == -1 ||
|
|
_ship->maxDurability() == -1) {
|
|
std::cout << "Mandatory fields are not set!\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return std::move(_ship);
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<Ship> _ship;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.67em" -->
|
|
___
|
|
|
|
## Setters
|
|
|
|
Now we need to add all setters to `ShipBuilder` class
|
|
|
|
```C++
|
|
ShipBuilder& setTime(Time* time) {
|
|
_ship->setTime(time);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setStrategy(std::unique_ptr<DifficultStrategy<Ship, bool>> strategy) {
|
|
_ship->setStrategy(std::move(strategy));
|
|
return *this;
|
|
}
|
|
ShipBuilder& setName(const std::string& name) {
|
|
_ship->setName(name);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setCapacity(int capacity) {
|
|
_ship->setCapacity(capacity);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setMaxCrew(int maxCrew) {
|
|
_ship->setMaxCrew(maxCrew);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setCrew(int crew) {
|
|
_ship->setCrew(crew);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setVisitor(std::unique_ptr<PrintVisitor> visitor) {
|
|
_ship->setVisitor(std::move(visitor));
|
|
return *this;
|
|
}
|
|
ShipBuilder& setArmor(int armor) {
|
|
_ship->setArmor(armor);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setMaxArmor(int maxArmor) {
|
|
_ship->setMaxArmor(maxArmor);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setCannons(int cannons) {
|
|
_ship->setCannons(cannons);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setMaxCannons(int maxCannons) {
|
|
_ship->setMaxCannons(maxCannons);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setDurability(int durability) {
|
|
_ship->setDurability(durability);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setMaxDurability(int maxDurability) {
|
|
_ship->setMaxDurability(maxDurability);
|
|
return *this;
|
|
}
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.62em" -->
|
|
___
|
|
|
|
## The ugly part
|
|
|
|
Unfortunately, if we want to follow the encapsulation rule we need to duplicate `setters` in class `Ship` we need also provide getters for all members (even if we don't use them by other classes).
|
|
|
|
```C++
|
|
class Ship {
|
|
public:
|
|
Ship& setTime(Time* time) {
|
|
_time = time;
|
|
return *this;
|
|
}
|
|
Ship& setStrategy(std::unique_ptr<DifficultStrategy<Ship, bool>> strategy) {
|
|
_strategy = std::move(strategy);
|
|
return *this;
|
|
}
|
|
Ship& setName(const std::string& name) {
|
|
_name = name;
|
|
return *this;
|
|
}
|
|
Ship& setCapacity(int capacity) {
|
|
_capacity = capacity;
|
|
return *this;
|
|
}
|
|
Ship& setMaxCrew(int maxCrew) {
|
|
_maxCrew = maxCrew;
|
|
return *this;
|
|
}
|
|
Ship& setCrew(int crew) {
|
|
_crew = crew;
|
|
return *this;
|
|
}
|
|
Ship& setVisitor(std::unique_ptr<PrintVisitor> visitor) {
|
|
visitor = std::move(_visitor);
|
|
return *this;
|
|
}
|
|
Ship& setArmor(int armor) {
|
|
_armor = armor;
|
|
return *this;
|
|
}
|
|
Ship& setMaxArmor(int maxArmor) {
|
|
_maxArmor = maxArmor;
|
|
return *this;
|
|
}
|
|
Ship& setCannons(int cannons) {
|
|
_canons = cannons;
|
|
return *this;
|
|
}
|
|
Ship& setMaxCannons(int maxCannons) {
|
|
_maxCannons = maxCannons;
|
|
return *this;
|
|
}
|
|
Ship& setDurability(int durability) {
|
|
_durability = durability;
|
|
return *this;
|
|
}
|
|
Ship& setMaxDurability(int maxDurability) {
|
|
_maxDurability = maxDurability;
|
|
return *this;
|
|
}
|
|
|
|
const Time* time() const { return _time; }
|
|
const std::string& name() const { return _name; }
|
|
int capacity() const { return _capacity; }
|
|
int maxCrew() const { return _maxCrew; }
|
|
int crew() const { return _crew; }
|
|
int armor() const { return _armor; }
|
|
int maxArmor() const { return _maxArmor; }
|
|
int canons() const { return _canons; }
|
|
int maxCannons() const { return _maxCannons; }
|
|
int durability() const { return _durability; }
|
|
int maxDurability() const { return _maxDurability; }
|
|
|
|
private:
|
|
// Allow to be created only by std::unique_ptr
|
|
friend std::unique_ptr<Ship> std::make_unique<Ship>();
|
|
Ship() = default;
|
|
|
|
Time* _time{};
|
|
std::unique_ptr<DifficultStrategy<Ship, bool>> _strategy{};
|
|
std::string _name{};
|
|
int _capacity{-1};
|
|
int _maxCrew{-1};
|
|
int _crew{10};
|
|
std::unique_ptr<PrintVisitor> _visitor{};
|
|
int _armor{0};
|
|
int _maxArmor{-1};
|
|
int _canons{0};
|
|
int _maxCannons{-1};
|
|
int _durability{100};
|
|
int _maxDurability{-1};
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.62em" -->
|
|
|
|
___
|
|
|
|
## Clean up
|
|
|
|
In my opinion, a better option is to declare friendship with class `ShipBuilder`. Generally, friend with other classes is not a good idea. But in builder pattern we have a lot of profits:
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Don't need to duplicate setters
|
|
* <!-- .element: class="fragment fade-in" --> Some of the fields should not be changed after Ship will be built, but still we need to have a setter for this field
|
|
* <!-- .element: class="fragment fade-in" --> We don't need to return a reference to `Ship` for all setters.
|
|
* <!-- .element: class="fragment fade-in" --> Don't need to have getters for all members
|
|
|
|
___
|
|
|
|
```C++
|
|
class Ship {
|
|
public:
|
|
void setCrew(int crew) { _crew = crew; }
|
|
void setArmor(int armor) { _armor = armor; }
|
|
void setCannons(int cannons) { _canons = cannons; }
|
|
void setDurability(int durability) { _durability = durability; }
|
|
|
|
const std::string& name() const { return _name; }
|
|
int capacity() const { return _capacity; }
|
|
int crew() const { return _crew; }
|
|
int armor() const { return _armor; }
|
|
int canons() const { return _canons; }
|
|
int durability() const { return _durability; }
|
|
|
|
private:
|
|
// Allow to be created only by std::unique_ptr
|
|
friend std::unique_ptr<Ship> std::make_unique<Ship>();
|
|
Ship() = default;
|
|
}
|
|
```
|
|
<!-- .slide: style="font-size: 0.82em" -->
|
|
___
|
|
|
|
```C++
|
|
class ShipBuilder {
|
|
public:
|
|
ShipBuilder()
|
|
: _ship(std::make_unique<Ship>()) {}
|
|
|
|
[[nodiscard]] std::unique_ptr<Ship> build() {
|
|
if (!_ship) {
|
|
std::cout << "Builder may be use once\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// Check mandatory fields
|
|
if (!_ship->_time ||
|
|
_ship->_name.empty() ||
|
|
_ship->_capacity == -1 ||
|
|
_ship->_maxCrew == -1 ||
|
|
_ship->_maxArmor == -1 ||
|
|
_ship->_maxCannons == -1 ||
|
|
_ship->_maxDurability == -1) {
|
|
std::cout << "Mandatory fields are not set!\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return std::move(_ship);
|
|
}
|
|
|
|
ShipBuilder& setTime(Time* time) {
|
|
_ship->_time = time;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setStrategy(std::unique_ptr<DifficultStrategy<Ship, bool>> strategy) {
|
|
_ship->_strategy = std::move(strategy);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setName(const std::string& name) {
|
|
_ship->_name = name;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setCapacity(int capacity) {
|
|
_ship->_capacity = capacity;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setMaxCrew(int maxCrew) {
|
|
_ship->_maxCrew = maxCrew;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setCrew(int crew) {
|
|
_ship->_crew = crew;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setVisitor(std::unique_ptr<PrintVisitor> visitor) {
|
|
_ship->_visitor = std::move(visitor);
|
|
return *this;
|
|
}
|
|
ShipBuilder& setArmor(int armor) {
|
|
_ship->_armor = armor;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setMaxArmor(int maxArmor) {
|
|
_ship->_maxArmor = maxArmor;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setCannons(int cannons) {
|
|
_ship->_canons = cannons;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setMaxCannons(int maxCannons) {
|
|
_ship->_maxCannons = maxCannons;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setDurability(int durability) {
|
|
_ship->_durability = durability;
|
|
return *this;
|
|
}
|
|
ShipBuilder& setMaxDurability(int maxDurability) {
|
|
_ship->_maxDurability = maxDurability;
|
|
return *this;
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<Ship> _ship;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.62em" -->
|
|
___
|
|
|
|
## Usage
|
|
|
|
For both implementations, we use builder in the same way:
|
|
|
|
```C++
|
|
int main() {
|
|
auto ship = ShipBuilder().build();
|
|
if (ship) {
|
|
std::cout << "Ship name: " << ship->name() << '\n';
|
|
}
|
|
|
|
Time time;
|
|
auto ship2 = ShipBuilder()
|
|
.setTime(&time)
|
|
.setName("Black Pearl")
|
|
.setCapacity(500)
|
|
.setMaxCrew(200)
|
|
.setMaxArmor(1000)
|
|
.setMaxCannons(20)
|
|
.setMaxDurability(1000)
|
|
.build();
|
|
|
|
if (ship2) {
|
|
std::cout << "Ship name: " << ship2->name() << '\n';
|
|
}
|
|
}
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```bash
|
|
Mandatory fields are not set!
|
|
Ship name: Black Pearl
|
|
```
|
|
<!-- .slide: style="font-size: 0.72em" -->
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
## gRPC
|
|
|
|
A perfect example of builder is an gRPC library. We use the builder pattern to create a server object, which we can customize. Based on provided arguments we can create synchronous or asynchronous server, with or without compression, with one or more services, with additional options like max response size etc...
|
|
|
|
```C++
|
|
grpc::Status status;
|
|
std::shared_ptr<grpc::AuthorizationPolicyProviderInterface> provider =
|
|
grpc::FileWatcherAuthorizationPolicyProvider::Create(
|
|
authz_policy_path, /*refresh_interval_sec=*/3600, &status);
|
|
auto option = std::make_unqiue<grpc::ServerBuilderOption>();
|
|
grpc::ChannelArguments args;
|
|
args.SetMaxReceiveMessageSize(4096);
|
|
args.SetMaxSendMessageSize(4096);
|
|
option->UpdateArguments(args);
|
|
|
|
std::unique_ptr<grpc::Server> server =
|
|
grpc::ServerBuilder().AddListeningPort("127.0.0.1:10000", InsecureServerCredentials())
|
|
.RegisterService(&service1)
|
|
.RegisterService("127.0.0.1:9999", &service2)
|
|
.SetAuthorizationPolicyProvider(provider)
|
|
.SetOption(std::move(option))
|
|
.SetDefaultCompressionLevel(grpc::GRPC_COMPRESS_LEVEL_HIGH)
|
|
.BuildAndStart();
|
|
```
|
|
<!-- .slide: style="font-size: 0.72em" -->
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
## DRY
|
|
|
|
At the beginning, we decided to have 3 types of ships. We always need to remember which value to pass to each parameter to create a specific type of ship. If we decide that something is not okay with balance, we need to change this parameter everywhere. If we forget to change it in some code, we end up with different parameters for the same ship type. We can of course decided to have some global variable, but this is an ugly solution. **We should wrap the whole logic in one place**. To make it more flexible, we will describe all parameters in a JSON file, so we will not be forced to recompile the whole binary every time we change the ship parameters. Because we don't want to modify existing code, we just create a new class that inherit `private` from the `ShipBuilder` so we can't use the interface `ShipBuilder` outside, but still `ShipJsonBuilder` may use this implementation.
|
|
|
|
```C++
|
|
class ShipJsonBuilder : private ShipBuilder {
|
|
public:
|
|
ShipJsonBuilder(const std::filesystem::path& path) { parseFile(path); }
|
|
|
|
std::unique_ptr<Ship> buildBrig(Time* time, const std::string& name) const {
|
|
return setTime(time)
|
|
.setName(name)
|
|
.setCapacity(_shipsData["brig"]["capacity"])
|
|
.setMaxCrew(_shipsData["brig"]["maxCrew"])
|
|
.setMaxArmor(_shipsData["brig"]["maxArmor"])
|
|
.setMaxCannons(_shipsData["brig"]["maxCannons"])
|
|
.setMaxDurability(_shipsData["brig"]["maxDurability"])
|
|
.build();
|
|
}
|
|
std::unique_ptr<Ship> buildFrigate(Time* time, const std::string& name) const;
|
|
std::unique_ptr<Ship> buildGaleon(Time* time, const std::string& name) const;
|
|
|
|
private:
|
|
json _shipsData;
|
|
void parseFile(const std::filesystem::path& path); // Parse file and store data in _shipsData
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.67em" -->
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
___
|
|
|
|
## Usage
|
|
|
|
Now, we can easily create one of existing types of ship, without worry about which values to pass. What's more, we read the file from JSON, so we decouple values from the binary, so every time we make some changes in balance, we don't need to recompile any single file.
|
|
|
|
```C++
|
|
int main() {
|
|
Time time;
|
|
|
|
auto ship1 = ShipJsonBuilder().buildGaleon(&time, "Queens Anne Revenge");
|
|
if (ship1) {
|
|
std::cout << "Ship name: " << ship1->name() << '\n';
|
|
}
|
|
|
|
auto ship2 = ShipJsonBuilder().buildFrigate(&time, "Black Pearl");
|
|
if (ship2) {
|
|
std::cout << "Ship name: " << ship2->name() << '\n';
|
|
}
|
|
|
|
auto ship3 = ShipJsonBuilder().buildBrig(&time, "Black Widow");
|
|
if (ship3) {
|
|
std::cout << "Ship name: " << ship3->name() << '\n';
|
|
}
|
|
}
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```bash
|
|
Ship name: Queens Anne Revenge
|
|
Ship name: Black Pearl
|
|
Ship name: Black Widow
|
|
```
|
|
<!-- .slide: style="font-size: 0.74em" -->
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
## Different builder
|
|
|
|
But there is even more! Currently, we focus only on one builder, which always create a `Ship` class. We also create a `ShipJsonBuilder` which reuse implementation from `ShipBuilder` and allow to user read default parameters from the file, so we always create a `Ship` of the same type with equal parameters. We also saw that gRPC use the same logic to also user create various type of server. Furthermore, gRPC add plenty of methods which allow us to set parameters of this server. We can even go further and create a few types of builder, and then pass a pointer to the base class to `Driector` and create a different object.
|
|
|
|
```C++
|
|
class CrewBuilder {
|
|
public:
|
|
CrewBuilder& setName(const std::string& name) = 0;
|
|
CrewBuilder& setWeapon(std::unique_ptr<Weapon>&& weapon) = 0;
|
|
CrewBuilder& setHp(Hp hp) = 0;
|
|
std::unique_ptr<Crew> build() = 0;
|
|
};
|
|
```
|
|
<!-- .slide: style="font-size: 0.74em" -->
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```C++
|
|
class PirateBuilder : public CrewBuilder {
|
|
public:
|
|
CrewBuilder& setName(const std::string& name) override { /* some implementation */ }
|
|
CrewBuilder& setWeapon(std::unique_ptr<Weapon>&& weapon) override { /* some implementation */ }
|
|
CrewBuilder& setHp(Hp hp) override { /* some implementation */ }
|
|
std::unique_ptr<Crew> build() { return std::make_unique<Pirate>(/* args */); }
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
___
|
|
|
|
## Tavern
|
|
|
|
Based on which tavern we are, we can recruit, for instance:
|
|
* <!-- .element: class="fragment fade-in" --> the marine,
|
|
* <!-- .element: class="fragment fade-in" --> the volunteer,
|
|
* <!-- .element: class="fragment fade-in" --> the pirate
|
|
|
|
Each kind of Sailor will act differently in combat and during sail. They also have a different pay. We don't want to hard-code a type of Tavern on every island, because during a game, we can for instance conquer an island and free it forms pirates, so we will no longer recruit their pirates, but maybe a marine or volunteer. That's why we should pass a builder class as an argument to tavern, so we can easily swap it during a game.
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```C++
|
|
class Tawern {
|
|
public:
|
|
Tawern(std::unique_ptr<Builder>&& builder): _builder(std::move(builder)) {}
|
|
|
|
std::unique_ptr<Crew> recruit(const std::string& name, std::unique_ptr<Weapon>&& weapon) {
|
|
return _builder->setName(name).setWeapon(weapon).setHp(40).build();
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<Builder> _builder;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.74em" -->
|
|
___
|
|
|
|
## Mixing two design patterns
|
|
|
|
If you have a feeling that this looks similar to factory method, you are right! Both design patterns are similar. There are few main differences:
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Builder focuses on constructing a complex object step by step. Abstract Factory emphasizes a family of product objects (either simple or complex). Builder returns the product as a final step, but as far as the Abstract Factory is concerned, the product gets returned immediately.
|
|
* <!-- .element: class="fragment fade-in" --> Builder often builds a Composite.
|
|
* <!-- .element: class="fragment fade-in" --> Often, designs start out using Factory Method (less complicated, more customizable, subclasses proliferate) and evolve toward Abstract Factory, Prototype, or Builder (more flexible, more complex) as the designer discovers where more flexibility is needed.
|
|
* <!-- .element: class="fragment fade-in" --> Sometimes creational patterns are complementary: Builder can use one of the other patterns to implement which components get built. Abstract Factory, Builder, and Prototype can use Singleton in their implementations.
|
|
|
|
<!-- .slide: style="font-size: 0.84em" -->
|
|
___
|
|
|
|
## Exercise 1
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Go to the directory <code>Ship</code> and create class <code>ShipBuilder</code>. The mandatory fields are:
|
|
* <!-- .element: class="fragment fade-in" --> name
|
|
* <!-- .element: class="fragment fade-in" --> capacity
|
|
* <!-- .element: class="fragment fade-in" --> time
|
|
* <!-- .element: class="fragment fade-in" --> The non-mandatory fields are:
|
|
* <!-- .element: class="fragment fade-in" --> difficultStrategy -> default value should be set as <b>Easy</b>
|
|
* <!-- .element: class="fragment fade-in" --> crew -> default value should be set as <b>10</b>
|
|
* <!-- .element: class="fragment fade-in" --> durability -> default value should be set as <b>1000</b>
|
|
* <!-- .element: class="fragment fade-in" --> armor -> default value should be set as <b>0</b>
|
|
* <!-- .element: class="fragment fade-in" --> PrintVisitor -> default value should be set as <b>Pretty Print</b>
|
|
* <!-- .element: class="fragment fade-in" --> Try to build your own <code>Ship</code>!
|
|
___
|
|
|
|
## Exercise 2
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Go to the directory <code>Ship</code> and finish implementation of class <code>ShipJsonBuilder</code>. You should allow building 3 types of ship:
|
|
* <!-- .element: class="fragment fade-in" --> Brig
|
|
* <!-- .element: class="fragment fade-in" --> Frigate
|
|
* <!-- .element: class="fragment fade-in" --> Galoen
|
|
* <!-- .element: class="fragment fade-in" --> If you want to read a JSON filed, just use <b>operator[]</b> to get any of nested field, example: <code>_shipsData["brige"]["armor"]</code>
|
|
* <!-- .element: class="fragment fade-in" --> Try to build a ship using a new builder.
|
|
* <!-- .element: class="fragment fade-in" --> Try to change some values in <code>ships.json</code> and run binary without recompilation. You should see new values!
|
|
|
|
___
|
|
|
|
## Factory method
|
|
|
|
Because we saw in the previous example a factory method patter, I want to tell a little more about it. Whenever we want to hide implementation details about creation of new object (and also a final type) we should use factory method.
|
|
|
|
<img data-src="images/factory_method_uml.png" alt="Factory method UML">
|
|
|
|
___
|
|
|
|
Interface of factory:
|
|
```C++
|
|
class Factory {
|
|
public:
|
|
virtual std::unique_ptr<Crew> create(const std::string& name, std::unique_ptr<Weapon>&& weapon, int hp) = 0;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
Implementations:
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```C++
|
|
class PirateBuilder : public CrewBuilder {
|
|
public:
|
|
std::unique_ptr<Crew> create(const std::string& name, std::unique_ptr<Weapon>&& weapon, int hp) override {
|
|
return std::make_unique<Pirate>(name, std::move(weapon), hp);
|
|
}
|
|
};
|
|
|
|
class MarineBuilder : public CrewBuilder {
|
|
public:
|
|
std::unique_ptr<Crew> create(const std::string& name, std::unique_ptr<Weapon>&& weapon, int hp) override {
|
|
return std::make_unique<Pirate>(name, std::move(weapon), hp);
|
|
}
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
Class that use factory to create object:
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```C++
|
|
class Tawern {
|
|
public:
|
|
Tawern(std::unique_ptr<Factory>&& factory): _factory(std::move(factory)) {}
|
|
std::unique_ptr<Crew> recruit(const std::string& name, std::unique_ptr<Weapon>&& weapon) {
|
|
return _factory->create(name, std::move(weapon), 40);
|
|
}
|
|
private:
|
|
std::unique_ptr<Builder> _factory_;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.60em" -->
|
|
|
|
___
|
|
|
|
## Exercise 3
|
|
|
|
* <!-- .element: class="fragment fade-in" --> Go to the directory <code>Ship</code> and create abstract class <code>FruitFactory</code>.
|
|
* <!-- .element: class="fragment fade-in" --> Create <code>FruitFactory</code>
|
|
* <!-- .element: class="fragment fade-in" --> Create <code>ItemFactory</code>
|
|
* <!-- .element: class="fragment fade-in" --> Create <code>AlcoholFactory</code>
|
|
* <!-- .element: class="fragment fade-in" --> Test in main.cpp if you can create any type of cargo
|
|
* <!-- .element: class="fragment fade-in" --> <b>How to handle scenario, when different cargo takes other arguments?</b>
|
|
|
|
___
|
|
|
|
## Different arguments
|
|
|
|
At the end, I want to talk about different arguments in constructors. We have three derived class from `Cargo`, each class takes different arguments.
|
|
|
|
```C++
|
|
struct Fruit : public Cargo {
|
|
int rottenCounter;
|
|
|
|
Fruit(size_t amount, int rottenCounter);
|
|
size_t getPrice() const override;
|
|
const std::string& name() const override;
|
|
void accept(const CargoDamageVisitor& visitor) override;
|
|
};
|
|
|
|
struct Item : public Cargo {
|
|
enum class Type { Common, Rare, Epic, Legendary };
|
|
Type type;
|
|
|
|
Item(size_t amount, Type type);
|
|
size_t getPrice() const override;
|
|
const std::string& name() const override;
|
|
void accept(const CargoDamageVisitor& visitor) override;
|
|
};
|
|
|
|
struct Alcohol : public Cargo {
|
|
enum class Type { White, Spiced, Dark};
|
|
|
|
int power;
|
|
Type type;
|
|
Alcohol(size_t amount, int power, Type type);
|
|
size_t getPrice() const override;
|
|
const std::string& name() const override;
|
|
void accept(const CargoDamageVisitor& visitor) override;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.60em" -->
|
|
|
|
___
|
|
|
|
## How to solve it?
|
|
|
|
**Of course by using template!**
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```C++
|
|
template <typename... Args>
|
|
class CargoFactory {
|
|
public:
|
|
virtual std::unique_ptr<Cargo> create(size_t amount, Args... args) = 0;
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
|
|
```C++
|
|
class FruitFactory : public CargoFactory<int> {
|
|
public:
|
|
std::unique_ptr<Cargo> create(size_t amount, int rottenCounter) override {
|
|
return std::make_unique<Fruit>(amount, rottenCounter);
|
|
}
|
|
};
|
|
|
|
class AlcoholFactory : public CargoFactory<int, Alcohol::Type> {
|
|
public:
|
|
std::unique_ptr<Cargo> create(size_t amount, int power, Alcohol::Type type) override {
|
|
return std::make_unique<Alcohol>(amount, power, type);
|
|
}
|
|
};
|
|
|
|
class ItemFactory : public CargoFactory<Item::Type> {
|
|
public:
|
|
std::unique_ptr<Cargo> create(size_t amount, Item::Type type) override {
|
|
return std::make_unique<Item>(amount, type);
|
|
}
|
|
};
|
|
```
|
|
<!-- .element: class="fragment fade-in" -->
|
|
<!-- .slide: style="font-size: 0.69em" --> |