过去我经常旅行,旅行适配器让我可以将欧洲插头插入英国或者美国的插座,这与适配器(Adapter)模式非常相似:我们想根据已有的接口得到另一个不同的接口,在接口上构建一个适配器就可以达到此目的。
考虑一个简单的例子:假设我们正在使用一个专用绘制像素的库。另外,我们要处理一些几何对象—直线、矩形等。现在,我们要继续使用这些几何对象并渲染它们,因此需要将几何对象与基于像素表示的对象进行适配处理。
首先,我们定义两个简单的对象:Point类表示笛卡尔空间中的二维坐标(对应屏幕中的网格),Line类表示表示由起止坐标表示的线段。
struct Point {
int x;
int y;
};
struct Line
{
Point start;
Point end;
};
现在从理论角度来说明向量几何。典型的向量对象可能由一组线段对象定义。我们定义一对纯虚迭代器接口,而不是继承vetctor<Line>:
struct VectorObject
{
virtual std::vector<Line>::iterator begin() = 0;
virtual std::vector<Line>::iterator end() = 0;
};
现在,假设要定义Rectangle,只需要将描述矩形的4条边的线段存入vector<Line>类型的成员中即可:
struct VectorRectangle : VectorObject {
/*
*(0,y+h)---------(x+w, y+h)
* | |
* | |
* (x,y) ---------(x+w, y)
*/
VectorRectangle(int x, int y, int width, int height) {
lines.emplace_back(Line{Point{x, y}, Point{x + width, y}});
lines.emplace_back(Line{Point{x + width, y}, Point{x + width, y + height}});
lines.emplace_back(Line{Point{x + width, y + height}, Point{x, y + height}});
lines.emplace_back(Line{Point{x, y + height}, Point{x, y}});
}
std::vector<Line>::iterator begin() override {
return lines.begin();
}
std::vector<Line>::iterator end() override {
return lines.end();
}
private:
std::vector<Line> lines;
};
现在,假设我们想在屏幕上画线段,甚至是画矩形!不幸的是,目前我们还做不到,因为用于绘制的唯一接口实际上是:
void DrawPoints(CPaintDC& dc, std::vector<Point>::iterator start,
std::vector<Point>::iterator end) {
for (auto i = start; i != end; i++) {
dc.SetPixel(i->x, i->y, 0);
}
}
上面的代码中,我们使用的是MFC(Microsoft Foundation Class)中的CPaintDC类,其中Setpixel()方法用于在指定坐标位置设定指定的像素值(在上面的示例中,0代表黑色)。
简而言之,这个实力遇到的问题是,我们需要提供像素坐标以渲染图像,但是我们只有一些向量对象。
假设我们要绘制一系列矩形:
std::vector<std::shared_ptr<VectorObject>> vectorObjects {
std::make_shared<VectorRectangle>(10, 10, 100, 100),
std::make_shared<VectorRectangle>(30, 30, 60, 60)
};
为了绘制这些对象,我们需要将每个举行从一组线段转化为数量庞大的像素点。为此,我们定义一个单独的适配器类,用于存储这些像素点,并且定义一组迭代器来访问这些点。
struct LineToPointAdapter {
typedef std::vector<Point> Points;
LineToPointAdapter(const Line& line) {
// TODO
}
virtual Points::iterator begin() { return points.begin();}
virtual Points::iterator end() { return points.end();}
private:
Points points;
};
将Line对象转换为像素点的过程由构造函数完成,所以LineToPointAdapter是饿汉式的适配器。在适配器对象构建的过程中,转化工作随之完成。实际的转化代码相当简单:
LineToPointAdapter(const Line& line) {
int left = std::min(line.start.x, line.end.x);
int right = std::max(line.start.x, line.end.x);
int top = std::min(line.start.y, line.end.y);
int bottom = std::max(line.start.y, line.end.y);
int dx = right - left;
int dy = line.end.y - line.start.y;
// we only support vertical or horizontal lines
if (dx == 0) { // vertical line
for (int y = top; y <= bottom; ++y) {
points.emplace_back(Point{left,y});
}
} else if (dy == 0) { // horizontal lines
for (int x = left; x <= right; ++x) {
points.emplace_back(Point{left,x});
}
}
}
这部分代码很简单:我们只处理垂直或水平的线段,忽略其他类型的线段。不论是垂直线段还是水平线段,我们都构造一个由连续相邻的点组成的即和来代表用像素点表示的线段。我们避免了对角线线段以及与平滑表示这些线段相关的问题(例如,反走样)。
现在,以之前定义的矩形为例,我们传入两个矩形对象,使用这个适配器来渲染几何对象:
void testAdapter6_2() {
std::vector<std::shared_ptr<VectorObject>> vectorObjects{
std::make_shared<VectorRectangle>(10, 10, 100, 100),
std::make_shared<VectorRectangle>(30, 30, 60, 60)};
CPaintDC dc;
for (const auto &obj: vectorObjects) {
for (const auto& line: *obj) {
LineToPointAdapter lpo{line};
DrawPoints(dc, lpo.begin(), lpo.end());
}
}
}
上述代码实现了如下工作:
上述代码存在一个主要问题:每次刷新屏幕时,函数DrawPoints()都会被调用,这意味着适配器对象会不断地为同样的线段生成相同的像素点数据,甚至是无数次!怎样改善这个问题呢?
一种办法是在程序的开始定义处定义一个像素点容器,例如:
vector<Point> points;
for (auto& o: vectorObject) {
for (auto& l: *o) {
LineToPointAdapter lpo{l};
for (auto& p : lpo) {
points.push_back(p);
}
}
}
然后,将DrawPoints()接口的实现简化为:
DrawPoints(dc, points.begin(), points.end());
但是,假如在某个时候,原始的几何对象vectorObjects发生了变化。我们并不知道它们发生了怎样的变化。但我们的确想缓存未改动的数据,而仅仅只为变化了的对象重新生成像素点数据。
首先,为了避免重新生成数据,我们需要独特的识别线段的方法,这意味着我们需要独特的识别点的方法。此时,ReSharper的Generate | Hash函数可以派上用场:
struct Point {
int x, y;
friend size_t hash_value(const Point& obj) {
size_t seed = 0x725C686F;
// boost
boost::hash_combine(seed, obj.x);
boost::hash_combine(seed, obj.y);
return seed;
}
};
struct Line {
Point start, end;
friend size_t hash_value(const Line& obj) {
size_t seed = 0x719E6B16;
// boost
boost::hash_combine(seed, obj.start);
boost::hash_combine(seed, obj.end);
return seed;
}
};
这里选择了Boost的hash实现。现在,我们可以构建一个新的LineToPointCachingAdapter,它可以缓存Point对象并在必要的时候生成它们。除了以下细微差别外,实现几乎相同。
首先,LineToPointCachingAdapter有一个缓存cach,它是一种哈希值到点集的映射,可存储值和对应的点集合:
static map<size_t, Point> cache;
类型size_t正好是Boost的hash函数返回的类型。现在,当迭代器访问生成的像素点集时,我们将以如下的方式返回被访问的对象:
virtual Points::iterator begin() { return cache[line_hash].begin();}
virtual Points::iterator end() { return cache[line_hash].end();}
这个算法有趣的地方在于:在生成像素点集之前,先检查这些像素点是否已经生成。如果已经生成,那么函数直接退出;如果没有生成,则算法生成像素点集,并将其保留在缓存cach中:
LineToPointCachingAdapter(Line &line)
{
typedef std::vector<Point> Points;
static std::hash<Line> hash;
line_hash = hash(line);
if (cache.find(line_hash) != cache.end())
{
return; // we alaready have it
}
Points points;
// same code as before
int left = std::min(line.start.x, line.end.x);
int right = std::max(line.start.x, line.end.x);
int top = std::min(line.start.y, line.end.y);
int bottom = std::max(line.start.y, line.end.y);
int dx = right - left;
int dy = line.end.y - line.start.y;
// we only support vertical or horizontal lines
if (dx == 0)
{ // vertical line
for (int y = top; y <= bottom; ++y)
{
points.emplace_back(Point{left, y});
}
}
else if (dy == 0)
{ // horizontal lines
for (int x = left; x <= right; ++x)
{
points.emplace_back(Point{left, x});
}
}
cache[line_hash] = points;
}
有了hash函数和缓存cache的帮助,我们可以显著减少转换次数。现在唯一的问题是,当不再需要某些像素点时,如何将它们移除?
【注】
1、关于如何移除,我的理解时在迭代器类析够的时候,释放对应的点集。
2、上面的代码用的是Boot库中hash_combine的方法生成哈希值。但是我们能否可以用C++标准库实现呢?答案是肯定的。
boost::hash_combine 是 Boost 库中的一个函数,用于结合多个哈希值以生成一个新的哈希值。这在编写自定义哈希函数时非常有用,特别是在需要将多个数据结合成单个哈希值的情况下。通常用于自定义哈希函数的实现中,以便在将对象插入到哈希容器(如 unordered_map 或 unordered_set)时生成合适的哈希值。
接下来我们自己定义一个hash函数:
template <class T>
void hash_combine(std::size_t &seed, const T &v)
{
std::hash<T> hasher;
seed ^= std::hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
return seed;
}
friend size_t hash_value(const Point& obj) {
size_t seed = 0x725C686F;
hash_combine(seed, obj.x);
hash_combine(seed, obj.y);
return seed;
}
friend size_t hash_value(const Line& obj) {
size_t seed = 0x719E6B16;
hash_combine(seed, obj.start);
hash_combine(seed, obj.end);
return seed;
}
我们已经为 Point 和 Line 结构提供了 hash_value 函数来计算哈希值。为了在代码中使用 std::hash,需要提供特化版本,如下所示:(不做特化处理会有报错: use of deleted function ‘std::hash<AP_6_3::Point>::hash())
// Point和Line均在namespace AP_6_3中定义
namespace std {
template<> struct hash<AP_6_3::Point> {
std::size_t operator()(const AP_6_3::Point& p) const {
return hash_value(p);
}
};
template<> struct hash<AP_6_3::Line> {
std::size_t operator()(const AP_6_3::Line& l) const {
return hash_value(l);
}
};
}
把上面的代码替换为我们自己实现的哈希函数即可。(这样我们就不需要去使用Boost库了)
开发带有用户界面(User Interface, UI)的应用程序时,常常碰到的一个问题是如何将UI的输入映射为适当的变量。例如,根据程序的设计,要求输入数字的文本框会将其内部的状态保存为字符串,而我们想要将输入的值记录为数字,然后验证该输入是否为有效的数字。
通常,我们需要i的时双向绑定:UI的输入会修改底层变量(例如,类的某个成员),但同时,如果底层的变量被修改,那么UI也将相应地更新。
我们定义一个独立的双向转换器,并将其作为基类,例如:
template<typename TFrom, typename TTo>
class Converter {
virtual TTo Convert(const TFrom& from) = 0;
virtual TFrom ConvertBack(const TTo& to) = 0;
};
这样,我们就可以在两种类型之间显式地定义具体的转换器,比如整数和字符串的转换器:
class IntToStringConverter: Converter<int, std::string>
{
public:
std::string Convert(const int &from) override {
return std::to_string(from);
}
int ConvertBack(const std::string &to) override {
int result;
try {
result = stoi(to);
} catch(...) {
return std::numeric_limits<int>::min();
}
return result;
}
};
接下来就可以使用它了:
void testConverter() {
IntToStringConverter converter;
std::cout << "123 to string is = " <<converter.Convert(123) << "\n";
std::cout << "456 to int is = " <<converter.ConvertBack("456") << "\n";
std::cout << "xyz to int is = " <<converter.ConvertBack("xyz") << "\n";
}
最后的一个例子很有趣,其打印的结果表明,如果转换器不能将输入的字符串解析为整数,那么就返回int所能表示的最小值作为转换结果。在实际开发中,这并不是最好的方式,实际上,大多数场景都会提前对输入参数进行检查验证,并对无效的输入报告错误信息。
在实际开发中,我们需要同时处理许多问题:不仅要用适配器来转换参数,还要进行验证,并在相关参数发生变化时自动完成转换(通过观察者模式)。
“适配器“是一个非常简单的概念:它允许我们将已有的接口调整(适配)为我们需要的另一个接口。适配器模式存在的真正问题是,在适配过程中,有时会生成临时数据以满足其他接口的要求。当发生这种情况时,我们可以采用缓存策略,确保只在必要时生成新的数据。当缓存的数据发生变化时,需要清理缓存中过时的数据。
我们尚未提及的一个主题时懒汉式适配器,我们之前实现的适配器总是在适配器创建时就完成适配转换。如果只希望在使用适配器时才完成适配转换的工作,又该如何呢?这个问题很简单。(懒汉式适配器)
#include<iostream>
#include<vector>
#include<memory>
struct Point {
int x;
int y;
};
struct Line
{
Point start;
Point end;
};
struct VectorObject
{
virtual std::vector<Line>::iterator begin() = 0;
virtual std::vector<Line>::iterator end() = 0;
};
struct VectorRectangle : VectorObject {
/*
*(0,y+h)---------(x+w, y+h)
* | |
* | |
* (x,y) ---------(x+w, y)
*/
VectorRectangle(int x, int y, int width, int height) {
lines.emplace_back(Line{Point{x, y}, Point{x + width, y}});
lines.emplace_back(Line{Point{x + width, y}, Point{x + width, y + height}});
lines.emplace_back(Line{Point{x + width, y + height}, Point{x, y + height}});
lines.emplace_back(Line{Point{x, y + height}, Point{x, y}});
}
std::vector<Line>::iterator begin() override {
return lines.begin();
}
std::vector<Line>::iterator end() override {
return lines.end();
}
private:
std::vector<Line> lines;
};
// 用于测试打印数据
struct CPaintDC {
void SetPixel(int x, int y , int color) {
printf("Point(x, y, color): (%d, %d, %d)\n", x, y, color);
}
};
void DrawPoints(CPaintDC& dc, std::vector<Point>::iterator start,
std::vector<Point>::iterator end) {
for (auto i = start; i != end; i++) {
dc.SetPixel(i->x, i->y, 0);
}
}
std::vector<std::shared_ptr<VectorObject>> vectorObjects {
std::make_shared<VectorRectangle>(10, 10, 100, 100),
std::make_shared<VectorRectangle>(30, 30, 60, 60)
};
struct LineToPointAdapter {
typedef std::vector<Point> Points;
LineToPointAdapter(const Line& line) {
int left = std::min(line.start.x, line.end.x);
int right = std::max(line.start.x, line.end.x);
int top = std::min(line.start.y, line.end.y);
int bottom = std::max(line.start.y, line.end.y);
int dx = right - left;
int dy = line.end.y - line.start.y;
// we only support vertical or horizontal lines
if (dx == 0) { // vertical line
for (int y = top; y <= bottom; ++y) {
points.emplace_back(Point{left,y});
}
} else if (dy == 0) { // horizontal lines
for (int x = left; x <= right; ++x) {
points.emplace_back(Point{left,x});
}
}
}
virtual Points::iterator begin() { return points.begin();}
virtual Points::iterator end() { return points.end();}
private:
Points points;
};
void testAdapter6_2() {
CPaintDC dc;
for (const auto &obj: vectorObjects) {
for (const auto& line: *obj) {
LineToPointAdapter lpo{line};
DrawPoints(dc, lpo.begin(), lpo.end());
}
}
}
void testAdapter6_3() {
static std::vector<Point> points;
CPaintDC dc;
for (auto& o: vectorObjects) {
for (auto& l: *o) {
LineToPointAdapter lpo{l};
for (auto& p : lpo) {
points.push_back(p);
}
}
}
DrawPoints(dc, points.begin(), points.end());
}
#if 1
#include<functional>
#include<map>
namespace AP_6_3 {
template <class T>
void hash_combine(std::size_t &seed, const T &v)
{
std::hash<T> hasher;
seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
struct Point {
int x, y;
friend size_t hash_value(const Point& obj) {
size_t seed = 0x725C686F;
// boost
// boost::hash_combine(seed, obj.x);
// boost::hash_combine(seed, obj.y);
hash_combine(seed, obj.x);
hash_combine(seed, obj.y);
return seed;
}
};
struct Line {
Point start, end;
friend size_t hash_value(const Line& obj) {
size_t seed = 0x719E6B16;
// boost
// boost::hash_combine(seed, obj.start);
// boost::hash_combine(seed, obj.end);
hash_combine(seed, obj.start);
hash_combine(seed, obj.end);
return seed;
}
};
struct LineToPointCachingAdapter {
typedef std::vector<Point> Points;
static std::map<size_t, Points> cache;
size_t line_hash;
virtual Points::iterator begin() { return cache[line_hash].begin();}
virtual Points::iterator end() { return cache[line_hash].end();}
LineToPointCachingAdapter(Line& line);
};
} // AP_6_3
namespace std
{
template <>
struct hash<AP_6_3::Point>
{
std::size_t operator()(const AP_6_3::Point &p) const
{
return hash_value(p);
}
};
template <>
struct hash<AP_6_3::Line>
{
std::size_t operator()(const AP_6_3::Line &l) const
{
return hash_value(l);
}
};
}
std::map<size_t, AP_6_3::LineToPointCachingAdapter::Points> AP_6_3::LineToPointCachingAdapter::cache;
AP_6_3::LineToPointCachingAdapter::LineToPointCachingAdapter(Line &line)
{
typedef std::vector<Point> Points;
static std::hash<Line> hash;
line_hash = hash(line);
if (cache.find(line_hash) != cache.end())
{
return; // we alaready have it
}
Points points;
// same code as before
int left = std::min(line.start.x, line.end.x);
int right = std::max(line.start.x, line.end.x);
int top = std::min(line.start.y, line.end.y);
int bottom = std::max(line.start.y, line.end.y);
int dx = right - left;
int dy = line.end.y - line.start.y;
// we only support vertical or horizontal lines
if (dx == 0)
{ // vertical line
for (int y = top; y <= bottom; ++y)
{
points.emplace_back(Point{left, y});
}
}
else if (dy == 0)
{ // horizontal lines
for (int x = left; x <= right; ++x)
{
points.emplace_back(Point{left, x});
}
}
cache[line_hash] = points;
}
#endif
#include<exception>
#include<limits>
template<typename TFrom, typename TTo>
class Converter {
virtual TTo Convert(const TFrom& from) = 0;
virtual TFrom ConvertBack(const TTo& to) = 0;
};
class IntToStringConverter: Converter<int, std::string>
{
public:
std::string Convert(const int &from) override {
return std::to_string(from);
}
int ConvertBack(const std::string &to) override {
int result;
try {
result = stoi(to);
} catch(...) {
return std::numeric_limits<int>::min();
}
return result;
}
};
void testConverter() {
IntToStringConverter converter;
std::cout << "123 to string is = " <<converter.Convert(123) << "\n";
std::cout << "456 to int is = " <<converter.ConvertBack("456") << "\n";
std::cout << "xyz to int is = " <<converter.ConvertBack("xyz") << "\n";
}
int main()
{
testAdapter6_2();
testAdapter6_3();
testConverter();
return 0;
}