在这篇文章中,我将提出一种基于抽象层次的技术,将一段模糊的代码转换为表现力强、优雅的一段代码。
你已经有相当多的人接受了圣诞假期的挑战,写下了富有表现力的代码,在挑战贴上发表了一条评论,或在Reddit上插了一笔。非常感谢大家!各种提案引发了有趣的讨论,参与者可以相互交流和学习。
赢家
挑战的获胜者是Fred Tingaud。他的解决方案非常简单,清楚地显示了代码的含义,这就是为什么它在选择过程中获得第一名的原因。祝贺弗莱德!
如果你也要祝贺弗莱德,可以在Twitter@fredtingaud上找到他。
对于这个表达性代码的挑战,很多人都表达了非常正面的反馈。因此,对于最具表现力的代码而言,这些挑战将定期在fluentc++上提出。这样,我们将继续互相学习,争取最具表现力的代码。
比赛题
以下是挑战的守则。我们将通过将不清晰的代码转换为表现力强的优雅代码的技术来解决这个问题。如果你已经接受了挑战,那么你可以直接跳到下一节展示该技术。
应用程序的用户正计划在全国多个城市进行一次旅行。
如果距离足够近(比如不到100公里),他会直接从一座城市驶往下一座城市,否则他会在二座城市之间的公路上稍作休息。用户在二个城市之间的休息时间不会超过一次。
假设我们有以城市集合的形式规划的路线。
你的目标是确定司机需要休息多少次,例如,这对他们的时间预算很有用。
此应用程序有一些现成组件,例如代表路线上指定城市的类别City
。City
可以提供其地理属性,其中其位置由一级位置表示。而Location
类型的对象本身可以计算到地图上任何其他位置的行驶距离:
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
从开始处开始,而it2
一直指向it1
之前的元素。使用end()
初始化it2
,并在实际开始工作前检查it2
是否有效。
不用说这段代码不太具有表现力。但现在我们已经确定了它的意义:它旨在一起操纵连续的元素。
让我们处理下一段代码,条件是:
it1->getGeographicalAttributes().getLocation().distanceTo(
it2->getGeographicalAttributes().getLocation()) > 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)}。
你可以在这里看到我的实现。最近,一个这样的创建相邻元素对的适配器以sliding
的名称被添加到流行的range-v3
库中。更多关于range
的重要主题。 - 为了确定二个相邻(consecutive )的城市之间的距离是否比
MaxDistance
远,我们可以简单地使用一个称为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));
}
(注意:C++原始的
count_if
函数用2个迭代器表示一个集合的开始点和结束点)。此处使用的count_if
仅使用一个参数传递表示开始和结束range
对象)
此代码明确地显示其所做的事情,并尊重抽象级别。因此,它比最初的一个更具表现力。最初的代码只讲述了它是如何完成工作的,剩下的(理解代码的)工作则交给读者。
此技术可应用于许多不清楚的代码段,将其转换为非常有表现力的代码段。它甚至可以应用于C++以外的其他语言。因此,下次当你无意中发现要重构的模糊代码时,请考虑识别代码所做的事情,并在每一个代码上贴上标签。你应该对结果感到惊喜。
(*)代码挑战的选择过程如下:我亲自审阅所有代码建议,尽管我没有最终决定权:我向团队中最年轻的人展示各种提交的文件,他说他最容易理解哪一个。
相关文章:
Respect levels of abstraction
Ranges: the STL to the Next Level