=== Design Patterns === * Have you ever heard of this expression: "Design Pattern" - It is a common solution to a recurring problem. * We have already seen some design patterns in this course. Can you think about them? - Factory - Build Method - Decorator - Chain of Responsibility - [Exceptions] * Are exceptions a design pattern? - Yes, in a way. Exceptions are implemented natively in C++. Languages that do not offer exceptions, like C, can still have them, if developers implement them as a library. The way to implement them is a design pattern. * Have you heard of the book called "The Gang of Four"? - Design Patterns: Elements of Reusable Object-Oriented Software * Why was this book so important? - Because it catalogued 23 classic design patterns. === Singleton === * One of the simplest patterns is the so called singleton. What is it? - It is a way to ensure that a class has only one instance, and to provide a global point of access to it. * In which situations is a singleton useful? * Can you write an example to simulate a log? * What are the operations that a log system must have? - log(string) - dump() * Can you implement a log system? #include #include class Log { public: void log(std::string msg) { _entries.push_back(msg); } void dump() const { for (unsigned i = 0; i < _entries.size(); i++) { std::cout << i << ": " << _entries[i] << std::endl; } } private: std::vector _entries; }; int main() { Log log; log.log("First event"); log.log("Second event"); log.dump(); } * What if we wanted to centralize all the logs into a single instance? How could we do it? * Can you try to design a singleton? #include #include class Log { public: static Log* instance() { if (!_instance) { _instance = new Log(); } return _instance; } void log(std::string msg) { _entries.push_back(msg); } void dump() const { for (unsigned i = 0; i < _entries.size(); i++) { std::cout << i << ": " << _entries[i] << std::endl; } } private: std::vector _entries; Log() {} // This is the declaration: static Log* _instance; }; // This is the definition: Log* Log::_instance; * What is the difference between declaration and definition? - Declaration informs the type and a name for the compiler. - A definition informs an implementation and a storage to the linker. * How can we use the above singleton? int main() { Log* log = Log::instance(); log->log("First event"); log->log("Second event"); log->dump(); } * Would the code below work? int main() { Log log; log.log("First event"); log.log("Second event"); log.dump(); } * Can we invoke Log's constructor directly? * And what happens if we invoke instance() multiple times? int main() { Log* log1 = Log::instance(); Log* log2 = Log::instance(); log1->log("First event"); log2->log("Second event"); log1->dump(); log2->dump(); } * Would it be possible to replace _instance with a static variable within the method instance()? class Log { public: static Log* instance() { static Log *log = new Log(); return log; } void log(std::string msg) { _entries.push_back(msg); } void dump() const { for (unsigned i = 0; i < _entries.size(); i++) { std::cout << i << ": " << _entries[i] << std::endl; } } private: std::vector _entries; Log() {} }; * Can you subclass a singleton? * In which situations would you like to subclass our log system? * How do you have to change the singleton to be able to subclass it? class Log { public: // ... virtual ~Log() {} virtual void log(std::string msg) { _entries.push_back(msg); } // ... protected: Log() {} }; * Imagine that you want to add the date when an event is logged. Can you subclass the singleton to do it? * Let's implement this subclass. First, how can you get the current date and time in C++ in a portable way? #include #include // ... char* getDate() const { // We use auto to avoid having to write: // std::chrono::time_point auto now = std::chrono::system_clock::now(); std::time_t end_time = std::chrono::system_clock::to_time_t(now); return std::ctime(&end_time); } * Now, can you use the code above to create your subclass of singleton? class DateLog: public Log { public: static Log* instance() { static DateLog *log = new DateLog(); return log; } void log(std::string msg) { std::string newMsg = getDate() + msg; Log::log(newMsg); } void dump() const { for (unsigned i = 0; i < _entries.size(); i++) { std::cout << i << ": " << _entries[i] << std::endl; } } private: std::vector _entries; char* getDate() const { // We use auto to avoid having to write: // std::chrono::time_point auto now = std::chrono::system_clock::now(); std::time_t end_time = std::chrono::system_clock::to_time_t(now); return std::ctime(&end_time); } }; * Do you have to change the client code to use it? int main() { Log* log1 = DateLog::instance(); Log* log2 = DateLog::instance(); log1->log("First event"); log2->log("Second event"); log1->dump(); log2->dump(); } * Do you think the method Log::instance(), i.e., the instance() method of the Log class is invoked, when we write: "Log* log1 = DateLog::instance();"? * How can you test it? class Log { public: static Log* instance() { static Log *log = new Log(); std::cout << "Creating an instance of Log\n"; return log; } // ... }; class DateLog: public Log { public: static Log* instance() { static DateLog *log = new DateLog(); std::cout << "Creating an instance of DateLog\n"; return log; } // ... }; * Actually, can we mark a static method as virtual? - error: 'virtual' can only appear on non-static member functions * The singleton pattern has one big problem. Can you see it? - It is a global variable visible across the whole program. * And what is the problem with global variables? - Complicate reasoning about the program, because they make it harder to find all the inputs that might change the behavior of a function. * Can you have a singleton without implementing it like a global variable? #include #include #include class Log { public: Log() { assert(!_instantiated); _instantiated = true; } virtual ~Log() { _instantiated = false; } void log(std::string msg) { _entries.push_back(msg); } void dump() const { for (unsigned i = 0; i < _entries.size(); i++) { std::cout << i << ": " << _entries[i] << std::endl; } } private: std::vector _entries; static bool _instantiated; }; bool Log::_instantiated = false; * Why do we need to make _instantiated static? * What does it mean this code snippet? "bool Log::_instantiated = false;" * How can you use this new version of our singleton? int main() { Log* log = new Log(); log->log("First event"); log->log("Second event"); log->dump(); } * What if you call it multiple times? int main() { Log* log1 = new Log(); Log* log2 = new Log(); log1->log("First event"); log2->log("Second event"); log1->dump(); log2->dump(); } * We are using an assertion in the code above. Why not an exception? - Assertions are better for bug finding: they give you the line, and once the code goes to production, they can be disabled. * How can you call the constructor multiple times? int main() { Log* log1 = new Log(); log1->log("First event"); log1->dump(); delete log1; Log* log2 = new Log(); log2->log("Second event"); log2->dump(); delete log2; } * That's a bit annoying: we are not keeping the logs from one instance to the other. Is it possible to do it? #include #include #include class Log { public: Log() { assert(!_instantiated); _instantiated = true; } virtual ~Log() { _instantiated = false; } void log(std::string msg) { _entries.push_back(msg); } void dump() const { for (unsigned i = 0; i < _entries.size(); i++) { std::cout << i << ": " << _entries[i] << std::endl; } } private: static std::vector _entries; static bool _instantiated; }; // The definition of the static fields: std::vector Log::_entries; bool Log::_instantiated = false; // And example of code that uses our log system: int main() { Log* log1 = new Log(); log1->log("First event"); log1->dump(); delete log1; Log* log2 = new Log(); log2->log("Second event"); log2->dump(); delete log2; } * But now the log will exist throughout the whole existence of the program. How can we clear it up? class Log { // ... void flush() { _entries.clear(); } // ... }; === Command === * Let's imagine that we want to record in a file all the messages in the log that contain the string "Error". Let's try some TDD. Can you start writing a test for this method? TEST_CASE("Testing the log system") { Log* log = new Log(); log->log("Msg 1"); log->log("Error: Msg 2"); log->log("Msg 3"); log->log("123"); log->log("Msg 4 (Error)"); log->log("564"); log->recordError(); std::ifstream file("ErrorMsgs.txt"); int count = 0; std::string line; while (std::getline(file, line)) { count++; } CHECK(count == 2); delete log; } * Now, can you write a function that passes this test? #include void recordError() const { std::ofstream error_file; error_file.open("ErrorMsgs.txt"); for (std::string msg: _entries) { if (msg.find("Error") < msg.size()) { error_file << msg << std::endl; } } error_file.close(); } * How does std::string::find() works? * Let's now imagine that we want to count all the strings that are larger than a certain size, say, N. Can you add a subtest to our test? TEST_CASE("Testing the log system") { Log* log = new Log(); log->log("Msg 1"); log->log("Error: Msg 2"); log->log("Msg 3"); log->log("123"); log->log("Msg 4 (Error)"); log->log("564"); SUBCASE("Testing the recordError method") { log->recordError(); std::ifstream file("ErrorMsgs.txt"); int count = 0; std::string line; while (std::getline(file, line)) { count++; } CHECK(count == 2); } SUBCASE("Testing the countLargeStrings method") { CHECK(log->countLargeStrings(6) == 2); } log->flush(); delete log; } * Now, write code that passes this test. unsigned countLargeStrings(unsigned N) const { unsigned count = 0; for (std::string msg: _entries) { if (msg.size() > N) { count++; } } return count; } * Now, let's imagine that we want to filter out all the messages that encode natural numbers. Can you add one more subtest to handle that? TEST_CASE("Testing the log system") { Log* log = new Log(); log->log("Msg 1"); log->log("Error: Msg 2"); log->log("Msg 3"); log->log("123"); log->log("Msg 4 (Error)"); log->log("564"); SUBCASE("Testing the recordError method") { log->recordError(); std::ifstream file("ErrorMsgs.txt"); int count = 0; std::string line; while (std::getline(file, line)) { count++; } CHECK(count == 2); } SUBCASE("Testing the countLargeStrings method") { CHECK(log->countLargeStrings(6) == 2); } SUBCASE("Testing the method that filters out naturals") { std::vector naturals; log->filterNaturals(naturals); CHECK(naturals.size() == 2); } log->flush(); delete log; } * Let's write code that passes this test. First, how can you know that a string represents a natural number? Write a test case for a function that does it. TEST_CASE("Testing is_natural") { CHECK(is_natural("123") == true); CHECK(is_natural("0") == true); CHECK(is_natural("1000000") == true); CHECK(is_natural("1000000a") == false); CHECK(is_natural("a1000000") == false); CHECK(is_natural("1000a000") == false); } * Now, write a function that passes this test. bool is_natural(const std::string& s) { unsigned num_digits = 0; for (char c: s) { if (std::isdigit(c)) { num_digits++; } } return num_digits == s.size(); } * Can you use the function above to implement your filterNaturals method? void filterNaturals(std::vector& nums) const { for (std::string msg: _entries) { if (is_natural(msg)) { nums.push_back(std::stoi(msg)); } } } * Let's take a look into the Log system. How does it look like now? class Log { public: Log() { assert(!_instantiated); _instantiated = true; } virtual ~Log() { _instantiated = false; } void log(std::string msg) { _entries.push_back(msg); } void dump() const { for (unsigned i = 0; i < _entries.size(); i++) { std::cout << i << ": " << _entries[i] << std::endl; } } void flush() { _entries.clear(); } void filterNaturals(std::vector& nums) const { for (std::string msg: _entries) { if (is_natural(msg)) { nums.push_back(std::stoi(msg)); } } } unsigned countLargeStrings(unsigned N) const { unsigned count = 0; for (std::string msg: _entries) { if (msg.size() > N) { count++; } } return count; } void recordError() const { std::ofstream error_file; error_file.open("ErrorMsgs.txt"); for (std::string msg: _entries) { if (msg.find("Error") < msg.size()) { error_file << msg << std::endl; } } error_file.close(); } private: static std::vector _entries; static bool _instantiated; }; std::vector Log::_entries; bool Log::_instantiated = false; * That's not a good design, is it? What are its problems? - The single responsibility principle: we are adding multiple methods to the log system, but these methods are not really related to logging. * Take a look into the three methods that we've added to the log system. What do they have in commong? - They all follow this structure: for every message M: if (check_condition(M) == true) execute_action(M) * Can you use this observation to factor our the commonalities of these methods into a single routine? * Let's start by removing the methods from Log, and making them into independent functions. How would be the new tests? TEST_CASE("Testing the log system") { Log* log = new Log(); log->log("Msg 1"); log->log("Error: Msg 2"); log->log("Msg 3"); log->log("123"); log->log("Msg 4 (Error)"); log->log("564"); SUBCASE("Testing the recordError method") { recordError(log); std::ifstream file("ErrorMsgs.txt"); int count = 0; std::string line; while (std::getline(file, line)) { count++; } CHECK(count == 2); } SUBCASE("Testing the countLargeStrings method") { CHECK(countLargeStrings(log, 6) == 2); } SUBCASE("Testing the method that filters out naturals") { std::vector naturals; filterNaturals(log, naturals); CHECK(naturals.size() == 2); } log->flush(); delete log; } * Can you add signatures to these functions, so that at least our program compiles? void filterNaturals(Log *log, std::vector& nums) { } unsigned countLargeStrings(Log *log, unsigned N) { unsigned count = 0; return count; } void recordError(Log *log) { } * Ok, now, let's discuss how to implement these functions. First, we need to add functionality to the Log class to implement the general pattern of the three methods that we are factoring out. class Log { public: void process(Command* cmd) { for (std::string msg: _entries) { if (cmd->check_condition(msg)) { cmd->execute_action(msg); } } } }; * This method, 'process' receives an instance of Command. What is a command? - check_condition - execute_action * Can you implement this class? class Command { public: virtual ~Command() {} virtual bool check_condition(std::string &msg) const = 0; virtual void execute_action(std::string &msg) = 0; }; * Now, our program compiles, but we can't pass any test. Can you implement a command that passes the record error subcase of our test? class CmdError: public Command { public: CmdError() { error_file.open("ErrorMsgs.txt"); } ~CmdError() { error_file.close(); } bool check_condition(std::string &msg) const { return msg.find("Error") < msg.size(); } void execute_action(std::string &msg) { error_file << msg << std::endl; } private: std::ofstream error_file; }; void recordError(Log *log) { Command* cmd = new CmdError(); log->process(cmd); delete cmd; } * Can you do something similar to implement countLargeStrings? class CmdLargeStrings: public Command { public: CmdLargeStrings(unsigned N): _count(0), _N(N) { } bool check_condition(std::string &msg) const { return msg.size() > _N; } void execute_action(std::string &msg) { _count++; } unsigned getCount() const { return _count; } private: const unsigned _N; unsigned _count; }; unsigned countLargeStrings(Log *log, unsigned N) { CmdLargeStrings *cmd = new CmdLargeStrings(N); log->process(cmd); unsigned count = cmd->getCount(); delete cmd; return count; } * And filterNaturals, how would you implement it? class CmdFilterNaturals: public Command { public: CmdFilterNaturals(std::vector& nums): _nums(nums) { } bool check_condition(std::string &msg) const { return is_natural(msg); } void execute_action(std::string &msg) { _nums.push_back(std::stoi(msg)); } private: std::vector& _nums; }; void filterNaturals(Log *log, std::vector& nums) { Command *cmd = new CmdFilterNaturals(nums); log->process(cmd); } * Why we could use the most generic type Command in filterNaturals and in recordError, but had to use the specific type in countLargeStrings? E.g.: unsigned countLargeStrings(Log *log, unsigned N) { CmdLargeStrings *cmd = new CmdLargeStrings(N); //... }