在这篇文章中,我想提出一种基于抽象级别的技术,可以将晦涩的代码片段转换为富有表现力的优雅代码。
示例
这里是挑战的代码。我们将使用将不清晰的代码转换为具有表现力和优雅的代码的技术来解决这个问题。如果你已经接受了挑战,那么你可以跳到下一节,那里会展示这项技术。
你的应用程序的用户正在计划一次横跨全国几个城市的旅行。
如果两座城市的距离足够近(比如在100公里以下),他就会开车从一个城市直穿另一个城市,否则他会在两座城市之间的公路上休息一下。用户不会在两个城市之间多于一次休息。
假设我们有计划路线,以城市集合的形式出现。
你的目标是确定驾驶员必须休息多少次,例如,这对于他们的预算时间很有用。
该应用程序具有现有的组件,例如代表路线上给定城市的城市类。 城市可以提供其地理属性,其中可以用位置类来表示其位置。 位置类型的对象本身可以计算到地图上任何其他位置的行驶距离:
class Location
{
public:
double distanceTo(const Location& other) const;
...
};
class GeographicalAttributes
{
public:
Location getLocation() const;
...
};
class City
{
public:
GeographicalAttributes const& getGeographicalAttributes() const;
...
};
现在,这里是用于计算用户必须休息的次数的当前实现:
#include <vector>
int computeNumberOfBreaks(const std::vector<City>& route)
{
static const double MaxDistance = 100;
int nbBreaks = 0;
for (std::vector<City>::const_iterator it1 = route.begin(), it2 = route.end();
it1 != route.end();
it2 = it1, ++it1)
{
if (it2 != route.end())
{
if(it1->getGeographicalAttributes().getLocation().distanceTo(
it2->getGeographicalAttributes().getLocation()) > MaxDistance)
{
++nbBreaks;
}
}
}
return nbBreaks;
}
你可能会承认这段代码是相当晦涩的,而且普通读者可能需要花一些时间来了解其中的情况。 不幸的是,在现实世界中你可能经常遇到。 而且,如果这段代码位于经常读取或更新的代码行的位置,那么它将成为一个真正的问题。
让我们来研究这段代码,将其转换为你的代码资产。
使代码富有表现力
使代码具有表现力是尊重抽象级别所发生的一件好事,我认为这是设计良好代码的最重要原则。
在许多不尊重抽象级别的情况下,问题出在较高层抽象的代码中夹杂着较低层抽象代码。 换句话说,问题是描述其如何执行动作而不是执行什么动作的代码。 为了改进这样的代码,你需要提高其抽象级别。
为此,你可以应用以下技术:
确定代码在做什么,并挨个用标签替换他们
这具有显着提高代码表达能力的效果。
上面这段代码的问题在于它没有说明含义——该代码没有表现力。 让我们使用之前的指南来提高表达能力,也就是说,让我们确定代码的作用,并在每个代码上加上标签。
让我们从迭代逻辑开始:
for (std::vector<City>::const_iterator it1 = route.begin(), it2 = route.end();
it1 != route.end();
it2 = it1, ++it1)
{
if (it2 != route.end())
{
也许你之前已经看过这种技术。 这是一种操纵容器中相邻元素的技巧。 it1从begin处开始,并且it2一直沿遍历指向it1之前的元素。 为了初始化它,我们首先把它设在容器的末尾,并检查它是否不再在循环主体的末尾以实际开始工作。
无需说这段代码并不完全具有表达力。 但是现在我们已经确定了它的含义:它旨在一起操纵连续的元素。
让我们在以下情况下处理下一部分代码:
it1->getGeographicalAttributes().getLocation().distanceTo(
it2->getGeographicalAttributes().getLocation()) > MaxDistance
单独考虑这一点,就很容易分析其含义。 它确定两个城市的距离是否比MaxDistance更远。
让我们用代码的其余部分变量nbBreaks完成分析:
int nbBreaks = 0;
for (...)
{
if(...)
{
++nbBreaks;
}
}
return nbBreaks;
此处,代码根据条件使变量递增。 这意味着要计算满足条件的次数。
因此,总而言之,下面是描述函数功能的标签:
- 一起处理连续的元素,
- 确定城市之间距离是否比MaxDistance更远,
- 计算满足条件的次数。
一旦完成了这一分析,模糊的代码变成有意义的代码只是时间问题。
准则是在代码执行的每件事上都贴上标签,并用它替换相应的代码。 在这里,我们将执行以下操作:
-
对操作连续元素,我们可以创建一个称为“consecutive”的组件,该组件会将一组元素转换成一组元素对,每一对都有初始容器中的一个元素和它的下一个元素。例如,如果路由包含{A,B,C,D,E},则consecutive将包含{(A,B),(B,C),(C,D),(D,E)}。
你可以查看我在这里的实现。一种创建相邻元素的适配器,最近才加入到很流行的range-v3库中。更多相关的话题可以看这篇文章Fluent C++:Ranges:STL的高级用法。
-
为了确定两个连续的城市是否比MaxDistance距离更远,我们可以简单地使用一个函数对象(functor),我们将其称为FartherThan。我认识到,由于C ++ 11函子已被lambda取代,但是在这里我们需要给它起个名字。用lambda优雅地进行此操作需要做更多的工作,我们将在专门的文章中对此进行详细探讨:
class FartherThan { public: explicit FartherThan(double distance) : m_distance(distance) {} bool operator()(const std::pair<City, City>& cities) { return cities.first.getGeographicalAttributes().getLocation().distanceTo( cities.second.getGeographicalAttributes().getLocation()) > m_distance; } private: double m_distance; };
为了计算满足条件的次数,我们可以仅使用STL算法count_if。
这是通过用相应的标签替换代码而获得的最终结果:
int computeNumberOfBreaks(const std::vector<City>& route)
{
static const double MaxDistance = 100;
return count_if(consecutive(route), FartherThan(MaxDistance));
}
(注意:原始的count_if C ++函数会将两个迭代器指向容器的begin和end位置。此处使用的一个迭代器仅使用传递range的begin和end来调用原始版本)。
这段代码明确显示了它在做什么,并尊重抽象级别。 因此,它比最初版本的更具表达力。 最初的版本只告诉了它是如何工作的,剩下的工作留给了读者。
可以将这种技术应用于许多不清楚的代码段,以将它们变成非常有表现力的代码段。 它甚至可以用C ++以外的其他语言来应用。 因此,下次你偶然发现想要重构的晦涩的代码时,请考虑确定代码的作用,并在每个代码上加上标签。 你应该会对结果感到惊喜。