Skip to content

Latest commit

 

History

History
265 lines (205 loc) · 9.02 KB

File metadata and controls

265 lines (205 loc) · 9.02 KB

六、适配器

我曾经经常旅行,一个旅行适配器可以让我将欧洲插头插入英国或美国插座 1 中,这是对适配器模式的一个很好的类比:我们有一个接口,但我们想要一个不同的接口,在接口上构建一个适配器可以让我们到达我们想要的地方。

方案

这里有一个简单的例子:假设您正在使用一个非常擅长绘制像素的库。另一方面,你处理的是几何对象——直线、矩形之类的东西。您希望继续处理这些对象,但也需要渲染,因此需要使几何体适应基于像素的表示。

让我们从定义示例中的(相当简单的)域对象开始:

1   struct Point
2   {
3     int x, y;
4   };
5
6   struct Line
7   {
8     Point start, end;
9   };

现在让我们从理论上研究矢量几何。典型的矢量对象很可能是由一组Line对象定义的。我们可以只定义一对纯虚拟迭代器方法,而不是从vector<Line>继承:

1   struct VectorObject
2   {
3     virtual std::vector<Line>::iterator begin() = 0;
4     virtual std::vector<Line>::iterator end() = 0;
5   };

这样,如果您想定义一个Rectangle,您可以在一个vector<Line>类型的字段中保存一串行,并简单地公开它的端点:

 1   struct VectorRectangle : VectorObject
 2   {
 3     VectorRectangle(int x, int y, int width, int height)
 4     {
 5       lines.emplace_back(Line{ Point{x,y}, Point{x + width,y} });
 6       lines.emplace_back(Line{ Point{x + width,y}, Point{x+width, y+height} });
 7       lines.emplace_back(Line{ Point{x,y}, Point{x,y+height} });
 8       lines.emplace_back(Line{ Point{ x,y + height },
 9       Point{ x + width, y + height } });
10     }
11
12     std::vector<Line>::iterator begin() override {
13       return lines.begin();
14     }
15     std::vector<Line>::iterator end() override {
16       return lines.end();
17     }
18   private:
19     std::vector<Line> lines;
20   };

现在,这里是设置。假设我们想在屏幕上画线。长方形,甚至!不幸的是,我们不能,因为绘图的唯一界面实际上是这样的:

1   void DrawPoints(CPaintDC& dc, std::vector<Point>::iterator\
2   start, std::vector<Point>::iterator end)
3   {
4     for (auto i = start; i != end; ++i)
5       dc.SetPixel(i->x, i->y, 0);
6   }

我这里用的是 MFC(微软基础类)的 CPaintDC 类,但这是题外话。关键是我们需要像素。我们只有台词。我们需要一个适配器。

适配器

好吧,假设我们想画几个矩形:

1   vector<shared_ptr<VectorObject>> vectorObjects{
2     make_shared<VectorRectangle>(10,10,100,100),
3     make_shared<VectorRectangle>(30,30,60,60)
4   }

为了绘制这些对象,我们需要将它们中的每一个从一系列线转换成大量的点。为此,我们创建一个单独的类来存储这些点,并将它们作为一对迭代器公开:

 1   struct LineToPointAdapter
 2   {
 3     typedef vector<Point> Points;
 4
 5     LineToPointAdapter(Line& line)
 6     {
 7       // TODO
 8     }
 9
10     virtual Points::iterator begin() { return points.begin(); }
11     virtual Points::iterator end() { return points.end(); }
12   private:
13     Points points;
14   };

从一条线到多个点的转换正好发生在构造器中,所以适配器非常渴望。 2 转换的实际代码也相当简单:

 1   LineToPointAdapter(Line& line)
 2   {
 3     int left = min(line.start.x, line.end.x);
 4     int right = max(line.start.x, line.end.x);
 5     int top = min(line.start.y, line.end.y);
 6     int bottom = max(line.start.y, line.end.y);
 7     int dx = right - left;
 8     int dy = line.end.y - line.start.y;
 9
10     // only vertical or horizontal lines
11     if (dx == 0)
12     {
13       // vertical
14       for (int y = top; y <= bottom; ++y)
15       {
16         points.emplace_back(Point{ left,y });
17       }
18     }
19     else if (dy == 0)
20     {
21       for (int x = left; x <= right; ++x)
22       {
23         points.emplace_back(Point{ x, top });
24       }
25     }
26   }

上面的代码很简单:我们只处理完全垂直或水平的行,而忽略其他的。我们现在可以使用这个适配器来实际呈现一些对象。我们从示例中取出两个矩形,并简单地将它们渲染成这样:

1   for (auto& obj : vectorObjects)
2   {
3     for (auto& line : *obj)
4     {
5       LineToPointAdapter lpo{ line };
6       DrawPoints(dc, lpo.begin(), lpo.end());
7     }
8   }

太美了!我们所做的就是,对于每个 vector 对象,获取它的每条线,为那条线构造一个LineToPointAdapter,然后迭代由适配器产生的点集,将它们提供给DrawPoints()。而且很管用!(相信我,确实如此。)

临时适配器

不过,我们的代码有一个主要问题:DrawPoints()在我们可能需要的每一次屏幕刷新时都会被调用,这意味着相同行对象的相同数据会被适配器重新生成无数次。我们能做些什么呢?

一方面,我们可以在应用程序启动时预定义所有点,例如:

 1   vector<Point> points;
 2   for (auto& o : vectorObjects)
 3   {
 4     for (auto& l : *o)
 5     {
 6       LineToPointAdapter lpo{ l };
 7       for (auto& p : lpo)
 8         points.push_back(p);
 9     }
10   }

然后DrawPoints()的实现简化为

1   DrawPoints(dc, points.begin(), points.end());

但是让我们假设一下,vectorObjects的原始集合是可以改变的。缓存这些点没有意义,但是我们仍然希望避免不断地重新生成潜在的重复数据。我们该如何应对?当然是带缓存的!

首先,为了避免再生,我们需要独特的识别线的方法,这就意味着我们需要独特的识别点的方法。ReSharper 的 Generate | Hash 函数拯救了我们:

 1   struct Point
 2   {
 3     int x, y;
 4
 5     friend std::size_t hash_value(const Point& obj)
 6     {
 7       std::size_t seed = 0x725C686F;
 8       boost::hash_combine(seed, obj.x);
 9       boost::hash_combine(seed, obj.y);
10       return seed;
11     }
12   };
13
14   struct Line
15   {
16     Point start, end;
17
18     friend std::size_t hash_value(const Line& obj)
19     {
20       std::size_t seed = 0x719E6B16;
21       boost::hash_combine(seed, obj.start);
22       boost::hash_combine(seed, obj.end);
23       return seed;
24     }
25   };

在前面的例子中,我选择了 Boost 的散列实现。现在,我们可以构建一个新的LineToPointCachingAdapter来缓存这些点,并在必要时重新生成它们。除了以下细微差别之外,实现几乎是相同的。

首先,适配器现在有了一个缓存:

1   static map<size_t, Points> cache;

这里的类型size_t正是 Boost 的哈希函数返回的类型。现在,当涉及到迭代生成的点时,我们产生它们如下:

1   virtual Points::iterator begin() { return cache[line_hash].begin(); }
2   virtual Points::iterator end() { return cache[line_hash].end(); }

这是算法有趣的部分:在生成点之前,我们检查它们是否已经生成。如果他们有,我们就退出;如果没有,我们会生成它们并将其添加到缓存中:

 1   LineToPointCachingAdapter(Line& line)
 2   {
 3     static boost::hash<Line> hash;
 4     line_hash = hash(line); // note: line_hash is a field!
 5     if (cache.find(line_hash) != cache.end())
 6       return; // we already have it
 7
 8     Points points;
 9
10     // same code as before
11
12     cache[line_hash] = points;
13   }

耶!多亏了哈希函数和缓存,我们大大减少了转换的次数。剩下的唯一问题是在不再需要旧点后将其删除。这个具有挑战性的问题留给读者做练习。

摘要

适配器是一个非常简单的概念:它允许您将您拥有的接口适配到您需要的接口。适配器唯一真正的问题是,在适配过程中,有时您最终会生成临时数据,以满足一些其他的数据表示。当这种情况发生时,转向缓存:确保新数据只在必要时生成。哦,如果您想在缓存的对象发生变化时清理过时的数据,您还需要做更多的工作。

我们还没有真正解决的另一个问题是懒惰:当前的适配器实现在创建转换时就执行转换。如果您只想在实际使用适配器时完成工作,该怎么办?这很容易做到,留给读者作为练习。

Footnotes 1

以防你像我一样是欧洲人,想抱怨每个人都应该使用欧洲插座:不;英国的设计在技术上更好,也更安全,所以如果我们真的只想要一个标准,英国的将会是我们的首选。

  2

我们能让适配器变得懒惰吗?当然,我们可以只在本地保存line(因为它是一个引用,我们不希望它过时或改变),然后,每当有人调用begin()时,如果还没有初始化,就执行初始化。然而,如果我们有几个适配器成员,这个初始化检查必须在每个成员中重复。