вторник, 13 января 2015 г.

Роберт Мартин "Чистый код. Создание, анализ и рефакторинг"


Роберт Мартин "Чистый код. Создание, анализ и рефакторинг"
или книга о мелочах.

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

А еще я хочу ее обсудить.
Глава 3: функции и методы.

Честно говоря, резюмируя эти первые сто страниц, мне очень хочется спросить у авторов "What?..". Ок, завершение главы, меня почти помирило с автором:
"Если вы будете слеовать этим правилам, ваши функции будут короткими, удачно названными и хорошо организованными. Но не забывайте, что ваша настоящая цель - "рассказать историю" системы, а написанные функции должны четко складываться в понятный и точный язык изложения".
Ура! Я нашла то, что хотела.
Казалось бы, что плохого в коротких, удачно названных и хорошо организованных функциях?.. С ними все прекрасно, кроме того, что по примерам из главы - эти функции идеально читаемы... каждая. Отдельно. И упорядочивание одной функции не должно вносить хаоса в логику класса. А увеличение сущностей в десяток раз - это хаос, которого стоит избегать.
К сожалению, в этой главе очень мало о том, что хорошо читаться должно все вместе. А стек вызовов, для решения маленькой простой задачки, все-таки, я полагаю, не должен зашкаливать за пару десятков. Это едва ли упрощает чтение кода, поддержку и отладку... на любом этапе.

Разбор смутившего меня.

  1. Методы не должны действовать скрыто. Т.е. если метод что-то делает, это должно быть очевидно из названия. И это, я считаю, прекрасно. 
  2. Никаких секций в функции. Если функция осмысленно делится на секции, значит она точно делает более одной вещи, значит... смотри номер 3.
  3. 1 операция - 1 функция.

    Я, вообще говоря, стараюсь следовать этому правилу. Но есть нюансы...

3.1 Я полагаю, что в ряде случаев метод, ради простоты использования нашего инструментария, обязан делать достаточно большое число штук. Я даже могу рассмотреть это на примере книги.
Вот есть у нас некий метод: checkPassword(string username, string password). В нем, как следует из названия, проверяется корректность пары и... и еще он открывает новую сессию подключения. Неждачик.
И тут мы бы должны сделать тaк: checkAuthAndInitConnection(string username, string password). Чем приведем по второму правилу все в норму. Но... но упс. Теперь этот метод очевидно делает чего-то два: сначала проверяет данные, потом открывает новую сессию подключения.
А я у меня объект подключения к БД. Этот объект предоставляет пользователю интерфейс работы с базой данных. И я очень не хочу объяснять пользователю что он сначала сам должен вызвать метод checkAuth(...) и только потом вызвать метод InitConnection(...).
А иначе на него посыплются градом ошибки. Ведь первый метод не должен устанавливать подключение, а второй не должен ничего проверять... и, сэкономив радостному программисту 15 секунд (он с удовольствием найдет в интерфейсе InitConnection) я заберу у него час (найти документацию к объекту, прочитать ее и исправить ошибку), когда упадет тест на ввод неверной авторизации.

3.2. Второй нюанс. Следствие этого правила: 1 функция - 1 уровень вложенности.
По факту эта оптимизация читабельности приводит нас сначала к замене одной функции на да листа пятью функциями на те же два листа в сумме: но это, обычно здорово.
А потом к 15 функциям по 3 строки... Нет, это определенно не улучшает отладку и сопровождение. Ну правда же.

3.3. Блоки исключений. Обработка ошибок - это отдельная операция, а значит отдельная функция. И она должна быть в отдельной функции. Поэтому вместо (например):

try
{
    a = R\N;
}
catch (Exception e)
{
    MessageBox.Show("О ужас! Деление на ноль");
    a = 0;
}
finally
{
    return a;
}


мы пишем что-то вроде: return getResultOrError(a, R, N)  и здорово упрощаем себе этим жизнь. Вообще мне, после обдумывания это понравилось. Хотя предложение создать еще и :
TryRNExpression(R,N);
CatchDivisionByZero();
вызвало некототорое смущение. Которое все еще меня грызет.

4. Аргументы.
По мнению авторов функции должны быть унарные и бинарные. И если совсем никак без этого то тернарные.
И это... это оченьсильно меня смущает, хотя здесь, как нигде ранее, я предполагаю свою далекость от просвещения. Просто не всегда удается обеспечить пока архитектуру. Хотя, мне кажется, авторы излишне категоричны и это еще одна эвристика, безговорочное следование которой может привести в итоге к усложнению понимаемости кода.

Также в третьей главе авторы:
 - рассказывают про именование. И это хорошо;
- советуют нам использовать исключения, вместо возвращаемых кодов ошибок;
- между делом объяснили мне абстрактную фабрику;
- выступают против флаговых атрибутов и хорошо это аргументируют... но не вполне меня убеждают;
- не рекомендуют нам использовать выходные параметры и... и ничто меня так не бесит в делфи как функция DateTimeStringFormat;
- и упоминают много других полезных штук, с которыми я не буду спорить и которые вызвали достаточную степень (как минимум) принятия.

2 комментария:

  1. Надо будет прочитать, похоже книга ценная),

    Но пару вещей скажу и со своей точки зрения:
    3.1 А почему не сделать два уровня функций? Первый уровень как раз "элементарные", второй - удобные для пользования. (А хуже переусложжнённого API / интерфейсов вызова по моему ничего нет только велосипеды)
    И например checkAuthAndInitConnection(string username, string password) превратится в InitConnection(string username, string password), вызывающую скажем atomCheckPassword atomNewConnection (atom - как признак атомарности).

    3.3 Встречался такой подход и в целом он интересен. Но мне кажется, что внедрять его в каждую строку - это слишком. Вот высокоуровневое get_HTMLpageOrError уже нравится больше.

    4. Что-то напомнило пару статей о функциональном программировании, которые из интереса листал, там похожий стиль встречался. Но опять-таки таким образом возвращаемся к 3.1 и вместо
    sendPost(string url, HashMap headers, byte[] data, int timeout)
    получаем что-то вроде:
    mekePost(string url)
    addHeadersToPostRequest(Request request, HashMap headers)
    addTimeoutToRequest(Request request, int timeout)
    AddDataToPOSTRequest(Request request, byte[] data)
    Что уже смотрится как-то не очень: то ли из-за того, что это требует другого стиля мышления, то ли из-за того, что подход менее универсален.

    >>- советуют нам использовать исключения, вместо возвращаемых кодов ошибок;<<
    Я бы поспорил. Хотя бы в случае, когда для языка исключения достаточно дорогостоящий процесс. Другое дело, что когда одна библиотека то возвращает исключение, то код ошибки - это плохо.

    Личные может далёкие от просвещённости и очевидные вещи: общие рекомендации же! В смысле нет правильного и неправильно. На мой взгляд правильно тогда, когда код наиболее
    а) понятен
    б) удобен в использовании
    в) надёжен и предсказуем
    г) легко расширяем и модифицируем.
    А то можно и до оценки кода

    Кстати, в порядке общей мысли: интересно, в какой области работал автор? В том смысле, что часть взглядом может иметь отношение именно к специфике.

    ОтветитьУдалить
    Ответы
    1. Вообще, я об этом, наверное, еще напишу... но отвечу тебе пока тут.
      Знаешь, я дочитала еще десяток глав. Попробовала применить часть узнанного в текущем проекте, в котором запуталась.
      В общем... надо признать, я тоже была немного категорична.

      3.1. Да. я тоже уже об этом подумала. В целом, далее, авторы все же разделили интерфейсы и функциональность. В не явном виде, правда... но меня устроило.

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

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

      try
      {
      v_sogl = Convert.ToDouble(this.VSoglTB.Text);
      } catch
      {
      // формирование ошибки по принципу Какое поле не заполнено или заполнено не корректно
      }

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

      4. Ну... в твоем примере, часть этих параметров (например url) должна бы стать членами класса... предположительно класса MyRequest

      И тогда это будут методы класса с одним параметром - это в целом будет весьма аккуратно читаться. И будет точно понятно что именно ты делаешь.

      Авторы вообще не стесняются и советуют создавать обертки используемых классов. Т.е. если тебе нужен тот же Request - но не совсем такой, лучше создай обертку и встрой все свои расширения в нее.

      При исключения. КНига преимущественно имеет в виду Java и, полагаю, может легко распространяться на С#. Собственно, это важно, с учетом что ряд исходных предпосылок, например "Не бойтесь переименовывать, если нашли более клевое название" имеет в виду возможности IDE. Это тоже не везде актуально.. .ну, в качестве примера.

      Ну, собвственно авторы полностью согласны с тобой в критериях.. они просто пытаются агрегировать свой опыт в создании такого кода и рассказать какие приемы чаще всего давали им результат. Если воспринимать их так - то книга становится действительно полезной.

      Про область автора не скажу, тем более что часть глав в соавторстве написаны, но думаю можешь погуглить. )

      Удалить