5
\$\begingroup\$

I'm studying design patterns from Head First Design Patterns and, in order to get confident, I plan to implement the each pattern in C++ after studying the corresponding chapter.

As regards the Strategy pattern, this is the result:

  • Duck.hpp:
#ifndef DUCK_H
#define DUCK_H
#include <memory>
#include "FlyBehavior.hpp"
#include "QuackBehavior.hpp"
class Duck {
 private:
  std::unique_ptr<FlyBehavior> flyBehavior;
  std::unique_ptr<QuackBehavior> quackBehavior;
 public:
  Duck(std::unique_ptr<FlyBehavior>, std::unique_ptr<QuackBehavior>);
  void performFly();
  void performQuack();
  void setFlyBehavior(std::unique_ptr<FlyBehavior>);
  void setQuackBehavior(std::unique_ptr<QuackBehavior>);
  virtual void display() = 0;
};
#endif /* ifndef DUCK_H */
  • Duck.cpp:
#include <algorithm>
#include <memory>
#include "Duck.hpp"
Duck::Duck(std::unique_ptr<FlyBehavior> fb,
           std::unique_ptr<QuackBehavior> qb)
  : flyBehavior(std::move(fb))
  , quackBehavior(std::move(qb))
{}
void Duck::performFly() {
  flyBehavior->fly();
};
void Duck::performQuack() {
  quackBehavior->quack();
};
void Duck::setFlyBehavior(std::unique_ptr<FlyBehavior> fb) {
  flyBehavior = std::move(fb);
};
void Duck::setQuackBehavior(std::unique_ptr<QuackBehavior> qb) {
  quackBehavior = std::move(qb);
};
  • RubberDuck.hpp:
#ifndef RUBBERDUCK_H
#define RUBBERDUCK_H
#include "Duck.hpp"
class RubberDuck : public Duck {
 public:
  RubberDuck();
  void display() override;
};
#endif /* ifndef RUBBERDUCK_H */
  • RubberDuck.cpp:
#include <iostream>
#include <memory>
#include "FlyNoWay.hpp"
#include "RubberDuck.hpp"
#include "Squeak.hpp"
RubberDuck::RubberDuck()
  : Duck(std::make_unique<FlyNoWay>(),
         std::make_unique<Squeak>()) {
};
void RubberDuck::display() {
  std::cout << "Hello, I am a rubber duck!\n";
};
  • SupersonicDuck.hpp:
#ifndef SUPERSONICDUCK_H
#define SUPERSONICDUCK_H
#include "Duck.hpp"
class SupersonicDuck : public Duck {
 public:
  SupersonicDuck();
  void display() override;
};
#endif /* ifndef SUPERSONICDUCK_H */
  • SupersonicDuck.cpp:
#include <iostream>
#include <memory>
#include "FlyRocketPowered.hpp"
#include "SupersonicDuck.hpp"
#include "SupersonicSqueak.hpp"

SupersonicDuck::SupersonicDuck()
  : Duck(std::make_unique<FlyRocketPowered>(),
         std::make_unique<SupersonicSqueak>()) {
}
void SupersonicDuck::display() {
  std::cout << "Hello, I am a supersonic duck!\n";
};
  • FlyBehavior.hpp:
#ifndef FLYBEHAVIOR_H
#define FLYBEHAVIOR_H
class FlyBehavior {
 public:
  virtual void fly() = 0;
};
#endif /* ifndef FLYBEHAVIOR_H */
  • FlyNoWay.hpp:
#ifndef NOFLY_H
#define NOFLY_H
#include "FlyBehavior.hpp"
class FlyNoWay : public FlyBehavior {
 public:
  void fly() override;
};
#endif /* ifndef NOFLY_H */
  • FlyNoWay.cpp:
#include <iostream>
#include "FlyNoWay.hpp"
void FlyNoWay::fly() {
  std::cout << "I can't fly!\n";
};
  • FlyRocketPowered.hpp:
#ifndef FLYROCKETPOWERED_H
#define FLYROCKETPOWERED_H
#include "FlyBehavior.hpp"
class FlyRocketPowered : public FlyBehavior {
  void fly() override;
};
#endif /* ifndef FLYROCKETPOWERED_H */
  • FlyRocketPowered.cpp:
#include <iostream>
#include "FlyRocketPowered.hpp"
void FlyRocketPowered::fly() {
  std::cout << "WHOOOOOOMMMMMM\n";
};
  • QuackBehavior.hpp
#ifndef QUACKBEHAVIOR_H
#define QUACKBEHAVIOR_H
class QuackBehavior {
 public:
  virtual void quack() = 0;
};
#endif /* ifndef QUACKBEHAVIOR_H */
  • Squeak.hpp
#ifndef SQUEAK_H
#define SQUEAK_H
#include "QuackBehavior.hpp"
class Squeak : public QuackBehavior {
 public:
  void quack() override;
};
#endif /* ifndef SQUEAK_H */
  • Squeak.cpp
#include <iostream>
#include "Squeak.hpp"
void Squeak::quack() {
  std::cout << "Squeak!\n";
};
  • SupersonicSqueak.hpp
#ifndef SUPERSONICSQUEAK_H
#define SUPERSONICSQUEAK_H
#include "QuackBehavior.hpp"
class SupersonicSqueak : public QuackBehavior {
 public:
  void quack() override;
};
#endif /* ifndef SUPERSONICSQUEAK_H */
  • SupersonicSqueak.cpp
#include <iostream>
#include "SupersonicSqueak.hpp"
void SupersonicSqueak::quack() {
  std::cout << "SQUEEEEAAAAK!\n";
};
  • main.cpp
#include <memory>
#include "RubberDuck.hpp"
#include "SupersonicDuck.hpp"
#include "FlyRocketPowered.hpp"
#include "Squeak.hpp"
int main() {
  RubberDuck rd;
  rd.display();
  rd.performFly();
  rd.performQuack();

  SupersonicDuck sd;
  sd.display();
  sd.performFly();
  sd.performQuack();

  // enhance rd with rockets:
  rd.setFlyBehavior(std::make_unique<FlyRocketPowered>());
  rd.display();
  rd.performFly();
  rd.performQuack();
}

The code works, hence I'm posting here an not on Stack Overflow, however I have some concerns about the following choices:

  • I've used make_unique/unique_ptr, but I could also use make_shared/shared_ptr;
  • I've tried to always split a translation unit in header and implementation files;
  • I've not defined a default constructor, but rather a two parameter constructor, as no duck should exist without the two behaviors;
  • The methods setFlyBehavior and setQuackBehavior can change the objects' behavior at runtime, such that the only two things that cannot change are the type itself of the object, and those methods (such as display) which have not been factored out of the Duck class.
\$\endgroup\$
2
\$\begingroup\$

The code looks good. It is probably a good idea to make the include guard macros agree with the file name (_H vs .hpp).

There is a simpler way to implement the strategy pattern — to store std::functions rather than smart pointers to base classes:

struct Duck {
    using behavior_t = std::function<void()>;
    behavior_t fly;
    behavior_t quack;
    behavior_t display; // maybe const; but that disables moving
};

Now everything is much simpler:

// for example
const auto plain_fly   = [] { std::cout << "(flies)\n"; };
const auto plain_quack = [] { std::cout << "(quacks)\n"; };

// can even determine operation dynamically
struct hello_display {
    std::string name;
    void operator()() const
    {
        std::cout << "Hello, I am a " << name << "!\n";
    }
};

int main()
{
    Duck plain_duck{plain_fly, plain_quack, hello_display{"plain duck"}};
    plain_duck.fly();
    plain_duck.quack();
    plain_duck.display();
}

(live demo)

I've used make_unique/unique_ptr, but I could also use make_shared/shared_ptr;

If the resource is owned by only one smart pointer rather than shared by multiple smart pointers, then unique_ptr is a reasonable choice.

I've tried to always split a translation unit in header and implementation files;

For simple operations, you can alternatively define the methods inline in the header file.

I've not defined a default constructor, but rather a two parameter constructor, as no duck should exist without the two behaviors;

Consider checking for null pointers.

The methods setFlyBehavior and setQuackBehavior can change the objects' behavior at runtime, such that the only two things that cannot change are the type itself of the object, and those methods (such as display) which have not been factored out of the Duck class.

This is fine if it fits the semantics of your program. Note that you can also use the strategy pattern for immutable attributes by not providing the corresponding modifier method.

\$\endgroup\$
  • \$\begingroup\$ Concerning unique_ptr vs shared_ptr, your comment seems correct by definition. But how does this translate in common (not necessarily toy) usecases? I mean, using shared_ptr means that one could use a single flyBehavior object for all Duck objects, whereas using unique_ptr means that one should use a flyBehavior object for each Duck object. \$\endgroup\$ – Enlico Mar 15 '20 at 9:13
  • 1
    \$\begingroup\$ @EnricoMariaDeAngelis As you mentioned, shared pointers can be copied, while unique pointers can only be moved (virtual clone methods are required if you want to copy them). The selection depends on the semantics - do you want each duck to have exclusive possession the objects indicating its behavior? There isn't a clear-cut choice to make. \$\endgroup\$ – L. F. Mar 15 '20 at 10:19
  • \$\begingroup\$ Ok, I understand that the two approaches (unique_ptr vs shared_ptr) are equally desirable in different scenarios (I just don't know what the two groups of scenarios are, but maybe I just need to have some exposure to them in a professional framework). \$\endgroup\$ – Enlico Mar 15 '20 at 10:23
  • 1
    \$\begingroup\$ @EnricoMariaDeAngelis Your understanding is correct. My point was that sometimes OOP patterns don't necessary need to be implemented with inheritance. \$\endgroup\$ – L. F. Mar 15 '20 at 10:39
  • 1
    \$\begingroup\$ @EnricoMariaDeAngelis Well, I always regarded FP as a programming paradigm [...] that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It is a declarative programming paradigm in that programming is done with expressions or declarations instead of statements. I don't think it has anything directly to do with first-class functions. I might be wrong. \$\endgroup\$ – L. F. Mar 16 '20 at 6:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.