# 實習課第十二次作業 ###### tags: `NTOU CSE C++ Programming` > 教學文件和作業說明文件: https://hackmd.io/@kogiokka/ntou-cse-cpp-nav 本次實習主題是程式設計中的**配接器模式(Adapter Pattern)**。還有練習 C++ 的繼承(inheritance)、虛擬函式(virtual functions)和抽象類別(abstract class),以及 `std::map` 的用法。 ## Adapter Pattern 配接器模式是一種設計模式(Design Pattern),讓介面不相容的物件能夠溝通。 如下圖,客戶(client)有一個固定存取服務(service)的方式(interface)。如果今天客戶想新增一個服務,但這個服務沒有提供客戶要求的方法,配接器(adapter)就可以為客戶提供跟服務溝通的管道。 ![adapter object](https://refactoring.guru/images/patterns/diagrams/adapter/structure-object-adapter.png?id=33dffbe3aece294162440c7ddd3d5d4f) 舉一個簡單的例子,一開始程式中有兩個類別 `RoundHole` 和 `RoundPeg`,分別代表圓孔和圓木樁。`RoundHole` 類別的物件有一個函式 `fit`,接受 `RoundPeg`物件並透過`getRadius`函式得知圓木樁的半徑,藉此來知道木樁是不是能放進圓孔中。 現在多了一個類別 `SquarePeg` 代表方木樁。如果我們想知道一個方木樁是不是也能放進圓孔,就必須要讓圓孔也知道它的「半徑」。然而,放一個 `getRadius` 函式在 `SquarePeg` 並不怎麼合理,讓圓孔為每一種型狀的木樁定義一個 `fit` 函式也不理想(必須把轉換的程式碼放在裡面)。這時 adapter 就派上用場了,將轉接的部分與主要的程式邏輯(`fit`函式)切開來。多寫一個 `Adapter` 類別的好處是不必為每次新增一種形狀木樁,就去動圓孔甚至是木樁的程式碼。 上述三個類別的關係如下圖,`SquarePegAdapter` 就是配接器: ![adapter01](https://refactoring.guru/images/patterns/diagrams/adapter/example.png?id=9d2b6857ce256f2c669383ce4df3d0aa) 在這個例子中,圓孔是 _Client_;木樁是 _Service_,要負責提供圓孔「告知半徑資訊」的服務。_Client_ 存取 _Service_ 的方式是客戶端介面,_Client Interface_,就是我們的`getRadius`函式。所有對客戶提供服務的類別都要符合 _Client Interface_ 的規範。方木樁沒有 `getRadius` 函式,客戶必須經由 _Adapter_ 存取它的服務(半徑資訊),所以方木樁是我們的 _Adaptee_,被配接的人。 ### 參考資料 * https://refactoring.guru/design-patterns/adapter ## 需求 * 依照配接器模式(Adapter Pattern)實作範例程式所提供的三個類別:`Adaptee`、`AdapterClass` 和 `AdapterObject`。 * 通過範例程式提供的測試案例。 ## 範例程式 * [實習第十二次範例程式:AdapterPattern(Google Drive)](https://drive.google.com/drive/folders/1kcpHET9E_aPaDWlm-ZdBHEToD9LXRTrY?usp=sharing) * 專案的資料夾路徑不能有中文字元,否則用 Visual Studio 編譯 CMake 專案會出錯。 ### AdapterPattern #### 編譯和執行 > 不是使用 Visual Studio 嗎?請見 [CMake專案建置](https://hackmd.io/@kogiokka/ntou-cse-cpp-tutorial-02)。 在 Visual Studio 選擇**開啟本機資料夾**或**檔案 → 開啟 → 資料夾**,並選擇`AdapterPattern/`資料夾。Visual Studio 會自動開始設定 CMake 專案。等待輸出欄位的訊息跑完之後,在上方工具列的**選取啟動項目**選`adapter.exe` 就可以自動編譯執行了。 #### 專案目錄結構 ```txt AdapterPattern ├── CMakeLists.txt └── Source ├── Adapter │ ├── AdapterClass.cpp │ ├── AdapterClass.hpp │ ├── AdapterObject.cpp │ └── AdapterObject.hpp ├── Book.cpp ├── Book.hpp ├── ClientInterface │ └── LookUpTable.hpp ├── main.cpp └── Service ├── Adaptee.cpp └── Adaptee.hpp ``` #### 原始碼 `CMakeLists.txt` ```cmake= cmake_minimum_required(VERSION 3.12) project("Adapter Pattern") # 執行 "Source/" 資料夾中的 CMakeLists.txt add_subdirectory("Source/") ``` `Source/Book.cpp` ```cpp= #include "Book.hpp" using std::ostream; using std::string; using std::vector; Book::Book() : m_name("") , m_authors({}) , m_price(0) { } Book::Book(const string& name, const vector<string>& authors, float price) : m_name(name) , m_authors(authors) , m_price(price) { } void Book::assign(const string& name, const vector<string>& authors, float price) { m_name = name; m_authors = authors; m_price = price; } std::string Book::name() const { return m_name; } std::vector<std::string> Book::authors() const { return m_authors; } float Book::price() const { return m_price; } ``` `Source/Book.hpp` ```cpp= #ifndef BOOK_H_ #define BOOK_H_ #include <iostream> #include <string> #include <vector> class Book { friend std::ostream& operator<<(std::ostream&, const Book&); private: std::string m_name; std::vector<std::string> m_authors; float m_price; public: Book(); Book(const std::string& name, const std::vector<std::string>& authors, float price); void assign(const std::string& name, const std::vector<std::string>& authors, float price); std::string name() const; std::vector<std::string> authors() const; float price() const; }; #endif // BOOK_H_ ``` `Source/main.cpp` ```cpp= #include <iomanip> // std::quoted #include <iostream> // std::cout, std::endl #include <map> // std::map #include <sstream> // std::osringstream #include <string> // std::string #include <tuple> // std::tie, std::ignore #include <utility> // std::pair #include <vector> // std::vector #include "Adapter/AdapterClass.hpp" #include "Adapter/AdapterObject.hpp" #include "Book.hpp" #include "ClientInterface/LookUpTable.hpp" using std::cout; using std::endl; using std::map; using std::ostream; // Output stream using std::ostringstream; // Output string stream using std::pair; using std::quoted; using std::string; using std::vector; static inline void BANNER(); static inline void HEAD1(const string& text); static inline void HEAD2(const string& text); static inline void HEAD3(const string& text); static inline void END(); static inline void CUT(); ostream& operator<<(ostream& out, const vector<string>& vec); ostream& operator<<(ostream& out, const Book& book); ostream& operator<<(ostream& out, const map<string, Book>& books); void testStdMap(); void runClient(LookUpTable* ptr); int main() { BANNER(); CUT(); HEAD1("Test std::map"); testStdMap(); CUT(); HEAD1("Test class AdapterClass"); AdapterClass ac; runClient(&ac); CUT(); HEAD1("Test class AdapterObject"); AdapterObject ao; runClient(&ao); CUT(); cout << "Press enter to continue..." << endl; std::cin.get(); // Portable system("pause") } /* Test Cases */ void runClient(LookUpTable* ptr) { Book book1("C++ Primer", { "Stanley Lippman", "Josée Lajoie", "Barbara E. Moo" }, 40.94f); Book book2("Modern Fortran: Building efficient parallel applications", { "Milan Curcic" }, 59.99f); Book book3("C++ Programming Language (hardcover), The 4th Edition", { "Bjarne Stroustrup" }, 70.63f); // insert HEAD2("LookUpTable Interface: insert"); { cout << ptr->insert(book1) << endl; // Ans: 1 cout << ptr->insert(book2) << endl; // Ans: 1 cout << ptr->insert(book3) << endl; // Ans: 1 cout << ptr->insert(book1) << endl; // Ans: 0 cout << ptr->insert(book3) << endl; // Ans: 0 } END(); // remove HEAD2("LookUpTable Interface: remove"); { cout << ptr->remove(book2) << endl; // Ans: 1 cout << ptr->remove(book2) << endl; // Ans: 0 } END(); // get HEAD2("LookUpTable Interface: get"); { cout << ptr->get(book1.name()) << endl; // Ans: <memory address> cout << ptr->get(book2.name()) << endl; // Ans: 0 cout << ptr->get(book3.name()) << endl; // Ans: <memory address> } END(); } void testStdMap() { // Key: std::string, Value: Book map<string, Book> mapbook; // insert HEAD2("Test Map: Insert"); { string key = "C++ Primer"; Book val("C++ Primer", { "Stanley Lippman", "Josée Lajoie", "Barbara E. Moo" }, 40.94f); bool success = false; std::tie(std::ignore, success) = mapbook.insert(pair<string, Book>(key, val)); if (success) { cout << "Inserted " << val << " into the map." << endl; } key = "Modern Fortran: Building efficient parallel applications"; val.assign(key, { "Milan Curcic" }, 59.99f); std::tie(std::ignore, success) = mapbook.emplace(key, val); if (success) { cout << "Inserted " << val << " into the map." << endl; } // Bjarne Stroustrup is a Danish computer scientist, most notable for the // invention and development of the C++ programming language. key = "C++ Programming Language (hardcover), The 4th Edition"; val.assign(key, { "Bjarne Stroustrup" }, 70.63f); std::tie(std::ignore, success) = mapbook.emplace(key, val); if (success) { cout << "Inserted " << val << " into the map." << endl; } } END(); HEAD2("Test Map: Print"); { HEAD3("Key-Value"); for (auto it = mapbook.begin(); it != mapbook.end(); it++) { cout << "" << quoted(it->first) << " -> " << it->second << endl; } END(); HEAD3("JSON"); cout << mapbook << endl; END(); } END(); HEAD2("Test Map: Find"); { string key = "Modern Fortran: Building efficient parallel applications"; if (mapbook.find(key) == mapbook.end()) { cout << "Could not find " << quoted(key) << "." << endl; } else { cout << "Found " << quoted(key) << "." << endl; } key = "Modern Fortran"; if (mapbook.find(key) == mapbook.end()) { cout << "Could not find " << quoted(key) << "." << endl; } else { cout << "Found " << quoted(key) << "." << endl; } } END(); HEAD2("Test Map: Find and Erase"); { string key = "Modern Fortran: Building efficient parallel applications"; auto it = mapbook.find("Modern Fortran: Building efficient parallel applications"); if (it == mapbook.end()) { cout << "Could not find " << quoted(key) << "." << endl; } else { cout << "Erased " << quoted(key) << "." << endl; mapbook.erase(it); } key = "Modern Fortran: Building efficient parallel applications"; it = mapbook.find(key); if (it == mapbook.end()) { cout << "Could not find " << quoted(key) << "." << endl; } else { cout << "Erased " << quoted(key) << "." << endl; mapbook.erase(it); } } END(); cout << endl; } ostream& operator<<(ostream& out, const vector<string>& vec) { ostringstream vectorStream; string separator = ""; vectorStream << "["; for (auto it = vec.begin(); it != vec.end(); ++it) { vectorStream << separator << quoted(*it); separator = ", "; } vectorStream << "]"; return (out << vectorStream.str()); } ostream& operator<<(ostream& out, const Book& book) { ostringstream bookStream; bookStream << "{" << quoted(book.m_name) << ", " << book.m_authors << ", " << book.m_price << "}"; return (out << bookStream.str()); } ostream& operator<<(ostream& out, const map<string, Book>& books) { ostringstream booksJSON; string separator = ""; const string indent = " "; booksJSON << "{\n"; for (auto it = books.begin(); it != books.end(); ++it) { booksJSON << separator << indent << quoted(it->first) << ": {\n" << indent << indent << quoted("name") << ": " << quoted(it->second.name()) << ",\n" << indent << indent << quoted("authors") << ": " << it->second.authors() << ",\n" << indent << indent << quoted("price") << ": " << it->second.price() << "\n" << indent << "}"; separator = ",\n"; } booksJSON << "\n}" << endl; return (out << booksJSON.str()); } void BANNER() { cout << R"AdapterPattern( ________ ______ ________ ______ _________ ______ ______ /_______/\ /_____/\ /_______/\ /_____/\ /________/\/_____/\ /_____/\ \::: _ \ \\:::_ \ \\::: _ \ \\:::_ \ \\__.::.__\/\::::_\/_\:::_ \ \ \::(_) \ \\:\ \ \ \\::(_) \ \\:(_) \ \ \::\ \ \:\/___/\\:(_) ) )_ \:: __ \ \\:\ \ \ \\:: __ \ \\: ___\/ \::\ \ \::___\/_\: __ `\ \ \:.\ \ \ \\:\/.:| |\:.\ \ \ \\ \ \ \::\ \ \:\____/\\ \ `\ \ \ \__\/\__\/ \____/_/ \__\/\__\/ \_\/ \__\/ \_____\/ \_\/ \_\/ ______ ________ _________ _________ ______ ______ ___ __ /_____/\ /_______/\ /________/\/________/\/_____/\ /_____/\ /__/\ /__/\ \:::_ \ \\::: _ \ \\__.::.__\/\__.::.__\/\::::_\/_\:::_ \ \ \::\_\\ \ \ \:(_) \ \\::(_) \ \ \::\ \ \::\ \ \:\/___/\\:(_) ) )_\:. `-\ \ \ \: ___\/ \:: __ \ \ \::\ \ \::\ \ \::___\/_\: __ `\ \\:. _ \ \ \ \ \ \:.\ \ \ \ \::\ \ \::\ \ \:\____/\\ \ `\ \ \\. \`-\ \ \ \_\/ \__\/\__\/ \__\/ \__\/ \_____\/ \_\/ \_\/ \__\/ \__\/ )AdapterPattern"; } void HEAD1(const string& text) { string tildes(text.length(), '~'); cout << tildes << "\n"; cout << text << "\n"; cout << tildes << "\n\n"; } static inline void _HEAD_INTERNAL(const string& text, char ch) { string line(text.length(), ch); cout << text << "\n"; cout << line << "\n\n"; } void HEAD2(const string& text) { _HEAD_INTERNAL(text, '^'); } void HEAD3(const string& text) { _HEAD_INTERNAL(text, '*'); } void END() { cout << endl; } void CUT() { string line(80, '='); cout << line << "\n" << endl; } ``` `Source/CMakeLists.txt` ```cmake= add_executable(adapter) # 新增 include 路徑 # #include 指令中的相對路徑會從專案根目錄之下的 "Source" 資料夾開始計算 target_include_directories(adapter PRIVATE "${CMAKE_SOURCE_DIR}/Source") target_sources(adapter PRIVATE "Adapter/AdapterClass.cpp" "Adapter/AdapterObject.cpp" "Book.cpp" "Service/Adaptee.cpp" "main.cpp" ) ``` `Source/Adapter/AdapterClass.cpp` ```cpp= #include "Adapter/AdapterClass.hpp" using std::string; int AdapterClass::insert(const Book& book) { // TODO Call methods of class Adaptee return 0; } int AdapterClass::remove(const Book& book) { // TODO Call methods of class Adaptee return 0; } Book* AdapterClass::get(const string& bookName) { // TODO Call methods of class Adaptee return nullptr; } ``` `Source/Adapter/AdapterClass.hpp` ```cpp= #ifndef ADAPTERCLASS_H_ #define ADAPTERCLASS_H_ #include "Book.hpp" #include "ClientInterface/LookUpTable.hpp" #include "Service/Adaptee.hpp" class AdapterClass : public LookUpTable, public Adaptee { public: int insert(const Book& book) override; int remove(const Book& book) override; Book* get(const std::string& bookName) override; }; #endif // ADAPTERCLASS_H_ ``` `Source/Adapter/AdapterObject.cpp` ```cpp= #include "Adapter/AdapterObject.hpp" using std::string; int AdapterObject::insert(const Book& book) { // TODO Call methods of class Adaptee return 0; } int AdapterObject::remove(const Book& book) { // TODO Call methods of class Adaptee return 0; } Book* AdapterObject::get(const string& bookName) { // TODO Call methods of class Adaptee return nullptr; } ``` `Source/Adapter/AdapterObject.hpp` ```cpp= #ifndef ADAPTEROBJECT_H_ #define ADAPTEROBJECT_H_ #include <string> #include "Book.hpp" #include "ClientInterface/LookUpTable.hpp" #include "Service/Adaptee.hpp" class AdapterObject : public LookUpTable { private: Adaptee ad; public: int insert(const Book& book) override; int remove(const Book& book) override; Book* get(const std::string& bookName) override; }; #endif // ADAPTEROBJECT_H_ ``` `Source/ClientInterface/LookUpTable.hpp` ```cpp= #ifndef LOOKUPTABLE_H_ #define LOOKUPTABLE_H_ #include <string> #include "Book.hpp" // Target Class class LookUpTable { public: /** 加入一個值。成功回傳 1;失敗回傳 0。 */ virtual int insert(const Book&) = 0; /** 刪除一個值。成功回傳 1;失敗回傳 0。 */ virtual int remove(const Book&) = 0; /** 查詢一個值 - 成功:回傳一個 Book 指標指向查詢到的 Book。 - 失敗:回傳 nullptr。 */ virtual Book* get(const std::string&) = 0; }; #endif // LOOKUPTABLE_H_ ``` `Source/Service/Adaptee.cpp` ```cpp= #include "Service/Adaptee.hpp" // void Adaptee::insert(const Book& book, int& result) {} // void Adaptee::remove(const Book& book, int& result) {} // void Adaptee::get(const std::string& bookName, Book*& ptr) {} ``` `Source/Service/Adaptee.hpp` ```cpp= #ifndef ADAPTEE_H_ #define ADAPTEE_H_ #include <map> #include <string> #include "Book.hpp" class Adaptee { private: std::map<std::string, Book> mapBook; public: /** 加入一個值 成功 result 設成 1;失敗 result 設成 0。 */ void insert(const Book& boo, int& result); /** 刪除一個值 成功 result 設成 1;失敗 result 設成 0。 */ void remove(const Book& book, int& result); /** 查詢一個值 - 成功:指標 ptr 指到 table 的值。 - 失敗:指標 ptr 設成 0。 */ void get(const std::string& bookName, Book*& ptr); }; #endif // ADAPTEE_H_ ```