Pull to refresh

Супер-выразительный код с привлечением уровней абстракций

Reading time5 min
Views8.4K

Предлагаю вашему вниманию перевод статьи Super expressive code by Raising Levels of Abstraction


Этим постом я хочу предложить технику трансформации неясного кода в элегантный и выразительный, основанной на уровнях абстракции.


Проблема


Ниже будет представлен проблемный код. Мы преобразим этот невыразительный и непонятный код в ясный и элегантный.


Пользователь нашего приложения планирует поездку через несколько городов в стране.


Он едет сразу из одного города в другой без остановки, если они достаточно близкие (скажем, на расстоянии до 100 км), иначе он делает ровно одну остановку между городами.


Предположим, у нас есть запланированный маршрут в виде коллекции городов.


Наша задача рассчитать необходимое количество остановок, экономя время на поезду.


Приложение уже имеет класс 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;
}

Вы наверняка согласитесь, что этот кусок кода довольно туманный, и средний читатель этого кода потратит некоторое время на то, чтобы понять, что же происходит в коде. К сожалению, это невыдуманный пример того, что мы можем обнаружить в коде реального приложения. И если подобный код расположен в таком месте, которое часто изучают и меняют, неясность становится настоящей проблемой.


Давайте поработаем над этим куском кода и превратим его в ваш актив.


Создаём ясный код


Создание ясного кода — это одно из положительных последствий привлечения уровней абстракций, которое, как я считаю, является важным принципом дизайна хорошего кода.


Во многих случаях неиспользование уровней абстракций возникает там, где код более низкого уровня вставляется среди кода среднего или высокого уровня. Другими словами, проблемой является код, который описывает как производится результат вместо что делается. Чтобы улучшить такой код, нам нужно повысить уровень абстракций.


Чтобы так сделать, мы можем применить следующую технику:


Определить, какие вещи делает код, и заменить их осмысленными метками


Это приведёт к значительному улучшению ясности кода.


Проблема вышеприведённого кода заключается в том, что он не подсказывает, что он значит — этот код не ясный. Давайте применим вышеозначенную технику для улучшения ясности кода: определим, какие вещи делает код, и пометим каждую такую вещь.


Начнём с логики цикла.


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 больше не указывает на конец коллекции, и в этом случае производятся вычисления.


Мы сразу не могли сказать, что этот код понятный. Но после описания трюка мы можем определённо сказать, что он делает: он производит манипуляции с двумя смежными элементами за раз.


Давайте теперь рассмотрим другой кусок кода в условии:


it1->getGeographicalAttributes().getLocation().distanceTo(
    it2->getGeographicalAttributes().getLocation()) > MaxDistance

Сам по себе этот код довольно легко проанализировать и понять, что он делает. Он определяет, что между двух городов расстояние больше, чем MaxDistance.


И наконец, проанализируем остаток кода, переменную nbBreaks:


int nbBreaks = 0;
for (...)
{
       if(...)
       {
           ++nbBreaks;
       }
}
return nbBreaks;

Этот код увеличивает переменную каждый раз, когда выполняется условие. Он вычисляет число, сколько раз условие было выполнено.


В итоге получаем такие метки, которые описывают, что делает функция:


  • Манипуляции смежными элементами,
  • Определение, что два города находятся на расстоянии большем MaxDistance,
  • Число, сколько раз условие было удовлетворено.

После того, как анализ сделан, остался только один шаг до превращения неясного кода в выразительный.


Методика заключается в назначении метки на каждую сущность, реализуемой кодом, и заменой соответствующего кода этой меткой. Здесь мы проделаем следующее:


  • Для манипуляции смежными элементами мы можем создать компонент, который мы назовём consecutive, трансформирующим коллекцию элементов в коллекцию пар элементов, каждая пара будет иметь элемент из изначальной коллекции и следующий за ним элемент. Если маршрут route содержит {A, B, C, D, E}, consecutive(route) будет создавать {(A, B), (B, C), (C, D), (D, E)}. Подобный адаптер, создающий пары смежных элементов, недавно был добавлен под именем sliding в популярную библиотеку range-v3.
  • Для определения превышения расстояния MaxDistance между двумя городами мы можем использовать функциональный объект (functor), назовём его 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;
    };
  • Для вычисления числа, сколько раз условие было удовлетворено, мы можем использовать алгоритм count_if из STL.

Вот, что в итоге получается после замены кода метками:


int computeNumberOfBreaks(const std::vector<City>& route)
{
    static const double MaxDistance = 100;

    return count_if(consecutive(route), FartherThan(MaxDistance));
}

(Примечание: count_if из STL принимает два итератора от начала и до конца коллекции. Здесь используется обёртка count_if над std::count_if, которая передаёт в стандартный std::count_if до C++17 начало и конец коллекции.)


Этот код явно показывает, что он делает и не перемешивает уровни абстракций. По этой причине он намного выразительнее и яснее, чем изначальный код. Изначальный код только описывал, как он делает свою работу, а остальное понимание оставалось на читателе.


Эта техника может применяться ко многим частям неясного кода, чтобы преобразовать его в ясный. Она так же может быть применена для других языков программирования. В следующий раз, когда вы наткнётесь на неясный код, который вам захочется отрефакторить, попробуйте идентифицировать вещи, который делает код, и присвойте им имена. Вы будет поражены достигнутым результатом.

Tags:
Hubs:
Total votes 12: ↑9 and ↓3+6
Comments27

Articles