Raising Levels of Abstractionによる記事「 Super expressive code 」の翻訳に注目してください
この投稿では、抽象化レベルに基づいて、あいまいなコードをエレガントで表現力豊かなコードに変換する手法を提案したいと思います。
問題
問題コードを以下に示します。 この表現力に欠ける理解できないコードを、明確でエレガントなコードに変換します。
このアプリケーションのユーザーは、国内の複数の都市を旅行する予定です。
彼は、ある都市から別の都市に十分に近い場合(100 kmまでの距離など)停止せずにまっすぐに移動します。
都市の集合の形で計画されたルートがあると仮定します。
私たちのタスクは、必要なストップ数を計算し、電車の時間を節約することです。
アプリケーションには、ルート上の都市を記述するCityクラスが既にあります。 Cityは地理的属性を提供します。その中にはLocationクラスによって実装された場所があります。 Locationクラスオブジェクトは、マップ上の他のLocationオブジェクトへのルートの長さを計算できます。
class Location { public: double distanceTo(const Location &other) const; ... }; class GeographicalAttributes { public: Location getLocation() const; ... }; class City { public: const GeographicalAttributes &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; }
このコードはかなり霧が多いことに同意するでしょう。このコードの平均的な読者は、コードで何が起こっているのかを理解するのにある程度の時間を費やすでしょう。 残念ながら、これは実際のアプリケーションのコードで見つけることができる発明された例ではありません。 そして、そのようなコードが頻繁に調査および変更される場所にある場合、あいまいさが本当の問題になります。
このコードに取り組み、それをあなたの資産に変えましょう。
明確なコードを作成する
明確なコードを作成することは、抽象レベルを引き付けることのプラスの結果の1つであり、これは、私の意見では、優れたコードの重要な設計原則です。
多くの場合、抽象レベルの不使用は、中間レベルまたは高レベルのコードに低レベルのコードが挿入されている場合に発生します。 言い換えれば、問題は、 何が行われるかではなく、 どの ように結果が生成されるかを記述するコードです。 このコードを改善するには、抽象化のレベルを上げる必要があります。
これを行うには、次の手法を適用できます。
コードが行うことを特定し、意味のあるラベルに置き換えます。
これにより、コードの明確性が大幅に向上します。
上記のコードの問題は、それが何を意味するかを示唆していないことです-このコードは明確ではありません。 上記の手法を適用して、コードの明瞭さを向上させましょう。コードが実行することを決定し、そのようなことをマークします。
ループのロジックから始めましょう。
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の直前の要素を指します。 始めに、it2はコレクションの終わりによって初期化され、ループの本体でit2がコレクションの終わりを指していないことがチェックされます。この場合、計算が実行されます。
このコードが明確であるとすぐには言えません。 しかし、トリックを説明した後、私たちは彼が何をするかを明確に言うことができます:彼は一度に2つの隣接する要素を操作します。
条件内の別のコードを見てみましょう。
it1->getGeographicalAttributes().getLocation().distanceTo( it2->getGeographicalAttributes().getLocation()) > MaxDistance
このコード自体は、解析してコードの機能を理解するのが非常に簡単です。 2つの都市間の距離がMaxDistance より大きいことを決定します。
最後に、残りのコードであるnbBreaks変数を分析しましょう。
int nbBreaks = 0; for (...) { if(...) { ++nbBreaks; } } return nbBreaks;
このコードは、条件が満たされるたびに変数を増分します。 条件が満たされた回数を計算します。
その結果、関数が何をするかを説明するラベルを取得します。
- 関連要素の操作
- 2つの都市の距離がMaxDistance よりも大きいと判断すると、
- 条件が満たされた回数。
分析が完了した後、あいまいなコードが表現力豊かになるまであと1つだけのステップがあります。
この手法は、コードによって実装される各エンティティにラベルを割り当て、対応するコードをこのラベルに置き換えることで構成されます。 ここでは次のことを行います。
- 隣接する要素を操作するために、コンポーネントを作成できます。これは、を呼び出して、要素のコレクションを要素のペアのコレクションに変換します。各ペアには、元のコレクションの要素とそれに続く要素があります。 ルートルートに{A、B、C、D、E}が含まれる場合、
consecutive(route)
は{(A、B)、(B、C)、(C、D)、(D、E)}を作成します。 隣接する要素のペアを作成する同様のアダプタが、最近人気のあるrange-v3ライブラリにスライドする名前で追加されました。 - 2つの都市間のMaxDistanceの距離を超えているかどうかを判断するには、ファンクターを使用します。FartherThanと呼びましょう。 C ++ 11から始まるファンクターは、基本的にラムダ関数に置き換えることができることを知っていますが、ここではアクションに名前を付ける必要があります。 ラムダ関数を使用してこれをエレガントに行うには、もう少し作業が必要です。これについては、別の投稿で説明します。
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)); }
(注:STLのcount_ifは、コレクションの先頭から末尾まで2つのイテレータを使用します。ここでは、std :: count_ifのラッパーcount_ifを使用します。
このコードは、それが何をするかを明確に示しており、抽象化のレベルを混在させていません。 このため、元のコードよりもはるかに表現力があり明確です。 元のコードは、それがどのように機能するかを説明しただけで、残りの理解は読者に残りました。
この手法は、あいまいなコードの多くの部分に適用して、クリアに変換できます。 他のプログラミング言語にも適用できます。 次にリファクタリングする不明瞭なコードに出くわしたときは、コードが行うことを特定して名前を付けてください。 あなたはその結果に驚くでしょう。