Fluent C++:通过提高抽象级别实现超级富有表现力的代码

原文

在这篇文章中,我想提出一种基于抽象级别的技术,可以将晦涩的代码片段转换为富有表现力的优雅代码。

示例

这里是挑战的代码。我们将使用将不清晰的代码转换为具有表现力和优雅的代码的技术来解决这个问题。如果你已经接受了挑战,那么你可以跳到下一节,那里会展示这项技术。

你的应用程序的用户正在计划一次横跨全国几个城市的旅行。

如果两座城市的距离足够近(比如在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 ++以外的其他语言来应用。 因此,下次你偶然发现想要重构的晦涩的代码时,请考虑确定代码的作用,并在每个代码上加上标签。 你应该会对结果感到惊喜。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容