![image](https://habrastorage.org/webt/lf/jd/9v/lfjd9v3si55yrluchh6dabb7wpu.jpeg)
Знание правил шахматной игры еще не делает человека гроссмейстером, знание языка программирования еще не делает человека программистом. А чего в обоих случаях недостает? Ищем ответы на оба вопроса у признанных мастеров и пытаемся иллюстрировать собственными примерами.
“- А что такое, товарищи, значит идея? Идея, товарищи, — это человеческая мысль, облеченная в логическую шахматную форму. Даже с ничтожными силами можно овладеть всей доской. Все зависит от каждого индивидуума в отдельности. Например, вон тот блондинчик в третьем ряду. Положим, он играет хорошо…Программирование подобно шахматам в том смысле, что знание “правил игры” (язык а программирования, стандартных библиотек) не гарантирует написания хорошего кода. Это, увы, очевидно всякому, кто провел хотя бы пару десятков интервью на программистскую позицию. Завет Великого Комбинатора (образование – ничто, главное опыт) работает не всегда – человек может иметь 10 лет однотипного опыта написания каких-нибудь форм (вряд ли силен шахматист, 10 лет подряд игравший одну и ту же партию).
Блондин в третьем ряду зарделся.
— А вон тот брюнет, допустим, хуже.
Все повернулись и осмотрели также брюнета.
— Что же мы видим, товарищи? Мы видим, что блондин играет хорошо, а брюнет играет плохо. И никакие лекции не изменят этого соотношения сил, если каждый индивидуум в отдельности не будет постоянно тренироваться в шашк… то есть я хотел сказать — в шахматы… “
И.Ильф, Е.Петров “12 стульев”
А вот слова маэстро про идею мне кажутся важными. Ибо программа, товарищи, – это человеческая мысль, воплощенная в форму кода. Соответственно, качественный код возникает в результате воплощения качественной мысли (ну или, как минимум, хоть какой-нибудь мысли).
Я собираюсь показать на простом примере, как увидеть эту самую мысль за строками (или, скорее, между строк) программы и почему такое “видение” помогает писать качественные программы. Скажу сразу – описанной технике (связанной в первую очередь с именами Дейкстры и Хоара) десятки лет, и она, к сожалению, так и не стала “мэйнстримом” программистского образования. Но меня не оставляет надежда, что еще пара убедительных и доступных примеров — и лед тронется…
Редкая книга по введению в программирование обходится без задачи о ханойских башнях. Сейчас мы закодируем совершенно “боковой” фрагмент такой программы. Нам даже необязательно знать, в чем состоит “полная версия” игры в ханойские башни, а в нашем языке программирования будет всего две команды. Итак.
Дано. На трехцветном экране изображены три штыря (1, 2, 3), на первый штырь надето кольцо.
![](https://habrastorage.org/webt/lg/hk/ug/lghkugk7jzs28763cel1skf7xxs.png)
Штыри черные, кольцо серое, фон белый. Доступны две команды:
Штырь (номер, цвет)
Кольцо(номер, цвет)
Каждая команда заливает соответствующий прямоугольник соответствующим цветом. Например, если подать команду Кольцо(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 становится очевидным:
![](https://habrastorage.org/webt/er/uh/xf/eruhxf8ftlg8prhaotaqovbfbum.png)
Программа С действует примерно в том же порядке, что и А, только она откладывает устранение мелких недоделок на потом. В ней нет понятного промежуточного состояния, вследствие чего внимание читающего постоянно перескакивает с первого штыря на второй и обратно – чтоб понять, что происходит, следует держать в голове одновременно все три команды и всю модифицируемую часть картинки. Возможно, опытный программист избегает этого пути опять-таки из экономии умственных усилий – лучше уж мы поскорее решим все проблемы со стартовым штырем и забудем о них.
Практические проблемы с программой C менее очевидны, чем в случае B. Представим себе, что позднее заказчик программы пожелал возможности переноса кольца с произвольного штыря s (предполагая, что оно там есть) на произвольный штырь d. Нам потребуется модификация первоначальной программы (рефакторинг, да). Естественной идеей кажется заменить везде константу 1 на переменную s и константу 2 на переменную d. Оказывается, что программа A такой рефакторинг прекрасно выдерживает, а программа C нет – она не будет работать корректно при s=d (проверьте!).
Наблюдение. Обратите внимание, что выбрать правильный вариант кодирования существенно легче, чем понять, где сломаются остальные варианты (если мы конечно, умеем читать программу между строк — осознанно или интуитивно). На наш взгляд, программистская квалификация в первую очередь состоит не в умении понять, где в программе ошибка, а в умении написать программу так, чтоб вероятность ошибки была минимальной. Вспомним поговорку про находчивого и мудрого: находчивый умеет выпутываться из сложных ситуаций, а мудрый в них просто не попадает.
В заключение, еще один вариант все той же программы, демонстрирующей некоторый водораздел между наукой и производством.
Кольцо(1, белый)
Кольцо(2, белый)
Кольцо(3, белый)
Штырь (1, белый)
Штырь (2, белый)
Штырь (3, белый)
// сплошной белый фон
Штырь (1, черный)
Штырь (2, черный)
Штырь (3, черный)
// три пустых штыря
Кольцо(2, серый)
// кольцо на штыре 2
Такое решение вряд ли уместно в университетском курсе или на интервью, но на производстве может быть пригодно. Автор программы честно признал, что не сумел вычислить правильное “инкрементное изменение” и нарисовал все с чистого листа. В отличие от авторов программ В и С, он, видимо, сумел разглядеть потенциальные трудности, но не имел времени или возможности справиться с ними более эффективно. Бесполезно расчитывать на то, что в ближайшее время найдется достаточно программистов, пишущих гениальный код. Но, может быть, мы хотя бы можем научить большинство кодеров писать “тупой” код вроде этого?
Комментарии (25)
wntgd
15.03.2018 00:36Как видим, и здесь можно описать предполагаемое промежуточное состояние. И это описание должно мгновенно зажечь тревожный сигнал для разработчика – по условию задачи у нас есть только одно кольцо, не два.
Но и не ни одного! Так что на мой взгляд решения А и Б правильны и логичны. Просто в Б нам важна сохранность данных. Поэтому мы сначала копируем кольцо, а лишь потом удаляем исходное.2_bytes Автор
15.03.2018 00:51А Вы возьмите три башенки и руками колечко переставьте. В какой-то момент на башенках не будет ни одного, правда?
wntgd
15.03.2018 02:15увы, но программа это далеко не точное отображение реальности, очень далеко.
Руками мне к примеру вообще не придется столбик домалевывать.
Я лишь хочу сказать, что правильный ответ зависит от конкретной ситуации и наших потребностей, поэтому нельзя однозначно утверждать А>Б
План B предполагает некую “виртуальную реальность”, непротиворечивость которой требует аккуратного анализа. Два кольца на соседних штырях – а они там поместятся? После такого вопроса провальный тест для программы B становится очевидным:
А что если для меня так же очевиден провал А. Мы передвигаем столбики примерно так
__|__ | |
Теперь после первых двух шагов А у нас нет кольца, а третий мы не можем выполнить, потому что столбик 3 мешает. В итоге остаемся ни с чем (ну или с бекапами). В варианте Б кольцо останется на столбике, мы ничего не теряем. Но опять же, это додумывание условий. И додумать можно и до А, и до Б.mayorovp
15.03.2018 08:57В данном случае речь все-таки идет о рисовании, а не о обработке данных. Нарисовать заданную картинку заново можно всегда, бэкапы тут не нужны.
2_bytes Автор
15.03.2018 14:59увы, но программа это далеко не точное отображение реальности
Конечно, нет. И вообще программа и реальность — вещи, слава богу, разные пока. Речь была о том, что придумывание отображения программы в реальность помогает оценить непротиворечивость объектной модели, поскольку непротиворечивоть реальности тогда за нас. Я б сказал, что это такой brain design pattern — concept reuse: мы придумываем привязку к тому, про что уже известно, что оно работает, и тем самым экономим ресурсы мозга (не тратя их на всесторонний анализ чего-то совершенно нового).
При этом да, вариантов проекции в реальность может быть более одного. По поводу Вашего варианта (в пределах этой игрушечной задачи) я имею такие сомнения:
1. Ваш test case демонстрирует задачу, которая в принципе не имеет решения (как выглядит финальная картинка?). Кейс, о котором шла речь в статье, имеет решение — финальная картинка понятна, просто надо не испортить ее по дороге.
2. Ваша проекция — это не проекция в реальность, это проекция в другой программистский опыт (что за бэкап на реальном объекте, типа деревянного кружочка?).
3. Если уж говорить про бэкап, использование одного и того же места и для бэкапа и для работы — вряд ли хорошая практика (см. первую букву в слове SOLID).wntgd
15.03.2018 15:25полностью согласен с первым абзацем. Пример как по мне чуть не подошел, потому что в статье про рисовать, а в первом ответе Вы уже просите передвигать. И пойди пойми, что здесь важно а что художественное описание.
насчет сомнений
1. расстояние между столбиками должен гарантировать к примеру другой юнит тест. Цель была показать, что после «выброса исключения» отсутствие кольца не всегда более желаемое состояние, чем дубликат.
2. так и задумывалось. Первая отсылка к реальным объектам (производство) идет уже после самой задачи, потому я не брал ее за дано.
3. про место я не сказал ни слова.
4eyes
15.03.2018 16:14Программисты, решившие с пару задачек на атомарные транзакции, например что-нибудь вроде exception-safe операции добавления элемента в конец непрерывного массива (с++ std::vector::push_back) могут почувствовать себя Буридановым ослом, выбирая между А и Б.
Мотивация: нефиг трогать 1 столбик до того, как 2 столбик получит кольцо. Связано с тем, что это упрощает откат к последнему корректному состоянию. В нашем случае, если перемещение было прервано исключением линейкой-по-рукам, то можно ойкнуть и сразу убежать — башенки останутся в корректном состоянии.
Sinatr
15.03.2018 13:29+1Просто в Б нам важна сохранность данных
Интересно. Транзакция? Что если произойдет сбой после отрисовки кольца и до того, как исходное удалено? На экране будет 2 кольца. Вы об этом не подумали?
Вообще я заметил, что нам (программистам) свойственно «додумывать» различные условия и крайности. Это хорошо видно на stackoverflow, когда плохо сформулированные вопросы неожиданно получают несколько ответов, решающих разные проблемы.
В вашем случае, выбрав Б, вы «додумываете» некую «сохранностить данных». В моем случае, выбирая А (см. мой комментарий ниже), я «додумываю» некую синхронность отрисовки, типа кольцо сначала должно быть убрано, а уже потом добавлено.wntgd
15.03.2018 15:35Вот Вы сами додумали какую-то транзакцию. Там чуточку другой калибр.
Самый простой пример: я переношу через сеть объект(кольцо) из одной бд(штыря) в другую.
Отослав пакет я не буду ничего удалять пока не придет подтверждение. В худшем случае у меня будет дубликат в базе. (надеюсь это не окажется база транзакций :D) И тогда лучше пусть так, чем ничего.
Разумеется пример надуманный, ровно как и пример кольцами и все остальные.
Собственно мою мысль Вы сами же сказали в последнем абзаце, кто куда додумает, тот туда и придет.
MadLord
15.03.2018 07:19Насчет «тупого» кода правильно сказано. Очень часто не всегда можно оценить все недостатки алгоритма, поэтому часто изначально пишу самый тупой код, добиваюсь его четкой работы и только после этого перехожу к оптимизации с учетом выявленных проблем.
2_bytes Автор
15.03.2018 14:31Ага. По-моему у Дейстры в «Дисциплине программирования» этот принцип сформулирован — сначала сделаем программу работающей, а уж потом работающей быстро.
Lachrimae
15.03.2018 11:40-1Либо я неправильный, либо лыжи не едут. Попробовав представить, что выйдет в результате варианта А и получил, что мы:
1) стираем середину первого штыря (первое-то кольцо на штыре 1 никуда не девается);
2) рисуем новый штырь поверх двух разноцветных колец на 1 штыре, и при этом получается, что у нас штырь как бы огибает первое серое кольцо;
3) надеваем на 2 штырь кольцо.mayorovp
15.03.2018 11:53Не середину стираем, а нижнюю часть. В задаче под «кольцом» понимается прямоугольная область изображения, находящаяся в фиксированном месте, не зависящем от текущего состояния картинки.
Sinatr
15.03.2018 13:15Хорошее вступление… Последнее предложение — кто это «мы»? Я? Я вообще не понял смысла. Вы ввели определенные ограничения в «вашу игру» — даны 2 команды и поставили задачу с помощью их нарисовать что-то. Круто! И?
«Сильный» программист просто знает правильный порядок, допустим, если бы были задержки при синхронной отрисовке или анимации, то правильный порядок был бы «убрать кольцо с 1» (требует 2 команды, из-за того, что прямоугольники накладываются друг на друга оптимальный порядок единственный) и «добавить кольцо к 2», т.к. тогда кольцо сначало было бы убрано, а потом надето, как в реальности. Несмотря на то, что вы добавили условие «нет анимаций» именно этот путь наиболее «логичный».
Последняя программа тоже логична, она просто сперва очищает экран, а потом рисует то состояние, которое нужно. Это может быть более оптимальный путь, если ввести несколько уровней абстракции, к примеру, добавить очередь/список команд, при изменение которого потребуется перерисовка, перед которой экран всегда будет очищаеться. Повысится читаемость, модифицируемость и поддерживаемость программы за счет производительности. Что лучше — зависит…
Вообще понять мысли разглядывая код написанный другим, если только он не намного ниже вашего уровня мышления/программирование, это сложная задача. Комментарии и использование стандарных паттернов/именований облегчает задачу.
Мне кажется, в статье хорошо освещена проблема (мне сложно представить мысли человека, выбравшего В или С). А вот анализа и решения (что с этим делать) — увы нет.2_bytes Автор
15.03.2018 14:27Спасибо за детальную рецензию. Возможно, и вправду стоит написать более подробную мораль в конце этой истории, просто опасаюсь переборщить — статья и так-то вышла довольно морализаторская. Если коротко:
«Мы» (по отношению к тупым программам) — это, допустим, тимлиды, которых задолбала демонстрация изобретательности, ведущая к нетривиальным багам. А ежели не тимлиды, то буквально мы все, на которых эти баги потом высыпаются в виде неработающего чего угодно (ибо внутри этого чего угодно в наше время наверняка сидит какая-нибудь такая программа).
«Сильный» программист просто знает правильный порядок…
мне сложно представить мысли человека, выбравшего В или С
Все так и есть. Но тот факт, что мысли для случаев В, С представить сложно не спасает от появления таких программ. То есть то, что этому сильному «просто», другому, оказывается, совсем непросто (даже в таком, казалось бы, игрушечном случае). Вопрос как раз в том, можно ли это вот «просто» как-то вербализовать и поделиться с теми, кому непросто? Ну вот ровно это я и попытался сделать.
nikitadanilov
17.03.2018 17:08-1«Сильный» программист, наверное, заметит, что команды X(номер, цвет) для разных значений параметра «номер» коммутативны (могут выполняться в произвольном порядке), а для одинаковых — нет. Это наблюдение сразу дает все три варианта A, B и C. Ну и по завету Дейкстры («детерминизм — частный случай недетерминизма»), напишет параллельную программу, для которой ABC являются сериализациями.
crmMaster
Это что за производство, в котором оверхед в 3 раза от нормального алгоритма приемлем? производство ханойских башен? :)
2_bytes Автор
Ну вот как-то на той планете, где я живу это далеко не самый страшный оверхед. Вы ж понимаете, что просто щелкая какой-нибудь DoubleBuffering в WinForms Вы мгновенно получаете двойной оверхед. Вот когда кто-нибудь невзначай забабахивает квадратичный (а то и кубичный) алгоритм вместо линейного — это да, жесть. И то живет порой в релизе до поры до времени.
humbug
Вообще-то надо аппроксимировать финт "Два кольца на соседних штырях – а они там поместятся?" до финта:
Тогда ни решение А ни В ни С не дадут верного решения. И только ваш этот "оверхед в 3 раза", над которым вы похихикали, сделает все так, как задумано.
Кстати, решение А является подмножеством решения на прозводстве, ибо смысл решения А в восстановлении картины без колец вообще.
crmMaster
При чем тут физические размеры колец? В api для задачи нет упоминаний размеров вообще.
Вы можете сколь угодно долго ловить коней в вакууме, но в реальной разработке требуется решение конкретных задач, а программы работают на конкретных платформах.
Вы же не думаете о том, что будет, если завтра появится новая чудо архитектура — вы просто уверены в том, что вместе с архитектурой появится и компилятор, так вот и тут тоже самое — вы не должны думать о том, что ваша программа должна работать на всем существующем множестве колец и штырей — вы просто должны решить конкретную задачу, у которой конкретные входные данные и конкретное ограниченное апи. Свое «а что если» оставьте пет проектам.
yrHeTaTeJlb
Пока профайлер не указал что это бутылочное горлышко всей системы, все нормально. "Преждевременная оптимизация..."©