C++高級(jí)編程:構(gòu)建高效穩(wěn)定接口與深入對(duì)象設(shè)計(jì)技巧
一、建立穩(wěn)定接口
類是C++中的主要抽象單位。你應(yīng)該將抽象原則應(yīng)用于你的類,盡可能將接口與實(shí)現(xiàn)分離。具體來(lái)說(shuō),你應(yīng)該使所有數(shù)據(jù)成員私有,并可選擇性地提供getter和setter方法。這就是SpreadsheetCell類的實(shí)現(xiàn)方式:m_value是私有的,而公共的set()方法設(shè)置值,getValue()和getString()方法檢索值。
1.使用接口和實(shí)現(xiàn)類
即便采取了上述措施和最佳設(shè)計(jì)原則,C++語(yǔ)言本質(zhì)上對(duì)抽象原則不友好。其語(yǔ)法要求你將公共接口和私有(或受保護(hù)的)數(shù)據(jù)成員及方法組合在一個(gè)類定義中,從而將類的一些內(nèi)部實(shí)現(xiàn)細(xì)節(jié)暴露給其客戶端。這樣做的缺點(diǎn)是,如果你需要在類中添加新的非公開(kāi)方法或數(shù)據(jù)成員,所有使用該類的客戶端都必須重新編譯。這在大型項(xiàng)目中可能成為負(fù)擔(dān)。
好消息是你可以讓你的接口更加干凈,并隱藏所有實(shí)現(xiàn)細(xì)節(jié),從而實(shí)現(xiàn)穩(wěn)定的接口。壞消息是這需要一些編碼工作。基本原則是為你想編寫(xiě)的每個(gè)類定義兩個(gè)類:接口類和實(shí)現(xiàn)類。實(shí)現(xiàn)類與你在不采取此方法時(shí)編寫(xiě)的類相同。接口類提供與實(shí)現(xiàn)類相同的公共方法,但它只有一個(gè)數(shù)據(jù)成員:指向?qū)崿F(xiàn)類對(duì)象的指針。這被稱為pimp習(xí)語(yǔ),私有實(shí)現(xiàn)習(xí)語(yǔ),或橋接模式。接口類的方法實(shí)現(xiàn)簡(jiǎn)單地調(diào)用實(shí)現(xiàn)類對(duì)象上的等效方法。
這樣的結(jié)果是,無(wú)論實(shí)現(xiàn)如何改變,都不會(huì)影響公共接口類。這減少了重新編譯的需要。如果實(shí)現(xiàn)(僅實(shí)現(xiàn))發(fā)生變化,使用接口類的客戶端無(wú)需重新編譯。請(qǐng)注意,這種習(xí)語(yǔ)僅在單一數(shù)據(jù)成員是指向?qū)崿F(xiàn)類的指針時(shí)才有效。如果它是按值數(shù)據(jù)成員,則在實(shí)現(xiàn)類定義發(fā)生變化時(shí),客戶端必須重新編譯。
要在Spreadsheet類中使用此方法,請(qǐng)定義以下公共接口類,稱為Spreadsheet。
module;
#include <cstddef>
export module spreadsheet;
export import spreadsheet_cell;
import <memory>;
export class SpreadsheetApplication { };
export class Spreadsheet {
public:
Spreadsheet(const SpreadsheetApplication& theApp, size_t width = MaxWidth, size_t height = MaxHeight);
Spreadsheet(const Spreadsheet& src);
Spreadsheet(Spreadsheet&&) noexcept;
~Spreadsheet();
Spreadsheet& operator=(const Spreadsheet& rhs);
Spreadsheet& operator=(Spreadsheet&&) noexcept;
void setCellAt(size_t x, size_t y, const SpreadsheetCell& cell);
SpreadsheetCell& getCellAt(size_t x, size_t y);
size_t getId() const;
static const size_t MaxHeight { 100 };
static const size_t MaxWidth { 100 };
void swap(Spreadsheet& other) noexcept;
private:
class Impl;
std::unique_ptr<Impl> m_impl;
};
export void swap(Spreadsheet& first, Spreadsheet& second) noexcept;
實(shí)現(xiàn)類Impl是一個(gè)私有嵌套類,因?yàn)槌薙preadsheet類之外,沒(méi)有人需要了解這個(gè)實(shí)現(xiàn)類。現(xiàn)在,Spreadsheet類只包含一個(gè)數(shù)據(jù)成員:指向Impl實(shí)例的指針。公共方法與舊的Spreadsheet類相同。
2.掌握類和對(duì)象
嵌套的Spreadsheet::Impl類在spreadsheet模塊的實(shí)現(xiàn)文件中定義。它應(yīng)該對(duì)客戶端隱藏,因此不導(dǎo)出Impl類。Spreadsheet.cpp模塊實(shí)現(xiàn)文件如下開(kāi)始:
module;
#include <cstddef
>
module spreadsheet;
import <utility>;
import <stdexcept>;
import <format>;
import <algorithm>;
using namespace std;
// Spreadsheet::Impl類定義。
class Spreadsheet::Impl {
/* 為簡(jiǎn)潔起見(jiàn)省略 */
};
// Spreadsheet::Impl方法定義。
Spreadsheet::Impl::Impl(const SpreadsheetApplication& theApp, size_t width, size_t height)
: m_id { ms_counter++ }
, m_width { min(width, Spreadsheet::MaxWidth) }
, m_height { min(height, Spreadsheet::MaxHeight) }
, m_theApp { theApp }
{
m_cells = new SpreadsheetCell*[m_width];
for (size_t i{ 0 }; i < m_width; i++) {
m_cells[i] = new SpreadsheetCell[m_height];
}
}
// 其他方法定義省略以簡(jiǎn)潔。
Impl類幾乎具有與原始Spreadsheet類相同的接口。對(duì)于方法實(shí)現(xiàn),需要記住Impl是一個(gè)嵌套類;因此,你需要指定作用域?yàn)镾preadsheet::Impl。所以,對(duì)于構(gòu)造函數(shù),它變成了Spreadsheet::Impl::Impl(...)。
由于Spreadsheet類具有指向?qū)崿F(xiàn)類的unique_ptr,因此Spreadsheet類需要有用戶聲明的析構(gòu)函數(shù)。由于我們不需要在此析構(gòu)函數(shù)中執(zhí)行任何操作,因此可以在實(shí)現(xiàn)文件中將其默認(rèn)為:
Spreadsheet::~Spreadsheet() = default;
事實(shí)上,它必須在實(shí)現(xiàn)文件中默認(rèn),而不是直接在類定義中。原因是Impl類僅在Spreadsheet類定義中前向聲明;也就是說(shuō),編譯器知道將會(huì)有一個(gè)Spreadsheet::Impl類出現(xiàn)在某處,但此時(shí)它還不知道定義。因此,你不能在類定義中默認(rèn)析構(gòu)函數(shù),因?yàn)榫幾g器會(huì)嘗試使用尚未定義的Impl類的析構(gòu)函數(shù)。在這種情況下,對(duì)其他方法進(jìn)行默認(rèn)操作時(shí)也是如此,例如移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符。
二、實(shí)現(xiàn)Spreadsheet方法
Spreadsheet類的方法實(shí)現(xiàn),如setCellAt()和getCellAt(),只是將請(qǐng)求傳遞給底層的Impl對(duì)象:
void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell& cell) {
m_impl->setCellAt(x, y, cell);
}
SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y) {
return m_impl->getCellAt(x, y);
}
Spreadsheet的構(gòu)造函數(shù)必須構(gòu)造一個(gè)新的Impl以執(zhí)行其工作:
Spreadsheet::Spreadsheet(const SpreadsheetApplication& theApp, size_t width, size_t height) {
m_impl = make_unique<Impl>(theApp, width, height);
}
Spreadsheet::Spreadsheet(const Spreadsheet& src) {
m_impl = make_unique<Impl>(*src.m_impl);
}
拷貝構(gòu)造函數(shù)看起來(lái)有些奇怪,因?yàn)樗枰獜脑碨preadsheet復(fù)制底層的Impl。拷貝構(gòu)造函數(shù)接受一個(gè)Impl的引用,而不是指針,所以你必須解引用m_impl指針來(lái)獲取對(duì)象本身。
Spreadsheet賦值運(yùn)算符必須同樣將賦值傳遞給底層的Impl:
Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs) {
*m_impl = *rhs.m_impl;
return *this;
}
賦值運(yùn)算符中的第一行看起來(lái)有些奇怪。Spreadsheet賦值運(yùn)算符需要將調(diào)用轉(zhuǎn)發(fā)給Impl賦值運(yùn)算符,這只在你復(fù)制直接對(duì)象時(shí)運(yùn)行。通過(guò)解引用m_impl指針,你強(qiáng)制執(zhí)行直接對(duì)象賦值,這導(dǎo)致調(diào)用Impl的賦值運(yùn)算符。
swap()方法簡(jiǎn)單地交換單一數(shù)據(jù)成員:
void Spreadsheet::swap(Spreadsheet& other) noexcept {
std::swap(m_impl, other.m_impl);
}
這種技術(shù)將接口與實(shí)現(xiàn)真正分離,是非常強(qiáng)大的。雖然一開(kāi)始有些笨拙,但一旦習(xí)慣了,你會(huì)發(fā)現(xiàn)它很自然易用。然而,在大多數(shù)工作環(huán)境中,這不是常見(jiàn)做法,所以你可能會(huì)遇到同事的一些抵觸。支持這種做法的最有力論據(jù)不是分離接口的美學(xué),而是如果類的實(shí)現(xiàn)發(fā)生變化,構(gòu)建時(shí)間的加速。
三、注意
使用穩(wěn)定的接口類,可以減少構(gòu)建時(shí)間。將實(shí)現(xiàn)與接口分離的另一種方法是使用抽象接口,即只有純虛方法的接口,然后有一個(gè)實(shí)現(xiàn)該接口的實(shí)現(xiàn)類。這是下個(gè)主題。