image

Знание правил шахматной игры еще не делает человека гроссмейстером, знание языка программирования еще не делает человека программистом. А чего в обоих случаях недостает? Ищем ответы на оба вопроса у признанных мастеров и пытаемся иллюстрировать собственными примерами.
“- А что такое, товарищи, значит идея? Идея, товарищи, — это человеческая мысль, облеченная в логическую шахматную форму. Даже с ничтожными силами можно овладеть всей доской. Все зависит от каждого индивидуума в отдельности. Например, вон тот блондинчик в третьем ряду. Положим, он играет хорошо…
Блондин в третьем ряду зарделся.
— А вон тот брюнет, допустим, хуже.
Все повернулись и осмотрели также брюнета.
— Что же мы видим, товарищи? Мы видим, что блондин играет хорошо, а брюнет играет плохо. И никакие лекции не изменят этого соотношения сил, если каждый индивидуум в отдельности не будет постоянно тренироваться в шашк… то есть я хотел сказать — в шахматы… “

И.Ильф, Е.Петров “12 стульев”
Программирование подобно шахматам в том смысле, что знание “правил игры” (язык а программирования, стандартных библиотек) не гарантирует написания хорошего кода. Это, увы, очевидно всякому, кто провел хотя бы пару десятков интервью на программистскую позицию. Завет Великого Комбинатора (образование – ничто, главное опыт) работает не всегда – человек может иметь 10 лет однотипного опыта написания каких-нибудь форм (вряд ли силен шахматист, 10 лет подряд игравший одну и ту же партию).

А вот слова маэстро про идею мне кажутся важными. Ибо программа, товарищи, – это человеческая мысль, воплощенная в форму кода. Соответственно, качественный код возникает в результате воплощения качественной мысли (ну или, как минимум, хоть какой-нибудь мысли).

Я собираюсь показать на простом примере, как увидеть эту самую мысль за строками (или, скорее, между строк) программы и почему такое “видение” помогает писать качественные программы. Скажу сразу – описанной технике (связанной в первую очередь с именами Дейкстры и Хоара) десятки лет, и она, к сожалению, так и не стала “мэйнстримом” программистского образования. Но меня не оставляет надежда, что еще пара убедительных и доступных примеров — и лед тронется…

Редкая книга по введению в программирование обходится без задачи о ханойских башнях. Сейчас мы закодируем совершенно “боковой” фрагмент такой программы. Нам даже необязательно знать, в чем состоит “полная версия” игры в ханойские башни, а в нашем языке программирования будет всего две команды. Итак.

Дано. На трехцветном экране изображены три штыря (1, 2, 3), на первый штырь надето кольцо.

Штыри черные, кольцо серое, фон белый. Доступны две команды:
    Штырь (номер, цвет)
    Кольцо(номер, цвет)
Каждая команда заливает соответствующий прямоугольник соответствующим цветом. Например, если подать команду Кольцо(3, белый) в описанной начальной ситуации, то у правого штыря исчезнет нижняя часть.

Требуется написать программу, в результате работы которой кольцо переместится на средний штырь. Команды работают мгновенно, никакие “эффекты анимации” невозможны и не требуются.

Попробуйте самостоятельно написать нужный фрагмент. Слишком очевидно? Тем лучше.

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

Решения
A B C
Кольцо(1, белый)
Штырь (1, черный)
Кольцо(2, серый)
Кольцо(2, серый)
Кольцо(1, белый)
Штырь (1, черный)
Кольцо(1, белый)
Кольцо(2, серый)
Штырь (1, черный)

В случайной аудитории решения A, B, C выдавались с приблизительно равной вероятностью. А вот сильные программисты писали исключительно вариант А, были удивлены, когда им сообщали, что есть другие варианты, и не могли объяснить, чем вариант А лучше. Попробуем разобраться. Для начала снабдим программу А комментариями:

    // кольцо на штыре 1
    Кольцо(1, белый)
    Штырь (1, черный)
    // все штыри пусты
    Кольцо(2, серый)
    // кольцо на штыре 2

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

Чем хороша программа А? У нее есть простое, симметричное (по отношению к номеру штыря) и естественное (так было бы и в реальности) промежуточное состояние. Состояние между первой и второй командой (испорченный первый штырь) не имеет простого описания и не соответствует реальности. Возможно? поэтому опытный программист интуитивно стремится сперва уйти из этого подозрительного состояния и лишь потом двигаться дальше.

Оокей, скажет терпеливый читатель, мы уважаем верования автора, но какой во всем этом практический смысл, помимо абстрактной морали? Программы В и С чем-то хуже на практике?

Давайте внимательнее посмотрим на программу B, используя тот же прием – прокомментируем промежуточные состояния системы.

    // кольцо на штыре 1
    Кольцо(2, серый)
    // кольцa на штырях 1 и 2 ???
    Кольцо(1, белый)
    Штырь (1, черный)

Как видим, и здесь можно описать предполагаемое промежуточное состояние. И это описание должно мгновенно зажечь тревожный сигнал для разработчика – по условию задачи у нас есть только одно кольцо, не два. Казалось бы, что за детские страхи – это software, сколько захотим, столько нарисуем. Почему опытный программист не выбирает этот путь? Возможно, потому, что экономит умственные усилия – программа А “списана” с реальности, именно так мы переставляли бы настоящее кольцо на настоящих штырях. Непротиворечивость и осуществимость плана А в каком-то смысле гарантирована природой, что позволяет сэкономить на формальных гарантиях. План B предполагает некую “виртуальную реальность”, непротиворечивость которой требует аккуратного анализа. Два кольца на соседних штырях – а они там поместятся? После такого вопроса провальный тест для программы B становится очевидным:


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

Практические проблемы с программой C менее очевидны, чем в случае B. Представим себе, что позднее заказчик программы пожелал возможности переноса кольца с произвольного штыря s (предполагая, что оно там есть) на произвольный штырь d. Нам потребуется модификация первоначальной программы (рефакторинг, да). Естественной идеей кажется заменить везде константу 1 на переменную s и константу 2 на переменную d. Оказывается, что программа A такой рефакторинг прекрасно выдерживает, а программа C нет – она не будет работать корректно при s=d (проверьте!).

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

В заключение, еще один вариант все той же программы, демонстрирующей некоторый водораздел между наукой и производством.

    Кольцо(1, белый)
    Кольцо(2, белый)
    Кольцо(3, белый)
    Штырь (1, белый)
    Штырь (2, белый)
    Штырь (3, белый)
    // сплошной белый фон
    Штырь (1, черный)
    Штырь (2, черный)
    Штырь (3, черный)
    // три пустых штыря
    Кольцо(2, серый)
    // кольцо на штыре 2

Такое решение вряд ли уместно в университетском курсе или на интервью, но на производстве может быть пригодно. Автор программы честно признал, что не сумел вычислить правильное “инкрементное изменение” и нарисовал все с чистого листа. В отличие от авторов программ В и С, он, видимо, сумел разглядеть потенциальные трудности, но не имел времени или возможности справиться с ними более эффективно. Бесполезно расчитывать на то, что в ближайшее время найдется достаточно программистов, пишущих гениальный код. Но, может быть, мы хотя бы можем научить большинство кодеров писать “тупой” код вроде этого?

Комментарии (25)


  1. crmMaster
    14.03.2018 18:37

    Это что за производство, в котором оверхед в 3 раза от нормального алгоритма приемлем? производство ханойских башен? :)


    1. 2_bytes Автор
      15.03.2018 00:48

      Ну вот как-то на той планете, где я живу это далеко не самый страшный оверхед. Вы ж понимаете, что просто щелкая какой-нибудь DoubleBuffering в WinForms Вы мгновенно получаете двойной оверхед. Вот когда кто-нибудь невзначай забабахивает квадратичный (а то и кубичный) алгоритм вместо линейного — это да, жесть. И то живет порой в релизе до поры до времени.


    1. humbug
      15.03.2018 01:43

      Вообще-то надо аппроксимировать финт "Два кольца на соседних штырях – а они там поместятся?" до финта:


      А что будет, если радиус кольца больше суммы диаметров 2 штырей и расстояний между ними, а 3 штырь очень сильно отстает от первых двух и нам надо переместить кольцо на 3 штырь?

      Тогда ни решение А ни В ни С не дадут верного решения. И только ваш этот "оверхед в 3 раза", над которым вы похихикали, сделает все так, как задумано.


      Кстати, решение А является подмножеством решения на прозводстве, ибо смысл решения А в восстановлении картины без колец вообще.


      1. crmMaster
        15.03.2018 21:49

        При чем тут физические размеры колец? В api для задачи нет упоминаний размеров вообще.
        Вы можете сколь угодно долго ловить коней в вакууме, но в реальной разработке требуется решение конкретных задач, а программы работают на конкретных платформах.
        Вы же не думаете о том, что будет, если завтра появится новая чудо архитектура — вы просто уверены в том, что вместе с архитектурой появится и компилятор, так вот и тут тоже самое — вы не должны думать о том, что ваша программа должна работать на всем существующем множестве колец и штырей — вы просто должны решить конкретную задачу, у которой конкретные входные данные и конкретное ограниченное апи. Свое «а что если» оставьте пет проектам.


    1. yrHeTaTeJlb
      15.03.2018 13:36

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


  1. Ritorno
    14.03.2018 21:52

    Честно скажу, сначала я начал говорить вариант B, но вовремя изменил его на A.


    1. Sinatr
      15.03.2018 13:21

      Было бы интересно узнать почему (интересует также мнение людей с вариантом С).


      1. Ritorno
        15.03.2018 18:54

        Как раз по тем причинам, что описаны в статье.


  1. VolCh
    14.03.2018 22:13

    Добавляем в последний вариант виртуальную башню и получаем реакт.


  1. wntgd
    15.03.2018 00:36

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

    Но и не ни одного! Так что на мой взгляд решения А и Б правильны и логичны. Просто в Б нам важна сохранность данных. Поэтому мы сначала копируем кольцо, а лишь потом удаляем исходное.


    1. 2_bytes Автор
      15.03.2018 00:51

      А Вы возьмите три башенки и руками колечко переставьте. В какой-то момент на башенках не будет ни одного, правда?


      1. wntgd
        15.03.2018 02:15

        увы, но программа это далеко не точное отображение реальности, очень далеко.
        Руками мне к примеру вообще не придется столбик домалевывать.

        Я лишь хочу сказать, что правильный ответ зависит от конкретной ситуации и наших потребностей, поэтому нельзя однозначно утверждать А>Б

        План B предполагает некую “виртуальную реальность”, непротиворечивость которой требует аккуратного анализа. Два кольца на соседних штырях – а они там поместятся? После такого вопроса провальный тест для программы B становится очевидным:

        А что если для меня так же очевиден провал А. Мы передвигаем столбики примерно так

        __|__ | |

        Теперь после первых двух шагов А у нас нет кольца, а третий мы не можем выполнить, потому что столбик 3 мешает. В итоге остаемся ни с чем (ну или с бекапами). В варианте Б кольцо останется на столбике, мы ничего не теряем. Но опять же, это додумывание условий. И додумать можно и до А, и до Б.


        1. mayorovp
          15.03.2018 08:57

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


        1. 2_bytes Автор
          15.03.2018 14:59

          увы, но программа это далеко не точное отображение реальности

          Конечно, нет. И вообще программа и реальность — вещи, слава богу, разные пока. Речь была о том, что придумывание отображения программы в реальность помогает оценить непротиворечивость объектной модели, поскольку непротиворечивоть реальности тогда за нас. Я б сказал, что это такой brain design pattern — concept reuse: мы придумываем привязку к тому, про что уже известно, что оно работает, и тем самым экономим ресурсы мозга (не тратя их на всесторонний анализ чего-то совершенно нового).

          При этом да, вариантов проекции в реальность может быть более одного. По поводу Вашего варианта (в пределах этой игрушечной задачи) я имею такие сомнения:
          1. Ваш test case демонстрирует задачу, которая в принципе не имеет решения (как выглядит финальная картинка?). Кейс, о котором шла речь в статье, имеет решение — финальная картинка понятна, просто надо не испортить ее по дороге.
          2. Ваша проекция — это не проекция в реальность, это проекция в другой программистский опыт (что за бэкап на реальном объекте, типа деревянного кружочка?).
          3. Если уж говорить про бэкап, использование одного и того же места и для бэкапа и для работы — вряд ли хорошая практика (см. первую букву в слове SOLID).


          1. wntgd
            15.03.2018 15:25

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

            насчет сомнений
            1. расстояние между столбиками должен гарантировать к примеру другой юнит тест. Цель была показать, что после «выброса исключения» отсутствие кольца не всегда более желаемое состояние, чем дубликат.
            2. так и задумывалось. Первая отсылка к реальным объектам (производство) идет уже после самой задачи, потому я не брал ее за дано.
            3. про место я не сказал ни слова.


      1. 4eyes
        15.03.2018 16:14

        Программисты, решившие с пару задачек на атомарные транзакции, например что-нибудь вроде exception-safe операции добавления элемента в конец непрерывного массива (с++ std::vector::push_back) могут почувствовать себя Буридановым ослом, выбирая между А и Б.

        Мотивация: нефиг трогать 1 столбик до того, как 2 столбик получит кольцо. Связано с тем, что это упрощает откат к последнему корректному состоянию. В нашем случае, если перемещение было прервано исключением линейкой-по-рукам, то можно ойкнуть и сразу убежать — башенки останутся в корректном состоянии.


    1. Sinatr
      15.03.2018 13:29
      +1

      Просто в Б нам важна сохранность данных
      Интересно. Транзакция? Что если произойдет сбой после отрисовки кольца и до того, как исходное удалено? На экране будет 2 кольца. Вы об этом не подумали?

      Вообще я заметил, что нам (программистам) свойственно «додумывать» различные условия и крайности. Это хорошо видно на stackoverflow, когда плохо сформулированные вопросы неожиданно получают несколько ответов, решающих разные проблемы.

      В вашем случае, выбрав Б, вы «додумываете» некую «сохранностить данных». В моем случае, выбирая А (см. мой комментарий ниже), я «додумываю» некую синхронность отрисовки, типа кольцо сначала должно быть убрано, а уже потом добавлено.


      1. wntgd
        15.03.2018 15:35

        Вот Вы сами додумали какую-то транзакцию. Там чуточку другой калибр.

        Самый простой пример: я переношу через сеть объект(кольцо) из одной бд(штыря) в другую.
        Отослав пакет я не буду ничего удалять пока не придет подтверждение. В худшем случае у меня будет дубликат в базе. (надеюсь это не окажется база транзакций :D) И тогда лучше пусть так, чем ничего.

        Разумеется пример надуманный, ровно как и пример кольцами и все остальные.

        Собственно мою мысль Вы сами же сказали в последнем абзаце, кто куда додумает, тот туда и придет.


  1. MadLord
    15.03.2018 07:19

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


    1. 2_bytes Автор
      15.03.2018 14:31

      Ага. По-моему у Дейстры в «Дисциплине программирования» этот принцип сформулирован — сначала сделаем программу работающей, а уж потом работающей быстро.


  1. Lachrimae
    15.03.2018 11:40
    -1

    Либо я неправильный, либо лыжи не едут. Попробовав представить, что выйдет в результате варианта А и получил, что мы:
    1) стираем середину первого штыря (первое-то кольцо на штыре 1 никуда не девается);
    2) рисуем новый штырь поверх двух разноцветных колец на 1 штыре, и при этом получается, что у нас штырь как бы огибает первое серое кольцо;
    3) надеваем на 2 штырь кольцо.


    1. mayorovp
      15.03.2018 11:53

      Не середину стираем, а нижнюю часть. В задаче под «кольцом» понимается прямоугольная область изображения, находящаяся в фиксированном месте, не зависящем от текущего состояния картинки.


  1. Sinatr
    15.03.2018 13:15

    Хорошее вступление… Последнее предложение — кто это «мы»? Я? Я вообще не понял смысла. Вы ввели определенные ограничения в «вашу игру» — даны 2 команды и поставили задачу с помощью их нарисовать что-то. Круто! И?

    «Сильный» программист просто знает правильный порядок, допустим, если бы были задержки при синхронной отрисовке или анимации, то правильный порядок был бы «убрать кольцо с 1» (требует 2 команды, из-за того, что прямоугольники накладываются друг на друга оптимальный порядок единственный) и «добавить кольцо к 2», т.к. тогда кольцо сначало было бы убрано, а потом надето, как в реальности. Несмотря на то, что вы добавили условие «нет анимаций» именно этот путь наиболее «логичный».

    Последняя программа тоже логична, она просто сперва очищает экран, а потом рисует то состояние, которое нужно. Это может быть более оптимальный путь, если ввести несколько уровней абстракции, к примеру, добавить очередь/список команд, при изменение которого потребуется перерисовка, перед которой экран всегда будет очищаеться. Повысится читаемость, модифицируемость и поддерживаемость программы за счет производительности. Что лучше — зависит…

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

    Мне кажется, в статье хорошо освещена проблема (мне сложно представить мысли человека, выбравшего В или С). А вот анализа и решения (что с этим делать) — увы нет.


    1. 2_bytes Автор
      15.03.2018 14:27

      Спасибо за детальную рецензию. Возможно, и вправду стоит написать более подробную мораль в конце этой истории, просто опасаюсь переборщить — статья и так-то вышла довольно морализаторская. Если коротко:

      «Мы» (по отношению к тупым программам) — это, допустим, тимлиды, которых задолбала демонстрация изобретательности, ведущая к нетривиальным багам. А ежели не тимлиды, то буквально мы все, на которых эти баги потом высыпаются в виде неработающего чего угодно (ибо внутри этого чего угодно в наше время наверняка сидит какая-нибудь такая программа).

      «Сильный» программист просто знает правильный порядок…
      мне сложно представить мысли человека, выбравшего В или С

      Все так и есть. Но тот факт, что мысли для случаев В, С представить сложно не спасает от появления таких программ. То есть то, что этому сильному «просто», другому, оказывается, совсем непросто (даже в таком, казалось бы, игрушечном случае). Вопрос как раз в том, можно ли это вот «просто» как-то вербализовать и поделиться с теми, кому непросто? Ну вот ровно это я и попытался сделать.


  1. nikitadanilov
    17.03.2018 17:08
    -1

    «Сильный» программист, наверное, заметит, что команды X(номер, цвет) для разных значений параметра «номер» коммутативны (могут выполняться в произвольном порядке), а для одинаковых — нет. Это наблюдение сразу дает все три варианта A, B и C. Ну и по завету Дейкстры («детерминизм — частный случай недетерминизма»), напишет параллельную программу, для которой ABC являются сериализациями.