NTOU CSE C++ Programming
教學文件和作業說明文件: https://hackmd.io/@kogiokka/ntou-cse-cpp-nav
本次實習主題是程式設計中的配接器模式(Adapter Pattern)。還有練習 C++ 的繼承(inheritance)、虛擬函式(virtual functions)和抽象類別(abstract class),以及 std::map
的用法。
配接器模式是一種設計模式(Design Pattern),讓介面不相容的物件能夠溝通。
如下圖,客戶(client)有一個固定存取服務(service)的方式(interface)。如果今天客戶想新增一個服務,但這個服務沒有提供客戶要求的方法,配接器(adapter)就可以為客戶提供跟服務溝通的管道。
舉一個簡單的例子,一開始程式中有兩個類別 RoundHole
和 RoundPeg
,分別代表圓孔和圓木樁。RoundHole
類別的物件有一個函式 fit
,接受 RoundPeg
物件並透過getRadius
函式得知圓木樁的半徑,藉此來知道木樁是不是能放進圓孔中。
現在多了一個類別 SquarePeg
代表方木樁。如果我們想知道一個方木樁是不是也能放進圓孔,就必須要讓圓孔也知道它的「半徑」。然而,放一個 getRadius
函式在 SquarePeg
並不怎麼合理,讓圓孔為每一種型狀的木樁定義一個 fit
函式也不理想(必須把轉換的程式碼放在裡面)。這時 adapter 就派上用場了,將轉接的部分與主要的程式邏輯(fit
函式)切開來。多寫一個 Adapter
類別的好處是不必為每次新增一種形狀木樁,就去動圓孔甚至是木樁的程式碼。
上述三個類別的關係如下圖,SquarePegAdapter
就是配接器:
在這個例子中,圓孔是 Client;木樁是 Service,要負責提供圓孔「告知半徑資訊」的服務。Client 存取 Service 的方式是客戶端介面,Client Interface,就是我們的getRadius
函式。所有對客戶提供服務的類別都要符合 Client Interface 的規範。方木樁沒有 getRadius
函式,客戶必須經由 Adapter 存取它的服務(半徑資訊),所以方木樁是我們的 Adaptee,被配接的人。
Adaptee
、AdapterClass
和 AdapterObject
。不是使用 Visual Studio 嗎?請見 CMake專案建置。
在 Visual Studio 選擇開啟本機資料夾或檔案 → 開啟 → 資料夾,並選擇AdapterPattern/
資料夾。Visual Studio 會自動開始設定 CMake 專案。等待輸出欄位的訊息跑完之後,在上方工具列的選取啟動項目選adapter.exe
就可以自動編譯執行了。
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_minimum_required(VERSION 3.12)
project("Adapter Pattern")
# 執行 "Source/" 資料夾中的 CMakeLists.txt
add_subdirectory("Source/")
Source/Book.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
#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
#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
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
#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
#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
#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
#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
#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
#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
#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_