Navigation

Искусство программирования под Unix (и не только). Часть первая, «правило модульности»

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

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

Есть ряд книг, которые, на мой взгляд, являются своеобразными «библиями» для тех, кто решил связать свое будущее с разработкой ПО. С одной из них я хотел бы начать цикл статей. Это книга Эрика Рейнмонда, «Искусство программирования под Unix». Я бы рекомендовал эту книгу не только тем, кто выбрал для себя открытые операционные системы. В основе лежит довольно универсальная философия, пригодная абсолютно всем, связавшим свою профессию с программированием.

Эрик Реймонд выделяет 17 правил этой «философии». Я буду посвящать по одной заметке на каждое правило. Я постараюсь изложить эти концепции в максимально понятной, упрощенной и популярной форме, насколько это будет возможно.

Начнем с самого первого правила — Правила модульности. Оно звучит так: «Простые блоки связывайте друг с другом ясными и понятными интерфейсами» (Rule of Modularity: Write simple parts connected by clean interfaces).

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

Два примера из реальной жизни: 1) классический домашний ПК из системного блока и монитора 2) ноутбук. К примеру, на обоих погас экран. Причина может быть в чем угодно, как мы понимаем. Но в случае домашнего ПК легко проверить первое предположение: а работает ли монитор на другом компьютере? если не работает — причина в мониторе. Если да — причина в компьютере. Далее можно попробовать видеокарту на другом компьютере - и сразу понять, в ней дело или нет. Аналогично дело обстоит с клавиатурой, мышью..

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

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

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

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

Когда-то давно, после института, я писал систему проектирования воздуховодов для рязанской компании Эковент. Там можно было создавать произвольную модель воздуховодов - труб с разветвлениями во всех трех измерениях. В какой-то момент пришлось полностью переделать всю программную часть, потому что она стала просто слишком сложной. Отдельные компоненты, отвечающие за изменение конструкции — такие как, например, добавление элемента, имели текстовый интерфейс в виде текстовых команд вида «add block width=100 height=100 depth=10». Разумеется, пользователя никто не заставлял набирать такие команды, для этого был отдельный графический оконный интерфейс, где он вводил ширину, высоту, глубину, но далее форма все равно преобразовывалась в команду. Это позволяло очень просто отлаживать сложные ситуации, когда нужно было создать сотню объектов и выполнить их отрисовку.

В UNIX-мире уже давно изобретен один унифицированный интерфейс для командной строки, называется он “каналом” (pipe, |). Через каналы можно очень просто связывать разные программы, выполняющие одну, очень простую задачу. Например, есть утилитка, которая считает число строк, символов (wc), и утилитка, которая показывает на экран содержимое файла (cat). Поставив их друг за другом, мы можем получить, например, число строк в файле (cat file | wc -l). И таких “утилиток” уже разработано очень много. Возможность комбинировать их «на лету» помогает решить довольно сложные задачи буквально за секунды.

В итоге, несколько простых советов — мои следствия правила модульности:

  • Каждый блок, программа, модуль должны делать только одну вещь и делать ее хорошо. Если цель, назначение блока начинает «размазываться», стоит задумываться о ее разделении на части.
  • Если есть возможность, выбирайте интерфейсы из числа промышленных стандартов (XML, HTTP, RPC, CSV, JSON) и ни в коем случае не выходите за их рамки.
  • Если можно упростить интерфейс за счет увеличения количества блоков — упрощайте. Пусть блоков будет больше, зато будет возможность тестировать их отдельно. Разумеется, нормально, когда блоки не сами по себе, а выстроены в иерархию.
  • Если можно упростить функционал блока, разбив его на более мелкие блоки, с более четкой выполняемой задачей, со стандартными интерфейсами, без заметного ущерба в производительности — разбивайте. Даже, если есть ущерб в производительности — тщательно продумайте, иногда им стоит жертвовать. «Железо» сейчас стоит дешевле.